From 8bc3ee305b4ffe99e67ef02fa733db83d217fb3e Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Wed, 9 Sep 2020 05:33:01 -0400 Subject: [PATCH] Cache pathfinding results to speed up calculations (#1541) --- VisualStudio/common.props | 2 +- fheroes2.vcxproj | 9 +- src/fheroes2/ai/simple/ai_simple_heroes.cpp | 4 +- src/fheroes2/dialog/dialog_quickinfo.cpp | 4 +- src/fheroes2/game/game_io.cpp | 2 - src/fheroes2/gui/interface_gamearea.cpp | 3 +- src/fheroes2/heroes/direction.cpp | 11 - src/fheroes2/heroes/direction.h | 1 - src/fheroes2/heroes/heroes.cpp | 13 +- src/fheroes2/heroes/route.cpp | 19 +- src/fheroes2/heroes/route.h | 3 +- src/fheroes2/heroes/route_pathfind.cpp | 313 ------------------ src/fheroes2/maps/maps.cpp | 141 ++++++-- src/fheroes2/maps/maps.h | 1 + src/fheroes2/maps/maps_tiles.cpp | 89 ++--- src/fheroes2/maps/maps_tiles.h | 10 +- src/fheroes2/system/settings.h | 5 +- src/fheroes2/{kingdom => world}/world.cpp | 14 +- src/fheroes2/{kingdom => world}/world.h | 9 +- .../{kingdom => world}/world_loadmap.cpp | 2 + src/fheroes2/world/world_pathfinding.cpp | 218 ++++++++++++ src/fheroes2/world/world_pathfinding.h | 54 +++ 22 files changed, 485 insertions(+), 442 deletions(-) delete mode 100644 src/fheroes2/heroes/route_pathfind.cpp rename src/fheroes2/{kingdom => world}/world.cpp (99%) rename src/fheroes2/{kingdom => world}/world.h (96%) rename src/fheroes2/{kingdom => world}/world_loadmap.cpp (99%) create mode 100644 src/fheroes2/world/world_pathfinding.cpp create mode 100644 src/fheroes2/world/world_pathfinding.h diff --git a/VisualStudio/common.props b/VisualStudio/common.props index 37f48106b5f..50f25d56f68 100644 --- a/VisualStudio/common.props +++ b/VisualStudio/common.props @@ -10,7 +10,7 @@ Level4 true - src\engine;src\fheroes2\gui;src\fheroes2\maps;src\fheroes2\kingdom;src\fheroes2\game;src\fheroes2\dialog;src\fheroes2\system;src\fheroes2\spell;src\fheroes2\monster;src\fheroes2\castle;src\fheroes2\agg;src\fheroes2\heroes;src\fheroes2\resource;src\fheroes2\ai;src\fheroes2\army;src\fheroes2\battle;src\fheroes2\pocketpc;src\fheroes2\objects;src\fheroes2\test;src\fheroes2\image;src\thirdparty\libsmacker;..\zlib\include\;%(AdditionalIncludeDirectories) + src\engine;src\fheroes2\gui;src\fheroes2\maps;src\fheroes2\kingdom;src\fheroes2\game;src\fheroes2\dialog;src\fheroes2\system;src\fheroes2\spell;src\fheroes2\monster;src\fheroes2\castle;src\fheroes2\agg;src\fheroes2\heroes;src\fheroes2\resource;src\fheroes2\ai;src\fheroes2\army;src\fheroes2\battle;src\fheroes2\pocketpc;src\fheroes2\objects;src\fheroes2\world;src\fheroes2\test;src\fheroes2\image;src\thirdparty\libsmacker;..\zlib\include\;%(AdditionalIncludeDirectories) _CRT_SECURE_NO_WARNINGS;_SCL_SECURE_NO_WARNINGS;WITH_ZLIB;WITH_MIXER;%(PreprocessorDefinitions) diff --git a/fheroes2.vcxproj b/fheroes2.vcxproj index 4e1e72e5170..bb63145aa6c 100644 --- a/fheroes2.vcxproj +++ b/fheroes2.vcxproj @@ -319,7 +319,6 @@ - @@ -332,8 +331,6 @@ - - @@ -371,6 +368,9 @@ + + + @@ -486,7 +486,6 @@ - @@ -524,6 +523,8 @@ + + diff --git a/src/fheroes2/ai/simple/ai_simple_heroes.cpp b/src/fheroes2/ai/simple/ai_simple_heroes.cpp index 6c794939289..80314820e2e 100644 --- a/src/fheroes2/ai/simple/ai_simple_heroes.cpp +++ b/src/fheroes2/ai/simple/ai_simple_heroes.cpp @@ -282,7 +282,7 @@ namespace AI if ( task.size() >= HERO_MAX_SHEDULED_TASK ) break; const int positionIndex = ( *it ).first; - const uint32_t distance = hero.GetPath().Calculate( positionIndex, PATHFINDING_LIMIT ); + const uint32_t distance = world.getDistance( hero.GetIndex(), positionIndex, hero.GetLevelSkill( Skill::Secondary::PATHFINDING ) ); if ( distance ) { DEBUG( DBG_AI, DBG_INFO, @@ -482,7 +482,7 @@ namespace AI if ( HeroesValidObject( hero, index ) ) { DEBUG( DBG_AI, DBG_TRACE, hero.GetName() << ", looking for: " << MP2::StringObject( world.GetTiles( index ).GetObject() ) << "(" << index << ")" ); - if ( hero.GetPath().Calculate( index, PATHFINDING_LIMIT ) ) + if ( hero.GetPath().Calculate( index ) ) break; DEBUG( DBG_AI, DBG_TRACE, hero.GetName() << " say: unable to get object: " << index << ", remove task..." ); diff --git a/src/fheroes2/dialog/dialog_quickinfo.cpp b/src/fheroes2/dialog/dialog_quickinfo.cpp index 34e14a5776d..39ff84611d3 100644 --- a/src/fheroes2/dialog/dialog_quickinfo.cpp +++ b/src/fheroes2/dialog/dialog_quickinfo.cpp @@ -285,9 +285,9 @@ std::string ShowGroundInfo( const Maps::Tiles & tile, bool show, const Heroes * std::string str = Maps::Ground::String( tile.GetGround() ); if ( show && hero ) { - int dir = Direction::Get( hero->GetIndex(), tile.GetIndex() ); + int dir = Maps::GetDirection( hero->GetIndex(), tile.GetIndex() ); if ( dir != Direction::UNKNOWN ) { - uint32_t cost = ( tile.isRoad( dir ) ) ? Maps::Ground::roadPenalty : Maps::Ground::GetPenalty( tile, hero->GetLevelSkill( Skill::Secondary::PATHFINDING ) ); + uint32_t cost = tile.isRoad() ? Maps::Ground::roadPenalty : Maps::Ground::GetPenalty( tile, hero->GetLevelSkill( Skill::Secondary::PATHFINDING ) ); if ( cost ) { str.append( "\n" ); diff --git a/src/fheroes2/game/game_io.cpp b/src/fheroes2/game/game_io.cpp index a84a06179eb..5ae1d4cb285 100644 --- a/src/fheroes2/game/game_io.cpp +++ b/src/fheroes2/game/game_io.cpp @@ -196,8 +196,6 @@ bool Game::Load( const std::string & fn ) fz >> World::Get() >> Settings::Get() >> GameOver::Result::Get() >> GameStatic::Data::Get() >> MonsterStaticData::Get() >> end_check; - World::Get().PostFixLoad(); - if ( fz.fail() || ( end_check != SAV2ID2 && end_check != SAV2ID3 ) ) { DEBUG( DBG_GAME, DBG_WARN, "invalid load file: " << fn ); return false; diff --git a/src/fheroes2/gui/interface_gamearea.cpp b/src/fheroes2/gui/interface_gamearea.cpp index 4be026c9242..4dfff7ac07d 100644 --- a/src/fheroes2/gui/interface_gamearea.cpp +++ b/src/fheroes2/gui/interface_gamearea.cpp @@ -259,9 +259,8 @@ void Interface::GameArea::Redraw( fheroes2::Image & dst, int flag ) const if ( pathEnd != nextStep ) { const Maps::Tiles & tileTo = world.GetTiles( currentStep->GetIndex() ); uint32_t cost = Maps::Ground::GetPenalty( tileTo, pathfinding ); - const int direction = currentStep->GetDirection(); - if ( world.GetTiles( currentStep->GetFrom() ).isRoad( direction ) || tileTo.isRoad( Direction::Reflect( direction ) ) ) + if ( world.GetTiles( currentStep->GetFrom() ).isRoad() && tileTo.isRoad() ) cost = Maps::Ground::roadPenalty; index = Route::Path::GetIndexSprite( ( *currentStep ).GetDirection(), ( *nextStep ).GetDirection(), cost ); diff --git a/src/fheroes2/heroes/direction.cpp b/src/fheroes2/heroes/direction.cpp index 6b618c828b7..69ceef43116 100644 --- a/src/fheroes2/heroes/direction.cpp +++ b/src/fheroes2/heroes/direction.cpp @@ -54,17 +54,6 @@ std::string Direction::String( int direct ) return res.empty() ? str_direct[0] : res; } -int Direction::Get( s32 from, s32 to ) -{ - const Directions directions = Direction::All(); - - for ( Directions::const_iterator it = directions.begin(); it != directions.end(); ++it ) - if ( to == Maps::GetDirectionIndex( from, *it ) ) - return *it; - - return to == from ? CENTER : UNKNOWN; -} - bool Direction::ShortDistanceClockWise( int from, int to ) { switch ( from ) { diff --git a/src/fheroes2/heroes/direction.h b/src/fheroes2/heroes/direction.h index 41d9fcd3439..ec86bfa7391 100644 --- a/src/fheroes2/heroes/direction.h +++ b/src/fheroes2/heroes/direction.h @@ -47,7 +47,6 @@ namespace Direction std::string String( int ); - int Get( s32 from, s32 to ); int Reflect( int direct ); bool ShortDistanceClockWise( int direct1, int direct2 ); diff --git a/src/fheroes2/heroes/heroes.cpp b/src/fheroes2/heroes/heroes.cpp index 069c15fbc77..ce1304a4eaa 100644 --- a/src/fheroes2/heroes/heroes.cpp +++ b/src/fheroes2/heroes/heroes.cpp @@ -1297,17 +1297,10 @@ int Heroes::GetDirection( void ) const int Heroes::GetRangeRouteDays( s32 dst ) const { const u32 maxMovePoints = GetMaxMovePoints(); - const u32 limit = maxMovePoints * 5 / 100; // limit ~5 day - // approximate distance, this restriction calculation - if ( ( 4 * maxMovePoints / 100 ) < Maps::GetApproximateDistance( GetIndex(), dst ) ) { - DEBUG( DBG_GAME, DBG_INFO, "distance limit" ); - return 0; - } - - Route::Path test( *this ); // approximate limit, this restriction path finding algorithm - uint32_t total = test.Calculate( dst, limit ); + uint32_t total = world.getDistance( GetIndex(), dst, GetLevelSkill( Skill::Secondary::PATHFINDING ) ); + DEBUG( DBG_GAME, DBG_TRACE, "path distance: " << total ); if ( total > 0 ) { if ( move_point >= total ) return 1; @@ -1323,7 +1316,7 @@ int Heroes::GetRangeRouteDays( s32 dst ) const return 4; } else { - DEBUG( DBG_GAME, DBG_INFO, "iteration limit: " << limit ); + DEBUG( DBG_GAME, DBG_TRACE, "unreachable point: " << dst ); } return 0; diff --git a/src/fheroes2/heroes/route.cpp b/src/fheroes2/heroes/route.cpp index d3cd876205a..a5d0b6b3c3b 100644 --- a/src/fheroes2/heroes/route.cpp +++ b/src/fheroes2/heroes/route.cpp @@ -82,7 +82,7 @@ Route::Path & Route::Path::operator=( const Path & p ) int Route::Path::GetFrontDirection( void ) const { - return empty() ? ( dst != hero->GetIndex() ? Direction::Get( hero->GetIndex(), dst ) : Direction::CENTER ) : front().GetDirection(); + return empty() ? ( dst != hero->GetIndex() ? Maps::GetDirection( hero->GetIndex(), dst ) : Direction::CENTER ) : front().GetDirection(); } u32 Route::Path::GetFrontPenalty( void ) const @@ -120,11 +120,16 @@ s32 Route::Path::GetDestinedIndex( void ) const } /* return length path */ -uint32_t Route::Path::Calculate( const s32 & dst_index, int limit /* -1 */ ) +uint32_t Route::Path::Calculate( const s32 & destIndex ) { - dst = dst_index; + const int fromIndex = hero->GetIndex(); + const uint32_t skill = hero->GetLevelSkill( Skill::Secondary::PATHFINDING ); - return Find( hero->GetIndex(), dst, hero->isShipMaster(), limit, hero->GetLevelSkill( Skill::Secondary::PATHFINDING ) ); + dst = destIndex; + + std::list::operator=( world.getPath( fromIndex, dst, skill, false ) ); + + return world.getDistance( fromIndex, dst, skill ); } void Route::Path::Reset( void ) @@ -139,18 +144,18 @@ void Route::Path::Reset( void ) bool Route::Path::isComplete( void ) const { - return dst == hero->GetIndex() || ( empty() && Direction::UNKNOWN != Direction::Get( hero->GetIndex(), dst ) ); + return dst == hero->GetIndex() || ( empty() && Direction::UNKNOWN != Maps::GetDirection( hero->GetIndex(), dst ) ); } bool Route::Path::isValid( void ) const { - return !empty(); + return !empty() && front().GetDirection() != Direction::UNKNOWN; } int Route::Path::GetIndexSprite( int from, int to, int mod ) { // ICN::ROUTE - // start index 1, 25, 49, 73, 97, 121 (size arrow path) + // start index 1, 25, 49, 73, 97, 121 (path arrow size) int index = 1; switch ( mod ) { diff --git a/src/fheroes2/heroes/route.h b/src/fheroes2/heroes/route.h index 1434a6c4e2e..0e26a9e8fc1 100644 --- a/src/fheroes2/heroes/route.h +++ b/src/fheroes2/heroes/route.h @@ -75,7 +75,7 @@ namespace Route int GetFrontDirection( void ) const; u32 GetFrontPenalty( void ) const; u32 GetTotalPenalty( void ) const; - uint32_t Calculate( const s32 &, int limit = -1 ); + uint32_t Calculate( const s32 & destIndex ); void Show( void ) { @@ -105,7 +105,6 @@ namespace Route static int GetIndexSprite( int from, int to, int mod ); private: - uint32_t Find( int32_t from, int32_t to, bool fromWater = false, int limit = -1, int pathfinding = Skill::Level::NONE ); friend StreamBase & operator<<( StreamBase &, const Path & ); friend StreamBase & operator>>( StreamBase &, Path & ); diff --git a/src/fheroes2/heroes/route_pathfind.cpp b/src/fheroes2/heroes/route_pathfind.cpp deleted file mode 100644 index 3c3308f16a0..00000000000 --- a/src/fheroes2/heroes/route_pathfind.cpp +++ /dev/null @@ -1,313 +0,0 @@ -/*************************************************************************** - * Copyright (C) 2009 by Andrey Afletdinov * - * * - * Part of the Free Heroes2 Engine: * - * http://sourceforge.net/projects/fheroes2 * - * * - * 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 - -#include "ai.h" -#include "direction.h" -#include "ground.h" -#include "heroes.h" -#include "maps.h" -#include "route.h" -#include "settings.h" -#include "world.h" - -struct cell_t -{ - cell_t() - : cost_g( MAXU16 ) - , cost_t( MAXU16 ) - , cost_d( MAXU16 ) - , passbl( 0 ) - , open( 1 ) - , direct( Direction::CENTER ) - , parent( -1 ) - {} - - u16 cost_g; // ground - u16 cost_t; // total - u16 cost_d; // distance - u16 passbl; - u16 open; // bool - u16 direct; - s32 parent; -}; - -int GetCurrentLength( std::map & list, s32 from ) -{ - int res = 0; - while ( 0 <= list[from].parent ) { - from = list[from].parent; - ++res; - } - return res; -} - -bool CheckMonsterProtectionAndNotDst( const s32 & to, const s32 & dst ) -{ - const MapsIndexes & monsters = Maps::GetTilesUnderProtection( to ); - return monsters.size() && monsters.end() == std::find( monsters.begin(), monsters.end(), dst ); -} - -bool PassableToTile( const Maps::Tiles & toTile, int direct, s32 dst, bool fromWater ) -{ - const int object = toTile.GetObject(); - - // check end point - if ( toTile.GetIndex() == dst ) { - // fix toTilePassable with action object - if ( MP2::isPickupObject( object ) ) - return true; - - // check direct to object - if ( MP2::isActionObject( toTile.GetObject( false ), fromWater ) ) - return direct & toTile.GetPassable(); - - if ( MP2::OBJ_HEROES == object ) - return toTile.isPassable( direct, fromWater, false ); - } - - // check to tile direct - if ( !toTile.isPassable( direct, fromWater, false ) ) - return false; - - if ( toTile.GetIndex() != dst ) { - if ( MP2::isPickupObject( object ) || MP2::isActionObject( object, fromWater ) ) - return false; - - // check hero/monster on route - switch ( object ) { - case MP2::OBJ_HEROES: - case MP2::OBJ_MONSTER: - return false; - - default: - break; - } - - // check monster protection - if ( CheckMonsterProtectionAndNotDst( toTile.GetIndex(), dst ) ) - return false; - } - - return true; -} - -bool PassableFromToTile( s32 from, s32 to, int direct, s32 dst, bool fromWater ) -{ - const Maps::Tiles & fromTile = world.GetTiles( from ); - const Maps::Tiles & toTile = world.GetTiles( to ); - const int directionReflect = Direction::Reflect( direct ); - - if ( !fromTile.isPassable( direct, fromWater, false ) ) - return false; - - if ( fromTile.isWater() && !toTile.isWater() ) { - switch ( toTile.GetObject() ) { - case MP2::OBJ_BOAT: - case MP2::OBJ_MONSTER: - case MP2::OBJ_HEROES: - return false; - - case MP2::OBJ_COAST: - return to == dst; - - default: - break; - } - } - else if ( !fromTile.isWater() && toTile.isWater() ) { - switch ( toTile.GetObject() ) { - case MP2::OBJ_BOAT: - return to == dst; - - case MP2::OBJ_HEROES: - return to == dst; - - default: - break; - } - } - - // check corner water/coast - if ( fromWater ) { - switch ( direct ) { - case Direction::TOP_LEFT: - if ( !world.GetTiles( Maps::GetDirectionIndex( from, Direction::TOP ) ).isWater() - || !world.GetTiles( Maps::GetDirectionIndex( from, Direction::LEFT ) ).isWater() ) - return false; - break; - - case Direction::TOP_RIGHT: - if ( !world.GetTiles( Maps::GetDirectionIndex( from, Direction::TOP ) ).isWater() - || !world.GetTiles( Maps::GetDirectionIndex( from, Direction::RIGHT ) ).isWater() ) - return false; - break; - - case Direction::BOTTOM_RIGHT: - if ( !world.GetTiles( Maps::GetDirectionIndex( from, Direction::BOTTOM ) ).isWater() - || !world.GetTiles( Maps::GetDirectionIndex( from, Direction::RIGHT ) ).isWater() ) - return false; - break; - - case Direction::BOTTOM_LEFT: - if ( !world.GetTiles( Maps::GetDirectionIndex( from, Direction::BOTTOM ) ).isWater() - || !world.GetTiles( Maps::GetDirectionIndex( from, Direction::LEFT ) ).isWater() ) - return false; - break; - - default: - break; - } - } - - return PassableToTile( toTile, directionReflect, dst, fromWater ); -} - -uint32_t GetPenaltyFromTo( int from, int to, int direction, uint32_t pathfinding ) -{ - const Maps::Tiles & tileTo = world.GetTiles( to ); - uint32_t penalty = ( world.GetTiles( from ).isRoad( direction ) || tileTo.isRoad( Direction::Reflect( direction ) ) ) - ? Maps::Ground::roadPenalty - : Maps::Ground::GetPenalty( tileTo, pathfinding ); - - // diagonal move costs 50% extra - if ( direction & ( Direction::TOP_RIGHT | Direction::BOTTOM_RIGHT | Direction::BOTTOM_LEFT | Direction::TOP_LEFT ) ) - penalty = penalty * 3 / 2; - - return penalty; -} - -uint32_t Route::Path::Find( int32_t from, int32_t to, bool fromWater /* false */, int limit /* -1 */, int pathfinding /* NONE */ ) -{ - uint32_t pathCost = 0; - - s32 cur = from; - s32 alt = 0; - s32 tmp = 0; - std::map list; - std::map::iterator it1 = list.begin(); - std::map::iterator it2 = list.end(); - - list[cur].cost_g = 0; - list[cur].cost_t = 0; - list[cur].parent = -1; - list[cur].open = 0; - - const Directions directions = Direction::All(); - clear(); - - while ( cur != to ) { - LocalEvent::Get().HandleEvents( false ); - for ( Directions::const_iterator it = directions.begin(); it != directions.end(); ++it ) { - if ( Maps::isValidDirection( cur, *it ) ) { - tmp = Maps::GetDirectionIndex( cur, *it ); - - if ( list[tmp].open ) { - const u32 costg = GetPenaltyFromTo( cur, tmp, *it, pathfinding ); - - // new - if ( -1 == list[tmp].parent ) { - if ( ( list[cur].passbl & *it ) || PassableFromToTile( cur, tmp, *it, to, fromWater ) ) { - list[cur].passbl |= *it; - - list[tmp].direct = *it; - list[tmp].cost_g = costg; - list[tmp].parent = cur; - list[tmp].open = 1; - list[tmp].cost_d = 75 * Maps::GetApproximateDistance( tmp, to ); - list[tmp].cost_t = list[cur].cost_t + costg; - } - } - // check alt - else { - if ( list[tmp].cost_t > list[cur].cost_t + costg && ( ( list[cur].passbl & *it ) || PassableFromToTile( cur, tmp, *it, to, fromWater ) ) ) { - list[cur].passbl |= *it; - - list[tmp].direct = *it; - list[tmp].parent = cur; - list[tmp].cost_g = costg; - list[tmp].cost_t = list[cur].cost_t + costg; - } - } - } - } - } - - list[cur].open = 0; - - it1 = list.begin(); - alt = -1; - tmp = MAXU16; - - DEBUG( DBG_OTHER, DBG_TRACE, "route, from: " << cur ); - - // find minimal cost - for ( ; it1 != it2; ++it1 ) - if ( ( *it1 ).second.open ) { - const cell_t & cell2 = ( *it1 ).second; -#ifdef WITH_DEBUG - if ( IS_DEBUG( DBG_OTHER, DBG_TRACE ) && cell2.cost_g != MAXU16 ) { - int direct = Direction::Get( cur, ( *it1 ).first ); - - if ( Direction::UNKNOWN != direct ) { - VERBOSE( "\t\tdirect: " << Direction::String( direct ) << ", index: " << ( *it1 ).first << ", cost g: " << cell2.cost_g - << ", cost t: " << cell2.cost_t << ", cost d: " << cell2.cost_d ); - } - } -#endif - - if ( cell2.cost_t + cell2.cost_d < tmp ) { - tmp = cell2.cost_t + cell2.cost_d; - alt = ( *it1 ).first; - } - } - - // not found, and exception - if ( MAXU16 == tmp || -1 == alt ) - break; -#ifdef WITH_DEBUG - else - DEBUG( DBG_OTHER, DBG_TRACE, "select: " << alt ); -#endif - cur = alt; - - if ( 0 < limit && GetCurrentLength( list, cur ) > limit ) - break; - } - - // save path - if ( cur == to ) { - while ( cur != from ) { - push_front( Route::Step( list[cur].parent, list[cur].direct, list[cur].cost_g ) ); - pathCost += list[cur].cost_g; - cur = list[cur].parent; - } - } - else { - DEBUG( DBG_OTHER, DBG_TRACE, - "not found" - << ", from:" << from << ", to: " << to ); - } - - return pathCost; -} diff --git a/src/fheroes2/maps/maps.cpp b/src/fheroes2/maps/maps.cpp index 8925c5f48e3..ef152dbbfd5 100644 --- a/src/fheroes2/maps/maps.cpp +++ b/src/fheroes2/maps/maps.cpp @@ -138,6 +138,42 @@ const char * Maps::GetMinesName( int type ) return _( "Mine" ); } +int Maps::GetDirection( int from, int to ) +{ + if ( from == to ) + return Direction::CENTER; + + const int diff = to - from; + const int width = world.w(); + + if ( diff == ( -width - 1 ) ) { + return Direction::TOP_LEFT; + } + else if ( diff == -width ) { + return Direction::TOP; + } + else if ( diff == ( -width + 1 ) ) { + return Direction::TOP_RIGHT; + } + else if ( diff == -1 ) { + return Direction::LEFT; + } + else if ( diff == 1 ) { + return Direction::RIGHT; + } + else if ( diff == width - 1 ) { + return Direction::BOTTOM_LEFT; + } + else if ( diff == width ) { + return Direction::BOTTOM; + } + else if ( diff == width + 1 ) { + return Direction::BOTTOM_RIGHT; + } + + return Direction::UNKNOWN; +} + s32 Maps::GetDirectionIndex( s32 from, int vector ) { switch ( vector ) { @@ -167,27 +203,29 @@ s32 Maps::GetDirectionIndex( s32 from, int vector ) // check bound bool Maps::isValidDirection( s32 from, int vector ) { + const int32_t width = world.w(); + switch ( vector ) { case Direction::TOP: - return ( from >= world.w() ); + return ( from >= width ); case Direction::RIGHT: - return ( ( from % world.w() ) < ( world.w() - 1 ) ); + return ( ( from % width ) < ( width - 1 ) ); case Direction::BOTTOM: - return ( from < world.w() * ( world.h() - 1 ) ); + return ( from < width * ( world.h() - 1 ) ); case Direction::LEFT: - return ( from % world.w() ); + return ( from % width ); case Direction::TOP_RIGHT: - return isValidDirection( from, Direction::TOP ) && isValidDirection( from, Direction::RIGHT ); + return ( from >= width ) && ( ( from % width ) < ( width - 1 ) ); case Direction::BOTTOM_RIGHT: - return isValidDirection( from, Direction::BOTTOM ) && isValidDirection( from, Direction::RIGHT ); + return ( from < width * ( world.h() - 1 ) ) && ( ( from % width ) < ( width - 1 ) ); case Direction::BOTTOM_LEFT: - return isValidDirection( from, Direction::BOTTOM ) && isValidDirection( from, Direction::LEFT ); + return ( from < width * ( world.h() - 1 ) ) && ( from % width ); case Direction::TOP_LEFT: - return isValidDirection( from, Direction::TOP ) && isValidDirection( from, Direction::LEFT ); + return ( from >= width ) && ( from % width ); default: break; @@ -248,14 +286,37 @@ Maps::Indexes Maps::GetAllIndexes( void ) Maps::Indexes Maps::GetAroundIndexes( s32 center ) { Indexes result; + if ( !isValidAbsIndex( center ) ) + return result; + result.reserve( 8 ); + const int width = world.w(); + const int x = center % width; + const int y = center / width; + + if ( y > 1 ) { + if ( x > 1 ) + result.push_back( center - width - 1 ); - if ( isValidAbsIndex( center ) ) { - const Directions directions = Direction::All(); + result.push_back( center - width ); - for ( Directions::const_iterator it = directions.begin(); it != directions.end(); ++it ) - if ( isValidDirection( center, *it ) ) - result.push_back( GetDirectionIndex( center, *it ) ); + if ( x < width - 1 ) + result.push_back( center - width + 1 ); + } + + if ( x > 1 ) + result.push_back( center - 1 ); + if ( x < width - 1 ) + result.push_back( center + 1 ); + + if ( y < world.h() - 1 ) { + if ( x > 1 ) + result.push_back( center + width - 1 ); + + result.push_back( center + width ); + + if ( x < width - 1 ) + result.push_back( center + width + 1 ); } return result; @@ -404,17 +465,18 @@ bool MapsTileIsUnderProtection( s32 from, s32 index ) /* from: center, index: mo const Maps::Tiles & tile1 = world.GetTiles( from ); const Maps::Tiles & tile2 = world.GetTiles( index ); - if ( tile1.isWater() == tile2.isWater() ) { + if ( tile2.GetObject() == MP2::OBJ_MONSTER && tile1.isWater() == tile2.isWater() ) { + const int monsterDirection = Maps::GetDirection( index, from ); /* if monster can attack to */ - result = ( tile2.GetPassable() & Direction::Get( index, from ) ) && ( tile1.GetPassable() & Direction::Get( from, index ) ); + result = ( tile2.GetPassable() & monsterDirection ) && ( tile1.GetPassable() & Maps::GetDirection( from, index ) ); if ( !result ) { /* h2 specific monster attack: BOTTOM_LEFT impassable! */ - if ( Direction::BOTTOM_LEFT == Direction::Get( index, from ) && ( Direction::LEFT & tile2.GetPassable() ) && ( Direction::TOP & tile1.GetPassable() ) ) + if ( Direction::BOTTOM_LEFT == monsterDirection && ( Direction::LEFT & tile2.GetPassable() ) && ( Direction::TOP & tile1.GetPassable() ) ) result = true; else /* h2 specific monster attack: BOTTOM_RIGHT impassable! */ - if ( Direction::BOTTOM_RIGHT == Direction::Get( index, from ) && ( Direction::RIGHT & tile2.GetPassable() ) && ( Direction::TOP & tile1.GetPassable() ) ) + if ( Direction::BOTTOM_RIGHT == monsterDirection && ( Direction::RIGHT & tile2.GetPassable() ) && ( Direction::TOP & tile1.GetPassable() ) ) result = true; } } @@ -424,7 +486,7 @@ bool MapsTileIsUnderProtection( s32 from, s32 index ) /* from: center, index: mo bool Maps::IsNearTiles( s32 index1, s32 index2 ) { - return DIRECTION_ALL & Direction::Get( index1, index2 ); + return DIRECTION_ALL & Maps::GetDirection( index1, index2 ); } bool Maps::TileIsUnderProtection( s32 center ) @@ -434,15 +496,48 @@ bool Maps::TileIsUnderProtection( s32 center ) Maps::Indexes Maps::GetTilesUnderProtection( s32 center ) { - Indexes indexes = Maps::ScanAroundObject( center, MP2::OBJ_MONSTER ); + Indexes result; + if ( !isValidAbsIndex( center ) ) + return result; + + result.reserve( 9 ); + const int width = world.w(); + const int x = center % width; + const int y = center / width; + + auto validateAndInsert = [&result, ¢er]( const int index ) { + if ( MapsTileIsUnderProtection( center, index ) ) + result.push_back( index ); + }; + + if ( y > 1 ) { + if ( x > 1 ) + validateAndInsert( center - width - 1 ); - indexes.resize( std::distance( indexes.begin(), - std::remove_if( indexes.begin(), indexes.end(), std::not1( std::bind1st( std::ptr_fun( &MapsTileIsUnderProtection ), center ) ) ) ) ); + validateAndInsert( center - width ); + if ( x < width - 1 ) + validateAndInsert( center - width + 1 ); + } + + if ( x > 1 ) + validateAndInsert( center - 1 ); if ( MP2::OBJ_MONSTER == world.GetTiles( center ).GetObject() ) - indexes.push_back( center ); + result.push_back( center ); + if ( x < width - 1 ) + validateAndInsert( center + 1 ); - return indexes; + if ( y < world.h() - 1 ) { + if ( x > 1 ) + validateAndInsert( center + width - 1 ); + + validateAndInsert( center + width ); + + if ( x < width - 1 ) + validateAndInsert( center + width + 1 ); + } + + return result; } u32 Maps::GetApproximateDistance( s32 index1, s32 index2 ) diff --git a/src/fheroes2/maps/maps.h b/src/fheroes2/maps/maps.h index b4276a99551..c8d43dc0a23 100644 --- a/src/fheroes2/maps/maps.h +++ b/src/fheroes2/maps/maps.h @@ -58,6 +58,7 @@ namespace Maps const char * SizeString( int size ); const char * GetMinesName( int res ); + int GetDirection( int from, int to ); s32 GetDirectionIndex( s32, int direct ); bool isValidDirection( s32, int direct ); diff --git a/src/fheroes2/maps/maps_tiles.cpp b/src/fheroes2/maps/maps_tiles.cpp index f84612ac1fc..9333618a5de 100644 --- a/src/fheroes2/maps/maps_tiles.cpp +++ b/src/fheroes2/maps/maps_tiles.cpp @@ -397,59 +397,27 @@ int Maps::TilesAddon::GetActionObject( const Maps::TilesAddon & ta ) return MP2::OBJ_ZERO; } -bool Maps::TilesAddon::isRoad( int direct ) const +bool Maps::TilesAddon::isRoad() const { switch ( MP2::GetICNObject( object ) ) { // from sprite road case ICN::ROAD: - if ( 0 == index || 26 == index || 31 == index ) - return direct & ( Direction::TOP | Direction::BOTTOM ); - else if ( 2 == index || 21 == index || 28 == index ) - return direct & ( Direction::LEFT | Direction::RIGHT ); - else if ( 17 == index || 29 == index ) - return direct & ( Direction::TOP_LEFT | Direction::BOTTOM_RIGHT ); - else if ( 18 == index || 30 == index ) - return direct & ( Direction::TOP_RIGHT | Direction::BOTTOM_LEFT ); - else if ( 3 == index ) - return direct & ( Direction::TOP | Direction::BOTTOM | Direction::LEFT | Direction::RIGHT ); - else if ( 4 == index ) - return direct & ( Direction::TOP | Direction::BOTTOM | Direction::TOP_LEFT | Direction::TOP_RIGHT ); - else if ( 5 == index ) - return direct & ( Direction::TOP | Direction::BOTTOM | Direction::TOP_RIGHT ); - else if ( 6 == index ) - return direct & ( Direction::TOP | Direction::BOTTOM | Direction::RIGHT ); - else if ( 7 == index ) - return direct & ( Direction::TOP | Direction::RIGHT ); - else if ( 9 == index ) - return direct & ( Direction::BOTTOM | Direction::TOP_RIGHT ); - else if ( 12 == index ) - return direct & ( Direction::BOTTOM | Direction::TOP_LEFT ); - else if ( 13 == index ) - return direct & ( Direction::TOP | Direction::BOTTOM | Direction::TOP_LEFT ); - else if ( 14 == index ) - return direct & ( Direction::TOP | Direction::BOTTOM | Direction::LEFT ); - else if ( 16 == index ) - return direct & ( Direction::TOP | Direction::LEFT ); - else if ( 19 == index ) - return direct & ( Direction::TOP_LEFT | Direction::BOTTOM_RIGHT ); - else if ( 20 == index ) - return direct & ( Direction::TOP_RIGHT | Direction::BOTTOM_LEFT ); - - break; + if ( 1 == index || 8 == index || 10 == index || 11 == index || 15 == index || 22 == index || 23 == index || 24 == index || 25 == index || 27 == index ) + return false; + else + return true; // castle and tower (gate) case ICN::OBJNTOWN: if ( 13 == index || 29 == index || 45 == index || 61 == index || 77 == index || 93 == index || 109 == index || 125 == index || 141 == index || 157 == index || 173 == index || 189 == index ) - return direct & ( Direction::TOP | Direction::BOTTOM ); - + return true; break; // castle lands (gate) case ICN::OBJNTWBA: if ( 7 == index || 17 == index || 27 == index || 37 == index || 47 == index || 57 == index || 67 == index || 77 == index ) - return direct & ( Direction::TOP | Direction::BOTTOM ); - + return true; break; default: @@ -459,7 +427,7 @@ bool Maps::TilesAddon::isRoad( int direct ) const return false; } -bool Maps::TilesAddon::isRoadObject() const +bool Maps::TilesAddon::hasRoadFlag() const { // This MP2 "object" is a bitfield // 6 bits is ICN tileset id, 1 bit isRoad flag, 1 bit hasAnimation flag @@ -537,7 +505,7 @@ bool Maps::TilesAddon::isStream( const TilesAddon & ta ) return ICN::STREAM == MP2::GetICNObject( ta.object ) || ( ICN::OBJNMUL2 == MP2::GetICNObject( ta.object ) && ( ta.index < 14 ) ); } -bool Maps::TilesAddon::isRoad( const TilesAddon & ta ) +bool Maps::TilesAddon::isRoadObject( const TilesAddon & ta ) { return ICN::ROAD == MP2::GetICNObject( ta.object ); } @@ -1237,7 +1205,7 @@ bool isForestsTrees( const Maps::TilesAddon & ta ) bool Exclude4LongObject( const Maps::TilesAddon & ta ) { - return Maps::TilesAddon::isStream( ta ) || Maps::TilesAddon::isRoad( ta ) || Maps::TilesAddon::isShadow( ta ); + return Maps::TilesAddon::isStream( ta ) || Maps::TilesAddon::isRoadObject( ta ) || Maps::TilesAddon::isShadow( ta ); } bool HaveLongObjectUniq( const Maps::Addons & level, u32 uid ) @@ -1407,6 +1375,11 @@ void Maps::Tiles::AddonsPushLevel1( const MP2::mp2tile_t & mt ) { if ( mt.objectName1 && mt.indexName1 < 0xFF ) AddonsPushLevel1( TilesAddon( 0, mt.uniqNumber1, mt.objectName1, mt.indexName1 ) ); + + // MP2 "objectName" is a bitfield + // 6 bits is ICN tileset id, 1 bit isRoad flag, 1 bit hasAnimation flag + if ( ( mt.objectName1 >> 1 ) & 1 ) + tileIsRoad = true; } void Maps::Tiles::AddonsPushLevel1( const MP2::mp2addon_t & ma ) @@ -1627,7 +1600,7 @@ void Maps::Tiles::RedrawBoat( fheroes2::Image & dst ) const bool SkipRedrawTileBottom4Hero( const Maps::TilesAddon & ta, int passable ) { - if ( Maps::TilesAddon::isStream( ta ) || Maps::TilesAddon::isRoad( ta ) ) + if ( Maps::TilesAddon::isStream( ta ) || Maps::TilesAddon::isRoadObject( ta ) ) return true; else switch ( MP2::GetICNObject( ta.object ) ) { @@ -1895,9 +1868,9 @@ bool Maps::Tiles::validateWaterRules( bool fromWater ) const if ( fromWater ) return mp2_object == MP2::OBJ_COAST || ( tileIsWater && mp2_object != MP2::OBJ_BOAT ); - // if we're not in water but tile is; allow movement in two cases + // if we're not in water but tile is; allow movement in three cases if ( tileIsWater ) - return mp2_object == MP2::OBJ_SHIPWRECK || mp2_object == MP2::OBJ_HEROES; + return mp2_object == MP2::OBJ_SHIPWRECK || mp2_object == MP2::OBJ_HEROES || mp2_object == MP2::OBJ_BOAT; return true; } @@ -1929,9 +1902,9 @@ void Maps::Tiles::SetObjectPassable( bool pass ) } /* check road */ -bool Maps::Tiles::isRoad( int direct ) const +bool Maps::Tiles::isRoad() const { - return addons_level1.end() != std::find_if( addons_level1.begin(), addons_level1.end(), std::bind2nd( std::mem_fun_ref( &TilesAddon::isRoad ), direct ) ); + return tileIsRoad; } bool Maps::Tiles::isStream( void ) const @@ -2481,9 +2454,7 @@ std::pair Maps::Tiles::GetMonsterSpriteIndices( const Tiles & tile, ui continue; } - Route::Path path( *hero ); - path.Calculate( tileIndex, 2 ); // we need to make sure that a hero needs exactly 1 step to the creature - if ( path.size() == 1 && tile.isWater() == heroTile.isWater() ) { + if ( tile.isWater() == heroTile.isWater() ) { attackerIndex = *it; break; } @@ -2495,7 +2466,7 @@ std::pair Maps::Tiles::GetMonsterSpriteIndices( const Tiles & tile, ui if ( attackerIndex != -1 && !Settings::Get().ExtWorldOnlyFirstMonsterAttack() ) { spriteIndices.first += 7; - switch ( Direction::Get( tileIndex, attackerIndex ) ) { + switch ( Maps::GetDirection( tileIndex, attackerIndex ) ) { case Direction::TOP_LEFT: case Direction::LEFT: case Direction::BOTTOM_LEFT: @@ -2842,6 +2813,18 @@ StreamBase & Maps::operator<<( StreamBase & msg, const Tiles & tile ) StreamBase & Maps::operator>>( StreamBase & msg, Tiles & tile ) { - return msg >> tile.maps_index >> tile.pack_sprite_index >> tile.tile_passable >> tile.mp2_object >> tile.fog_colors >> tile.quantity1 >> tile.quantity2 - >> tile.quantity3 >> tile.addons_level1 >> tile.addons_level2; + msg >> tile.maps_index >> tile.pack_sprite_index >> tile.tile_passable >> tile.mp2_object >> tile.fog_colors >> tile.quantity1 >> tile.quantity2 >> tile.quantity3 + >> tile.addons_level1 >> tile.addons_level2; + if ( FORMAT_VERSION_082_RELEASE > Game::GetLoadVersion() ) { + for ( const Maps::TilesAddon & addon : tile.addons_level1 ) { + if ( addon.isRoad() ) { + tile.tileIsRoad = true; + break; + } + } + } + else { + msg >> tile.tileIsRoad; + } + return msg; } diff --git a/src/fheroes2/maps/maps_tiles.h b/src/fheroes2/maps/maps_tiles.h index d84d4e4d893..56e95815f12 100644 --- a/src/fheroes2/maps/maps_tiles.h +++ b/src/fheroes2/maps/maps_tiles.h @@ -60,15 +60,15 @@ namespace Maps TilesAddon & operator=( const TilesAddon & ta ); bool isUniq( u32 ) const; - bool isRoad( int ) const; - bool isRoadObject() const; + bool isRoad() const; + bool hasRoadFlag() const; bool isICN( int ) const; std::string String( int level ) const; static bool hasColorCycling( const TilesAddon & addon ); static bool isStream( const TilesAddon & ); - static bool isRoad( const TilesAddon & ); + static bool isRoadObject( const TilesAddon & ); static bool isResource( const TilesAddon & ); static bool isWaterResource( const TilesAddon & ); @@ -169,7 +169,7 @@ namespace Maps bool validateWaterRules( bool fromWater ) const; bool isPassable( int direct, bool fromWater, bool skipfog ) const; - bool isRoad( int = DIRECTION_ALL ) const; + bool isRoad() const; bool isObject( int obj ) const { return obj == mp2_object; @@ -309,6 +309,8 @@ namespace Maps u8 quantity2; u8 quantity3; + bool tileIsRoad = false; + #ifdef WITH_DEBUG u8 passable_disable; #endif diff --git a/src/fheroes2/system/settings.h b/src/fheroes2/system/settings.h index f7e5dab4d35..c8276b02687 100644 --- a/src/fheroes2/system/settings.h +++ b/src/fheroes2/system/settings.h @@ -34,12 +34,15 @@ #include "system.h" #define FORMAT_VERSION_090_RELEASE 9000 +#define FORMAT_VERSION_082_RELEASE 8200 #define FORMAT_VERSION_080_RELEASE 8000 #define FORMAT_VERSION_070_RELEASE 3269 #define FORMAT_VERSION_3255 3255 -#define CURRENT_FORMAT_VERSION FORMAT_VERSION_080_RELEASE // TODO: update this value for a new release #define LAST_FORMAT_VERSION FORMAT_VERSION_3255 +// Value is set to 8100+ to distinguish save format changes in master branch after 0.8.1 release +#define CURRENT_FORMAT_VERSION 8111 // TODO: update this value for a new release + enum { DBG_WARN = 0x0001, diff --git a/src/fheroes2/kingdom/world.cpp b/src/fheroes2/world/world.cpp similarity index 99% rename from src/fheroes2/kingdom/world.cpp rename to src/fheroes2/world/world.cpp index 83cbda1d7f9..add81452c41 100644 --- a/src/fheroes2/kingdom/world.cpp +++ b/src/fheroes2/world/world.cpp @@ -1037,6 +1037,16 @@ u32 World::GetUniq( void ) return ++GameStatic::uniq; } +uint32_t World::getDistance( int from, int to, uint32_t skill ) +{ + return _pathfinder.getDistance( from, to, skill ); +} + +std::list World::getPath( int from, int to, uint32_t skill, bool ignoreObjects ) +{ + return _pathfinder.buildPath( from, to, skill ); +} + StreamBase & operator<<( StreamBase & msg, const CapturedObject & obj ) { return msg << obj.objcol << obj.guardians << obj.split; @@ -1179,11 +1189,11 @@ StreamBase & operator>>( StreamBase & msg, World & w ) // heroes postfix std::for_each( w.vec_heroes.begin(), w.vec_heroes.end(), []( Heroes * hero ) { hero->RescanPathPassable(); } ); + world._pathfinder.reset(); + return msg; } -void World::PostFixLoad( void ) {} - void EventDate::LoadFromMP2( StreamBuf st ) { // id diff --git a/src/fheroes2/kingdom/world.h b/src/fheroes2/world/world.h similarity index 96% rename from src/fheroes2/kingdom/world.h rename to src/fheroes2/world/world.h index 70b1e1775b7..6d4a66168ba 100644 --- a/src/fheroes2/kingdom/world.h +++ b/src/fheroes2/world/world.h @@ -33,6 +33,7 @@ #include "maps_objects.h" #include "maps_tiles.h" #include "week.h" +#include "world_pathfinding.h" #include class Heroes; @@ -250,9 +251,12 @@ class World : protected Size MapObjectSimple * GetMapObject( u32 uid ); void RemoveMapObject( const MapObjectSimple * ); - static u32 GetUniq( void ); + bool isTileBlocked( int toTile, bool fromWater ) const; + bool isValidPath( int index, int direction ) const; + uint32_t getDistance( int from, int to, uint32_t skill ); + std::list getPath( int from, int to, uint32_t skill, bool ignoreObjects = true ); - void PostFixLoad( void ); + static u32 GetUniq( void ); private: World() @@ -294,6 +298,7 @@ class World : protected Size MapActions map_actions; MapObjects map_objects; + Pathfinder _pathfinder; }; StreamBase & operator<<( StreamBase &, const CapturedObject & ); diff --git a/src/fheroes2/kingdom/world_loadmap.cpp b/src/fheroes2/world/world_loadmap.cpp similarity index 99% rename from src/fheroes2/kingdom/world_loadmap.cpp rename to src/fheroes2/world/world_loadmap.cpp index 8195d8aa5f8..ef287bb1069 100644 --- a/src/fheroes2/kingdom/world_loadmap.cpp +++ b/src/fheroes2/world/world_loadmap.cpp @@ -1529,6 +1529,8 @@ void World::PostLoad( void ) // update tile passable std::for_each( vec_tiles.begin(), vec_tiles.end(), std::mem_fun_ref( &Maps::Tiles::UpdatePassable ) ); + _pathfinder.reset(); + // play with hero vec_kingdoms.ApplyPlayWithStartingHero(); diff --git a/src/fheroes2/world/world_pathfinding.cpp b/src/fheroes2/world/world_pathfinding.cpp new file mode 100644 index 00000000000..2571a57e69a --- /dev/null +++ b/src/fheroes2/world/world_pathfinding.cpp @@ -0,0 +1,218 @@ +/*************************************************************************** + * Free Heroes of Might and Magic II: https://github.com/ihhub/fheroes2 * + * Copyright (C) 2020 * + * * + * 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 "world_pathfinding.h" +#include "ground.h" +#include "world.h" + +void Pathfinder::reset() +{ + _cache.clear(); + _pathStart = -1; + _pathfindingSkill = Skill::Level::NONE; +} + +std::list Pathfinder::buildPath( int from, int target, uint8_t skill ) +{ + std::list path; + + // check if we have to re-cache the map (new hero selected, etc) + reEvaluateIfNeeded( from, skill ); + + // trace the path from end point + PathfindingNode & firstNode = _cache[target]; + uint32_t cost = firstNode._cost; + + // add cost of the first node + if ( firstNode._from != -1 ) + cost += cost - _cache[firstNode._from]._cost; + + int currentNode = target; + while ( currentNode != from && currentNode != -1 ) { + PathfindingNode & node = _cache[currentNode]; + + path.emplace_front( node._from, Maps::GetDirection( node._from, currentNode ), cost - node._cost ); + currentNode = node._from; + cost = node._cost; + } + + return path; +} + +bool World::isTileBlocked( int tileIndex, bool fromWater ) const +{ + const Maps::Tiles & tile = world.GetTiles( tileIndex ); + const bool toWater = tile.isWater(); + const int object = tile.GetObject(); + + if ( object == MP2::OBJ_HEROES || object == MP2::OBJ_MONSTER || object == MP2::OBJ_BOAT ) + return true; + + if ( MP2::isPickupObject( object ) || MP2::isActionObject( object, fromWater ) ) + return true; + + if ( fromWater && !toWater && object == MP2::OBJ_COAST ) + return true; + + return false; +} + +bool World::isValidPath( int index, int direction ) const +{ + const Maps::Tiles & fromTile = GetTiles( index ); + const Maps::Tiles & toTile = GetTiles( Maps::GetDirectionIndex( index, direction ) ); + const bool fromWater = fromTile.isWater(); + + // check corner water/coast + if ( fromWater ) { + const int mapWidth = world.w(); + switch ( direction ) { + case Direction::TOP_LEFT: + if ( !GetTiles( index - mapWidth ).isWater() || !GetTiles( index - 1 ).isWater() ) + return false; + break; + + case Direction::TOP_RIGHT: + if ( !GetTiles( index - mapWidth ).isWater() || !GetTiles( index + 1 ).isWater() ) + return false; + break; + + case Direction::BOTTOM_RIGHT: + if ( !GetTiles( index + mapWidth ).isWater() || !GetTiles( index + 1 ).isWater() ) + return false; + break; + + case Direction::BOTTOM_LEFT: + if ( !GetTiles( index + mapWidth ).isWater() || !GetTiles( index - 1 ).isWater() ) + return false; + break; + + default: + break; + } + } + + if ( !fromTile.isPassable( direction, fromWater, false ) ) + return false; + + return toTile.isPassable( Direction::Reflect( direction ), fromWater, false ); +} + +bool Pathfinder::isBlockedByObject( int from, int target, bool fromWater ) +{ + int currentNode = target; + while ( currentNode != from && currentNode != -1 ) { + if ( world.isTileBlocked( currentNode, fromWater ) ) { + return true; + } + currentNode = _cache[currentNode]._from; + } + return false; +} + +uint32_t Pathfinder::getDistance( int from, int target, uint8_t skill ) +{ + reEvaluateIfNeeded( from, skill ); + return _cache[target]._cost; +} + +void Pathfinder::reEvaluateIfNeeded( int from, uint8_t skill ) +{ + if ( _pathStart != from || _pathfindingSkill != skill ) { + evaluateMap( from, skill ); + } +} + +uint32_t Pathfinder::getMovementPenalty( int from, int target, int direction, uint8_t skill ) +{ + const Maps::Tiles & tileTo = world.GetTiles( target ); + uint32_t penalty = ( world.GetTiles( from ).isRoad() && tileTo.isRoad() ) ? Maps::Ground::roadPenalty : Maps::Ground::GetPenalty( tileTo, skill ); + + // diagonal move costs 50% extra + if ( direction & ( Direction::TOP_RIGHT | Direction::BOTTOM_RIGHT | Direction::BOTTOM_LEFT | Direction::TOP_LEFT ) ) + penalty = penalty * 3 / 2; + + return penalty; +} + +// Destination is optional +void Pathfinder::evaluateMap( int start, uint8_t skill ) +{ + const bool fromWater = world.GetTiles( start ).isWater(); + const int width = world.w(); + const int height = world.h(); + + const Directions directions = Direction::All(); + std::vector offset( directions.size() ); + for ( size_t i = 0; i < directions.size(); ++i ) { + offset[i] = Maps::GetDirectionIndex( 0, directions[i] ); + } + + _pathStart = start; + _pathfindingSkill = skill; + + _cache.clear(); + _cache.resize( width * height ); + _cache[start] = PathfindingNode( -1, 0 ); + + std::vector nodesToExplore; + nodesToExplore.push_back( start ); + for ( size_t lastProcessedNode = 0; lastProcessedNode < nodesToExplore.size(); ++lastProcessedNode ) { + const int currentNodeIdx = nodesToExplore[lastProcessedNode]; + const MapsIndexes & monsters = Maps::GetTilesUnderProtection( currentNodeIdx ); + PathfindingNode & currentNode = _cache[currentNodeIdx]; + + // check if current tile is protected, can move only to adjacent monster + if ( !monsters.empty() ) { + for ( int monsterIndex : monsters ) { + const int direction = Maps::GetDirection( currentNodeIdx, monsterIndex ); + + if ( direction != Direction::UNKNOWN && direction != Direction::CENTER && world.isValidPath( currentNodeIdx, direction ) ) { + // add straight to cache, can't move further from the monster + const uint32_t moveCost = currentNode._cost + getMovementPenalty( currentNodeIdx, monsterIndex, direction, skill ); + PathfindingNode & monsterNode = _cache[monsterIndex]; + if ( monsterNode._from == -1 || monsterNode._cost > moveCost ) { + monsterNode._from = currentNodeIdx; + monsterNode._cost = moveCost; + } + } + } + } + else if ( currentNodeIdx == start || !world.isTileBlocked( currentNodeIdx, fromWater ) ) { + for ( size_t i = 0; i < directions.size(); ++i ) { + if ( Maps::isValidDirection( currentNodeIdx, directions[i] ) ) { + const int newIndex = currentNodeIdx + offset[i]; + if ( newIndex == start ) + continue; + + const uint32_t moveCost = currentNode._cost + getMovementPenalty( currentNodeIdx, newIndex, directions[i], skill ); + PathfindingNode & newNode = _cache[newIndex]; + if ( world.isValidPath( currentNodeIdx, directions[i] ) && ( newNode._from == -1 || newNode._cost > moveCost ) ) { + newNode._from = currentNodeIdx; + newNode._cost = moveCost; + + // duplicates are allowed if we find a cheaper way there + nodesToExplore.push_back( newIndex ); + } + } + } + } + } +} diff --git a/src/fheroes2/world/world_pathfinding.h b/src/fheroes2/world/world_pathfinding.h new file mode 100644 index 00000000000..1355877c384 --- /dev/null +++ b/src/fheroes2/world/world_pathfinding.h @@ -0,0 +1,54 @@ +/*************************************************************************** + * Free Heroes of Might and Magic II: https://github.com/ihhub/fheroes2 * + * Copyright (C) 2020 * + * * + * 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 "route.h" + +struct PathfindingNode +{ + int _from = -1; + uint32_t _cost = 0; + + PathfindingNode() {} + PathfindingNode( int node, uint32_t cost ) + : _from( node ) + , _cost( cost ) + {} +}; + +class Pathfinder +{ +public: + Pathfinder() {} + void reset(); + void evaluateMap( int start, uint8_t skill ); + std::list buildPath( int from, int target, uint8_t skill = Skill::Level::NONE ); + uint32_t getDistance( int from, int target, uint8_t skill = Skill::Level::NONE ); + uint32_t getMovementPenalty( int from, int target, int direction, uint8_t skill = Skill::Level::NONE ); + bool isBlockedByObject( int from, int target, bool fromWater = false ); + +private: + void reEvaluateIfNeeded( int from, uint8_t skill ); + + std::vector _cache; + int _pathStart = -1; + uint8_t _pathfindingSkill = 0; +};