From 0701373eb076e4fa116f63a2b59df46fad26a286 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Tue, 16 Feb 2021 22:34:02 +0800 Subject: [PATCH 01/84] Add logic to support video frame rescaling (#2803) close #2528 --- src/engine/smk_decoder.cpp | 29 +++++++++++++++++++++++++---- src/fheroes2/game/game_video.cpp | 2 +- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/engine/smk_decoder.cpp b/src/engine/smk_decoder.cpp index acf102a5cd0..795d23d866e 100644 --- a/src/engine/smk_decoder.cpp +++ b/src/engine/smk_decoder.cpp @@ -25,6 +25,7 @@ #include "smacker.h" #include "smk_decoder.h" +#include #include SMKVideoSequence::SMKVideoSequence( const std::string & filePath ) @@ -147,7 +148,9 @@ unsigned long SMKVideoSequence::width() const unsigned long SMKVideoSequence::height() const { - return _height; + // Some videos are written in compressed format where video's height is exactly a half of the original height of the screen. + // Detecting such videos and rescaling helps to deal with such situations. + return ( _width == 640 && _height == 240 ) ? 480 : _height; } double SMKVideoSequence::fps() const @@ -174,16 +177,34 @@ void SMKVideoSequence::getNextFrame( fheroes2::Image & image, std::vector( _width ) ) || ( image.height() != static_cast( _height ) ); + const bool resizedImage = ( image.width() != static_cast( width() ) ) || ( image.height() != static_cast( height() ) ); - image.resize( _width, _height ); + image.resize( width(), height() ); image._disableTransformLayer(); const uint8_t * data = smk_get_video( _videoFile ); const uint8_t * paletteData = smk_get_palette( _videoFile ); const size_t size = static_cast( _width ) * _height; - std::copy( data, data + size, image.image() ); + + // Some videos are written in compressed format where video's height is exactly a half of the original height of the screen. + // Detecting such videos and rescaling helps to deal with such situations. + if ( _height != height() ) { + assert( _height * 2 == height() ); + uint8_t * imageData = image.image(); + for ( unsigned long i = 0; i < _height; ++i ) { + std::copy( data, data + _width, imageData ); + imageData += _width; + std::copy( data, data + _width, imageData ); + imageData += _width; + + data += _width; + } + } + else { + std::copy( data, data + size, image.image() ); + } + if ( resizedImage ) { std::fill( image.transform(), image.transform() + size, 0 ); } diff --git a/src/fheroes2/game/game_video.cpp b/src/fheroes2/game/game_video.cpp index c928d9fdca7..8f357b151b1 100644 --- a/src/fheroes2/game/game_video.cpp +++ b/src/fheroes2/game/game_video.cpp @@ -88,7 +88,7 @@ namespace Video } } - fheroes2::ScreenPaletteRestorer screenRestorer; + const fheroes2::ScreenPaletteRestorer screenRestorer; fheroes2::Image frame; std::vector palette; From 8f572963e2bb5d3f5a55ad1808441f4e6d187dc4 Mon Sep 17 00:00:00 2001 From: dimag0g Date: Tue, 16 Feb 2021 17:10:28 +0100 Subject: [PATCH 02/84] Nintendo switch logging / path fixes (#2807) --- src/Makefile.switch | 12 ++++-------- src/engine/logging.cpp | 6 +++--- src/engine/logging.h | 11 ++++++++--- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/Makefile.switch b/src/Makefile.switch index 304ec83f840..71756a1303f 100644 --- a/src/Makefile.switch +++ b/src/Makefile.switch @@ -1,13 +1,9 @@ -PREFIX = $(DEVKITPRO)/devkitA64/bin/aarch64-none-elf- - -CC = $(PREFIX)gcc -CXX = $(PREFIX)g++ -AR = $(PREFIX)ar +include $(DEVKITPRO)/libnx/switch_rules ARCH = -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE -CFLAGS := $(CFLAGS) $(ARCH) -D__SWITCH__ -CXXFLAGS := $(CXXFLAGS) $(ARCH) -D__SWITCH__ +CFLAGS := $(CFLAGS) $(ARCH) -D__SWITCH__ -DCONFIGURE_FHEROES2_DATA=\"//switch/fheroes2\" +CXXFLAGS := $(CXXFLAGS) $(ARCH) -D__SWITCH__ -DCONFIGURE_FHEROES2_DATA=\"//switch/fheroes2\" CFLAGS_TP := $(CFLAGS_TP) $(ARCH) CXXFLAGS_TP := $(CXXFLAGS_TP) $(ARCH) -LIBS := $(LIBS) -lSDL2 -lfreetype -lbz2 -lvorbisidec -lmodplug -lmpg123 -lm -lopusfile -logg -lopus -ljpeg -lwebp -specs=$(DEVKITPRO)/libnx/switch.specs +LIBS := $(LIBS) -lSDL2 -lfreetype -lpng16 -lbz2 -lvorbisidec -lmodplug -lmpg123 -lm -lopusfile -logg -lopus -ljpeg -lwebp -specs=$(DEVKITPRO)/libnx/switch.specs diff --git a/src/engine/logging.cpp b/src/engine/logging.cpp index a52aca819e8..fad4fc1403f 100644 --- a/src/engine/logging.cpp +++ b/src/engine/logging.cpp @@ -24,14 +24,14 @@ namespace { int g_debug = DBG_ALL_WARN + DBG_ALL_INFO; +} +namespace Logging +{ #if defined( __SWITCH__ ) // Platforms which log to file std::ofstream logFile; #endif -} -namespace Logging -{ const char * GetDebugOptionName( const int name ) { if ( name & DBG_ENGINE ) diff --git a/src/engine/logging.h b/src/engine/logging.h index 46211394af7..1d041e49105 100644 --- a/src/engine/logging.h +++ b/src/engine/logging.h @@ -94,11 +94,16 @@ namespace std #elif defined( __SWITCH__ ) // Platforms which log to file #include -extern std::ofstream log_file; + +namespace Logging +{ + extern std::ofstream logFile; +} + #define COUT( x ) \ { \ - log_file << x << std::endl; \ - log_file.flush(); \ + Logging::logFile << x << std::endl; \ + Logging::logFile.flush(); \ } #else // Default: log to STDERR #define COUT( x ) \ From 24af7b0a22d2f0797c7a7d2b3102fc0e3c567a67 Mon Sep 17 00:00:00 2001 From: Andrii Kurdiumov Date: Tue, 16 Feb 2021 22:21:25 +0600 Subject: [PATCH 03/84] Removal of the resource.h dependency (#2790) allows to not bring whole world while compile difficulty.cpp which is required for Setting.cpp --- src/fheroes2/game/difficulty.cpp | 32 ------------------------------- src/fheroes2/game/difficulty.h | 1 - src/fheroes2/kingdom/kingdom.cpp | 33 +++++++++++++++++++++++++++++++- src/fheroes2/kingdom/kingdom.h | 1 + 4 files changed, 33 insertions(+), 34 deletions(-) diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index 3fdbc126ed4..53636574f1b 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -21,7 +21,6 @@ ***************************************************************************/ #include "difficulty.h" -#include "resource.h" #include "translations.h" const std::string & Difficulty::String( int difficulty ) @@ -47,37 +46,6 @@ const std::string & Difficulty::String( int difficulty ) return str_difficulty[5]; } -cost_t Difficulty::GetKingdomStartingResources( int difficulty, bool isAIKingdom ) -{ - static cost_t startingResourcesSet[] = {{10000, 30, 10, 30, 10, 10, 10}, - {7500, 20, 5, 20, 5, 5, 5}, - {5000, 10, 2, 10, 2, 2, 2}, - {2500, 5, 0, 5, 0, 0, 0}, - {0, 0, 0, 0, 0, 0, 0}, - // ai resource - {10000, 30, 10, 30, 10, 10, 10}}; - - if ( isAIKingdom ) - return startingResourcesSet[5]; - - switch ( difficulty ) { - case Difficulty::EASY: - return startingResourcesSet[0]; - case Difficulty::NORMAL: - return startingResourcesSet[1]; - case Difficulty::HARD: - return startingResourcesSet[2]; - case Difficulty::EXPERT: - return startingResourcesSet[3]; - case Difficulty::IMPOSSIBLE: - return startingResourcesSet[4]; - default: - break; - } - - return startingResourcesSet[1]; -} - int Difficulty::GetScoutingBonus( int difficulty ) { switch ( difficulty ) { diff --git a/src/fheroes2/game/difficulty.h b/src/fheroes2/game/difficulty.h index 47d15fdd568..33f8c683c00 100644 --- a/src/fheroes2/game/difficulty.h +++ b/src/fheroes2/game/difficulty.h @@ -38,7 +38,6 @@ namespace Difficulty const std::string & String( int ); - cost_t GetKingdomStartingResources( int difficulty, bool isAIKingdom ); int GetScoutingBonus( int difficulty ); double GetGoldIncomeBonus( int difficulty ); double GetUnitGrowthBonus( int difficulty ); diff --git a/src/fheroes2/kingdom/kingdom.cpp b/src/fheroes2/kingdom/kingdom.cpp index 90c096fa9a9..416e719cd86 100644 --- a/src/fheroes2/kingdom/kingdom.cpp +++ b/src/fheroes2/kingdom/kingdom.cpp @@ -101,7 +101,7 @@ int Kingdom::GetRace( void ) const void Kingdom::UpdateStartingResource( void ) { - resource = Difficulty::GetKingdomStartingResources( Settings::Get().GameDifficulty(), isControlAI() ); + resource = GetKingdomStartingResources( Settings::Get().GameDifficulty(), isControlAI() ); } bool Kingdom::isLoss( void ) const @@ -827,6 +827,37 @@ bool Kingdom::IsTileVisibleFromCrystalBall( const int32_t dest ) const return false; } +cost_t Kingdom::GetKingdomStartingResources( int difficulty, bool isAIKingdom ) +{ + static cost_t startingResourcesSet[] = {{10000, 30, 10, 30, 10, 10, 10}, + {7500, 20, 5, 20, 5, 5, 5}, + {5000, 10, 2, 10, 2, 2, 2}, + {2500, 5, 0, 5, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0}, + // ai resource + {10000, 30, 10, 30, 10, 10, 10}}; + + if ( isAIKingdom ) + return startingResourcesSet[5]; + + switch ( difficulty ) { + case Difficulty::EASY: + return startingResourcesSet[0]; + case Difficulty::NORMAL: + return startingResourcesSet[1]; + case Difficulty::HARD: + return startingResourcesSet[2]; + case Difficulty::EXPERT: + return startingResourcesSet[3]; + case Difficulty::IMPOSSIBLE: + return startingResourcesSet[4]; + default: + break; + } + + return startingResourcesSet[1]; +} + StreamBase & operator<<( StreamBase & msg, const Kingdom & kingdom ) { return msg << kingdom.modes << kingdom.color << kingdom.resource << kingdom.lost_town_days << kingdom.castles << kingdom.heroes << kingdom.recruits diff --git a/src/fheroes2/kingdom/kingdom.h b/src/fheroes2/kingdom/kingdom.h index a7c28c9ad15..4b3c98c71b2 100644 --- a/src/fheroes2/kingdom/kingdom.h +++ b/src/fheroes2/kingdom/kingdom.h @@ -170,6 +170,7 @@ class Kingdom : public BitModes, public Control bool IsTileVisibleFromCrystalBall( const int32_t dest ) const; static u32 GetMaxHeroes( void ); + static cost_t GetKingdomStartingResources( int difficulty, bool isAIKingdom ); private: friend StreamBase & operator<<( StreamBase &, const Kingdom & ); From 13b609134dd4cebf392ea2293462a78968612813 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Tue, 16 Feb 2021 11:57:20 -0500 Subject: [PATCH 04/84] Add auto battle resolve mode to the game (#2801) --- src/fheroes2/ai/normal/ai_normal_battle.cpp | 3 +- src/fheroes2/battle/battle_arena.cpp | 13 +++++- src/fheroes2/battle/battle_dialogs.cpp | 7 +++ src/fheroes2/battle/battle_main.cpp | 15 ++++--- src/fheroes2/dialog/dialog_system.cpp | 42 ++++++++++++++++-- src/fheroes2/system/settings.cpp | 47 +++++++++++++++++++++ src/fheroes2/system/settings.h | 4 ++ 7 files changed, 119 insertions(+), 12 deletions(-) diff --git a/src/fheroes2/ai/normal/ai_normal_battle.cpp b/src/fheroes2/ai/normal/ai_normal_battle.cpp index 298fb3e8e94..eb73bfcf85b 100644 --- a/src/fheroes2/ai/normal/ai_normal_battle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_battle.cpp @@ -50,7 +50,8 @@ namespace AI bool CheckCommanderCanSpellcast( const Arena & arena, const HeroBase * commander ) { - return commander && commander->HaveSpellBook() && !commander->Modes( Heroes::SPELLCASTED ) && !arena.isSpellcastDisabled(); + return commander && ( !commander->isControlHuman() || Settings::Get().BattleAutoSpellcast() ) && commander->HaveSpellBook() + && !commander->Modes( Heroes::SPELLCASTED ) && !arena.isSpellcastDisabled(); } bool CheckBattleRetreat( double myArmy, double enemy ) diff --git a/src/fheroes2/battle/battle_arena.cpp b/src/fheroes2/battle/battle_arena.cpp index d3f7fbe8881..b7adff9b80c 100644 --- a/src/fheroes2/battle/battle_arena.cpp +++ b/src/fheroes2/battle/battle_arena.cpp @@ -239,6 +239,15 @@ Battle::Arena::Arena( Army & a1, Army & a2, s32 index, bool local ) armies_order->reserve( 25 ); interface->SetArmiesOrder( armies_order ); } + else { + // no interface - force auto battle mode for human player + if ( a1.isControlHuman() ) { + auto_battle |= a1.GetColor(); + } + if ( a2.isControlHuman() ) { + auto_battle |= a2.GetColor(); + } + } towers[0] = NULL; towers[1] = NULL; @@ -391,7 +400,9 @@ void Battle::Arena::TurnTroop( Unit * current_troop ) board.Reset(); - fheroes2::delayforMs( 10 ); + if ( interface ) { + fheroes2::delayforMs( 10 ); + } } } diff --git a/src/fheroes2/battle/battle_dialogs.cpp b/src/fheroes2/battle/battle_dialogs.cpp index b1d35cd7306..3450083d523 100644 --- a/src/fheroes2/battle/battle_dialogs.cpp +++ b/src/fheroes2/battle/battle_dialogs.cpp @@ -172,6 +172,7 @@ void Battle::RedrawBattleSettings( const std::vector & areas ) Text text( str, Font::SMALL ); text.Blit( areas[0].x + ( sprite.width() - text.w() ) / 2, areas[0].y + sprite.height() + 3 ); + RedrawOnOffSetting( areas[2], _( "Auto Spell Casting" ), 6, conf.BattleAutoSpellcast() ); RedrawOnOffSetting( areas[3], _( "Grid" ), 8, conf.BattleShowGrid() ); RedrawOnOffSetting( areas[4], _( "Shadow Movement" ), 10, conf.BattleShowMoveShadow() ); RedrawOnOffSetting( areas[5], _( "Shadow Cursor" ), 12, conf.BattleShowMouseShadow() ); @@ -249,6 +250,12 @@ void Battle::DialogBattleSettings( void ) display.render(); saveConfiguration = true; } + else if ( le.MouseClickLeft( optionAreas[2] ) ) { + conf.setBattleAutoSpellcast( !conf.BattleAutoSpellcast() ); + fheroes2::Blit( dialog, display, pos_rt.x, pos_rt.y ); + RedrawBattleSettings( optionAreas ); + saveConfiguration = true; + } else if ( le.MouseClickLeft( optionAreas[3] ) ) { conf.SetBattleGrid( !conf.BattleShowGrid() ); fheroes2::Blit( dialog, display, pos_rt.x, pos_rt.y ); diff --git a/src/fheroes2/battle/battle_main.cpp b/src/fheroes2/battle/battle_main.cpp index f3982c5f695..20e879834be 100644 --- a/src/fheroes2/battle/battle_main.cpp +++ b/src/fheroes2/battle/battle_main.cpp @@ -83,17 +83,18 @@ Battle::Result Battle::Loader( Army & army1, Army & army2, s32 mapsindex ) army2.GetCommander()->ActionPreBattle(); } - bool local = army1.isControlHuman() || army2.isControlHuman(); + const bool isHumanBattle = army1.isControlHuman() || army2.isControlHuman(); + bool showBattle = !Settings::Get().BattleAutoResolve() && isHumanBattle; #ifdef WITH_DEBUG if ( IS_DEBUG( DBG_BATTLE, DBG_TRACE ) ) - local = true; + showBattle = true; #endif - if ( local ) + if ( showBattle ) AGG::ResetMixer(); - Arena arena( army1, army2, mapsindex, local ); + Arena arena( army1, army2, mapsindex, showBattle ); DEBUG_LOG( DBG_BATTLE, DBG_INFO, "army1 " << army1.String() ); DEBUG_LOG( DBG_BATTLE, DBG_INFO, "army2 " << army2.String() ); @@ -113,15 +114,17 @@ Battle::Result Battle::Loader( Army & army1, Army & army2, s32 mapsindex ) = ( hero_wins && hero_loss && !( ( RESULT_RETREAT | RESULT_SURRENDER ) & loss_result ) && hero_wins->isHeroes() && hero_loss->isHeroes() ); bool artifactsTransferred = !transferArtifacts; - if ( local ) { + if ( showBattle ) { AGG::ResetMixer(); // fade arena const bool clearMessageLog = ( result.army1 & RESULT_RETREAT ) || ( result.army2 & RESULT_RETREAT ) || ( result.army1 & RESULT_SURRENDER ) || ( result.army2 & RESULT_SURRENDER ); arena.FadeArena( clearMessageLog ); + } - // dialog summary + // summary dialog + if ( isHumanBattle ) { if ( isWinnerHuman ) { artifactsTransferred = true; } diff --git a/src/fheroes2/dialog/dialog_system.cpp b/src/fheroes2/dialog/dialog_system.cpp index 49813be2c93..008637abae3 100644 --- a/src/fheroes2/dialog/dialog_system.cpp +++ b/src/fheroes2/dialog/dialog_system.cpp @@ -76,7 +76,7 @@ int Dialog::SystemOptions( void ) rects.emplace_back( optionOffset.x + 2 * optionStep.x, optionOffset.y + optionStep.y, optionSprite.width(), optionSprite.height() ); rects.emplace_back( optionOffset.x, optionOffset.y + 2 * optionStep.y, optionSprite.width(), optionSprite.height() ); rects.emplace_back( optionOffset.x + optionStep.x, optionOffset.y + 2 * optionStep.y, optionSprite.width(), optionSprite.height() ); - rects.emplace_back( optionOffset.x + 2 * optionStep.x, optionOffset.y + 2 * optionStep.y, optionSprite.width(), optionSprite.height() ); // not in use + rects.emplace_back( optionOffset.x + 2 * optionStep.x, optionOffset.y + 2 * optionStep.y, optionSprite.width(), optionSprite.height() ); const fheroes2::Rect & rect1 = rects[0]; const fheroes2::Rect & rect2 = rects[1]; @@ -86,6 +86,7 @@ int Dialog::SystemOptions( void ) const fheroes2::Rect & rect6 = rects[5]; const fheroes2::Rect & rect7 = rects[6]; const fheroes2::Rect & rect8 = rects[7]; + const fheroes2::Rect & rect9 = rects[8]; DrawSystemInfo( rects ); @@ -182,6 +183,26 @@ int Dialog::SystemOptions( void ) saveConfig = true; } + // toggle manual/auto battles + if ( le.MouseClickLeft( rect9 ) ) { + if ( conf.BattleAutoResolve() ) { + if ( conf.BattleAutoSpellcast() ) { + conf.setBattleAutoSpellcast( false ); + } + else { + conf.setBattleAutoResolve( false ); + } + } + else { + conf.setBattleAutoResolve( true ); + conf.setBattleAutoSpellcast( true ); + } + + result |= 0x20; + redraw = true; + saveConfig = true; + } + if ( redraw ) { fheroes2::Blit( dialog, display, dialogArea.x, dialogArea.y ); DrawSystemInfo( rects ); @@ -348,10 +369,23 @@ void Dialog::DrawSystemInfo( const std::vector & rects ) text.Set( str ); text.Blit( rect8.x + ( rect8.w - text.w() ) / 2, rect8.y + rect8.h + textOffset ); - // unused - // const fheroes2::Sprite & sprite9 = fheroes2::AGG::GetICN(ICN::SPANEL, 17); + // auto-battles const Rect & rect9 = rects[8]; - str = "unused"; + str = _( "Battles" ); + text.Set( str ); + text.Blit( rect9.x + ( rect9.w - text.w() ) / 2, rect9.y - text.h() - textOffset ); + + if ( conf.BattleAutoResolve() ) { + const bool spellcast = conf.BattleAutoSpellcast(); + str = spellcast ? _( "Auto Resolve" ) : str = _( "Auto, No Spells" ); + + const fheroes2::Sprite & sprite9 = fheroes2::AGG::GetICN( ICN::CSPANEL, spellcast ? 7 : 6 ); + fheroes2::Blit( sprite9, display, rect9.x, rect9.y ); + } + else { + str = _( "Manual" ); + fheroes2::Blit( fheroes2::AGG::GetICN( ICN::SPANEL, 18 ), display, rect9.x, rect9.y ); + } text.Set( str ); text.Blit( rect9.x + ( rect9.w - text.w() ) / 2, rect9.y + rect9.h + textOffset ); } diff --git a/src/fheroes2/system/settings.cpp b/src/fheroes2/system/settings.cpp index 10863afed8d..c930532900e 100644 --- a/src/fheroes2/system/settings.cpp +++ b/src/fheroes2/system/settings.cpp @@ -67,6 +67,8 @@ enum GLOBAL_BATTLE_SHOW_GRID = 0x00800000, GLOBAL_BATTLE_SHOW_MOUSE_SHADOW = 0x01000000, GLOBAL_BATTLE_SHOW_MOVE_SHADOW = 0x02000000, + GLOBAL_BATTLE_AUTO_RESOLVE = 0x04000000, + GLOBAL_BATTLE_AUTO_SPELLCAST = 0x08000000, GLOBAL_MUSIC = GLOBAL_MUSIC_CD | GLOBAL_MUSIC_EXT | GLOBAL_MUSIC_MIDI }; @@ -373,6 +375,7 @@ Settings::Settings() opt_global.SetModes( GLOBAL_BATTLE_SHOW_GRID ); opt_global.SetModes( GLOBAL_BATTLE_SHOW_MOUSE_SHADOW ); opt_global.SetModes( GLOBAL_BATTLE_SHOW_MOVE_SHADOW ); + opt_global.SetModes( GLOBAL_BATTLE_AUTO_SPELLCAST ); } Settings::~Settings() @@ -602,6 +605,14 @@ bool Settings::Read( const std::string & filename ) SetBattleMouseShaded( config.StrParams( "battle shadow cursor" ) == "on" ); } + if ( config.Exists( "auto resolve battles" ) ) { + setBattleAutoResolve( config.StrParams( "auto resolve battles" ) == "on" ); + } + + if ( config.Exists( "auto spell casting" ) ) { + setBattleAutoSpellcast( config.StrParams( "auto spell casting" ) == "on" ); + } + // network port port = config.Exists( "port" ) ? config.IntParams( "port" ) : DEFAULT_PORT; @@ -766,6 +777,12 @@ std::string Settings::String( void ) const os << std::endl << "# show battle shadow cursor: on off" << std::endl; os << "battle shadow cursor = " << ( opt_global.Modes( GLOBAL_BATTLE_SHOW_MOUSE_SHADOW ) ? "on" : "off" ) << std::endl; + os << std::endl << "# auto resolve battles: on off" << std::endl; + os << "auto resolve battles = " << ( opt_global.Modes( GLOBAL_BATTLE_AUTO_RESOLVE ) ? "on" : "off" ) << std::endl; + + os << std::endl << "# auto combat spell casting: on off" << std::endl; + os << "auto spell casting = " << ( opt_global.Modes( GLOBAL_BATTLE_AUTO_SPELLCAST ) ? "on" : "off" ) << std::endl; + if ( video_driver.size() ) { os << std::endl << "# sdl video driver, windows: windib, directx, wince: gapi, raw, linux: x11, other see sdl manual (to be deprecated)" << std::endl; os << "videodriver = " << video_driver << std::endl; @@ -1051,6 +1068,26 @@ void Settings::SetBattleSpeed( int speed ) battle_speed = speed; } +void Settings::setBattleAutoResolve( bool enable ) +{ + if ( enable ) { + opt_global.SetModes( GLOBAL_BATTLE_AUTO_RESOLVE ); + } + else { + opt_global.ResetModes( GLOBAL_BATTLE_AUTO_RESOLVE ); + } +} + +void Settings::setBattleAutoSpellcast( bool enable ) +{ + if ( enable ) { + opt_global.SetModes( GLOBAL_BATTLE_AUTO_SPELLCAST ); + } + else { + opt_global.ResetModes( GLOBAL_BATTLE_AUTO_SPELLCAST ); + } +} + void Settings::setFullScreen( const bool enable ) { if ( enable ) { @@ -1146,6 +1183,16 @@ bool Settings::BattleShowMoveShadow( void ) const return opt_global.Modes( GLOBAL_BATTLE_SHOW_MOVE_SHADOW ); } +bool Settings::BattleAutoResolve( void ) const +{ + return opt_global.Modes( GLOBAL_BATTLE_AUTO_RESOLVE ); +} + +bool Settings::BattleAutoSpellcast( void ) const +{ + return opt_global.Modes( GLOBAL_BATTLE_AUTO_SPELLCAST ); +} + const fheroes2::Size & Settings::VideoMode() const { return video_mode; diff --git a/src/fheroes2/system/settings.h b/src/fheroes2/system/settings.h index 8e2f283a0b8..ba357329d3b 100644 --- a/src/fheroes2/system/settings.h +++ b/src/fheroes2/system/settings.h @@ -174,6 +174,8 @@ class Settings bool BattleShowGrid( void ) const; bool BattleShowMouseShadow( void ) const; bool BattleShowMoveShadow( void ) const; + bool BattleAutoResolve() const; + bool BattleAutoSpellcast() const; bool UseAltResource( void ) const; bool PriceLoyaltyVersion( void ) const; bool LoadedGameVersion( void ) const; @@ -258,6 +260,8 @@ class Settings void SetScrollSpeed( int ); void SetHeroesMoveSpeed( int ); void SetBattleSpeed( int ); + void setBattleAutoResolve( bool enable ); + void setBattleAutoSpellcast( bool enable ); void setFullScreen( const bool enable ); void SetSoundVolume( int v ); From 522d68ab05e45e8048f97d27da89e105fd04a3f2 Mon Sep 17 00:00:00 2001 From: Taras <11291116+Northfear@users.noreply.github.com> Date: Wed, 17 Feb 2021 06:07:16 +0200 Subject: [PATCH 05/84] Fix wrong controller pointer speed option name and touch coordinates translation (#2813) --- src/engine/localevent.cpp | 6 ++---- src/fheroes2/system/settings.cpp | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/engine/localevent.cpp b/src/engine/localevent.cpp index 42cf2c24dd9..a5fce5f4602 100644 --- a/src/engine/localevent.cpp +++ b/src/engine/localevent.cpp @@ -1153,10 +1153,8 @@ void LocalEvent::HandleTouchEvent( const SDL_TouchFingerEvent & event ) SetModes( MOUSE_MOTION ); - _emulatedPointerPosX = ( ( screenResolution.width * event.x ) - ( screenResolution.width - windowRect.width - windowRect.x ) ) - * ( static_cast( gameSurfaceRes.width ) / windowRect.width ); - _emulatedPointerPosY = ( ( screenResolution.height * event.y ) - ( screenResolution.height - windowRect.height - windowRect.y ) ) - * ( static_cast( gameSurfaceRes.height ) / windowRect.height ); + _emulatedPointerPosX = ( screenResolution.width * event.x - windowRect.x ) * ( static_cast( gameSurfaceRes.width ) / windowRect.width ); + _emulatedPointerPosY = ( screenResolution.height * event.y - windowRect.y ) * ( static_cast( gameSurfaceRes.height ) / windowRect.height ); mouse_cu.x = static_cast( _emulatedPointerPosX ); mouse_cu.y = static_cast( _emulatedPointerPosY ); diff --git a/src/fheroes2/system/settings.cpp b/src/fheroes2/system/settings.cpp index c930532900e..983ed3cdae4 100644 --- a/src/fheroes2/system/settings.cpp +++ b/src/fheroes2/system/settings.cpp @@ -646,8 +646,8 @@ bool Settings::Read( const std::string & filename ) } } - if ( config.Exists( "controller_pointer_speed" ) ) { - _controllerPointerSpeed = config.IntParams( "controller_pointer_speed" ); + if ( config.Exists( "controller pointer speed" ) ) { + _controllerPointerSpeed = config.IntParams( "controller pointer speed" ); if ( _controllerPointerSpeed > 100 ) _controllerPointerSpeed = 100; else if ( _controllerPointerSpeed < 0 ) From 9943758cf249bb8271148719aadbe17a4a22f3d0 Mon Sep 17 00:00:00 2001 From: vincent-grosbois Date: Wed, 17 Feb 2021 05:07:54 +0100 Subject: [PATCH 06/84] Deterministic bonus for hero level-up (#2630) close #1973 --- src/engine/rand.cpp | 34 ++++++++---- src/engine/rand.h | 11 ++-- src/fheroes2/heroes/heroes.cpp | 98 +++++++++++++++++++++++----------- src/fheroes2/heroes/heroes.h | 12 ++++- src/fheroes2/heroes/skill.cpp | 18 +++---- src/fheroes2/heroes/skill.h | 4 +- src/fheroes2/system/settings.h | 3 +- src/fheroes2/world/world.cpp | 20 ++++++- src/fheroes2/world/world.h | 4 ++ 9 files changed, 145 insertions(+), 59 deletions(-) diff --git a/src/engine/rand.cpp b/src/engine/rand.cpp index 7c83b36c181..41d45652a85 100644 --- a/src/engine/rand.cpp +++ b/src/engine/rand.cpp @@ -37,24 +37,30 @@ uint32_t Rand::Get( uint32_t from, uint32_t to ) if ( to == 0 || from > to ) std::swap( from, to ); - std::uniform_int_distribution<> distrib( from, to ); + std::uniform_int_distribution distrib( from, to ); - return static_cast( distrib( s_gen ) ); + return distrib( s_gen ); } -Rand::Queue::Queue( u32 size ) +uint32_t Rand::GetWithSeed( uint32_t from, uint32_t to, uint32_t seed ) { - reserve( size ); + if ( from > to ) + std::swap( from, to ); + + std::uniform_int_distribution distrib( from, to ); + std::mt19937 seededGen( seed ); + + return distrib( seededGen ); } -void Rand::Queue::Reset( void ) +Rand::Queue::Queue( u32 size ) { - clear(); + reserve( size ); } void Rand::Queue::Push( s32 value, u32 percent ) { - if ( percent ) + if ( percent > 0 ) emplace_back( value, percent ); } @@ -63,7 +69,7 @@ size_t Rand::Queue::Size( void ) const return size(); } -s32 Rand::Queue::Get( void ) +int32_t Rand::Queue::Get( const std::function & randomFunc ) { std::vector::iterator it; @@ -86,7 +92,7 @@ s32 Rand::Queue::Get( void ) for ( ; it != end(); ++it ) max += ( *it ).second; - uint32_t rand = Rand::Get( max ); + uint32_t rand = randomFunc( max ); uint32_t amount = 0; it = begin(); @@ -99,3 +105,13 @@ s32 Rand::Queue::Get( void ) ERROR_LOG( "weight not found, return 0" ); return 0; } + +int32_t Rand::Queue::Get() +{ + return Rand::Queue::Get( []( uint32_t max ) { return Rand::Get( 0, max ); } ); +} + +int32_t Rand::Queue::GetWithSeed( uint32_t seed ) +{ + return Rand::Queue::Get( [seed]( uint32_t max ) { return Rand::GetWithSeed( 0, max, seed ); } ); +} diff --git a/src/engine/rand.h b/src/engine/rand.h index d7ec583d6ab..1690346a8e7 100644 --- a/src/engine/rand.h +++ b/src/engine/rand.h @@ -23,6 +23,7 @@ #define H2RAND_H #include +#include #include #include #include @@ -32,6 +33,7 @@ namespace Rand { uint32_t Get( uint32_t from, uint32_t to = 0 ); + uint32_t GetWithSeed( uint32_t from, uint32_t to, uint32_t seed ); template const T * Get( const std::vector & vec ) @@ -61,10 +63,13 @@ namespace Rand public: Queue( u32 size = 0 ); - void Reset( void ); - void Push( s32, u32 ); + void Push( s32 value, u32 percent ); size_t Size( void ) const; - s32 Get( void ); + int32_t Get(); + int32_t GetWithSeed( uint32_t seed ); + + private: + int32_t Get( const std::function & randomFunc ); }; } diff --git a/src/fheroes2/heroes/heroes.cpp b/src/fheroes2/heroes/heroes.cpp index 361c67e9219..afb051f8bd3 100644 --- a/src/fheroes2/heroes/heroes.cpp +++ b/src/fheroes2/heroes/heroes.cpp @@ -50,6 +50,16 @@ #include "text.h" #include "world.h" +namespace +{ + template + void hashCombine( std::size_t & seed, const T & v ) + { + std::hash hasher; + seed ^= hasher( v ) + 0x9e3779b9 + ( seed << 6 ) + ( seed >> 2 ); + } +} + const char * Heroes::GetName( int id ) { const char * names[] @@ -1432,69 +1442,60 @@ int Heroes::GetRangeRouteDays( s32 dst ) const return 0; } -/* up level */ void Heroes::LevelUp( bool skipsecondary, bool autoselect ) { - int primary = LevelUpPrimarySkill(); + const HeroSeedsForLevelUp seeds = GetSeedsForLevelUp(); + + // level up primary skill + const int primarySkill = Skill::Primary::LevelUp( race, GetLevel(), seeds.seedPrimarySkill ); + + DEBUG_LOG( DBG_GAME, DBG_INFO, "for " << GetName() << ", up " << Skill::Primary::String( primarySkill ) ); + if ( !skipsecondary ) - LevelUpSecondarySkill( primary, ( autoselect || isControlAI() ) ); + LevelUpSecondarySkill( seeds, primarySkill, ( autoselect || isControlAI() ) ); if ( isControlAI() ) AI::Get().HeroesLevelUp( *this ); } -int Heroes::LevelUpPrimarySkill( void ) -{ - int skill = Skill::Primary::LevelUp( race, GetLevel() ); - - DEBUG_LOG( DBG_GAME, DBG_INFO, "for " << GetName() << ", up " << Skill::Primary::String( skill ) ); - return skill; -} - -void Heroes::LevelUpSecondarySkill( int primary, bool autoselect ) +void Heroes::LevelUpSecondarySkill( const HeroSeedsForLevelUp & seeds, int primary, bool autoselect ) { Skill::Secondary sec1; Skill::Secondary sec2; - secondary_skills.FindSkillsForLevelUp( race, sec1, sec2 ); + secondary_skills.FindSkillsForLevelUp( race, seeds.seedSecondaySkill1, seeds.seedSecondaySkill2, sec1, sec2 ); DEBUG_LOG( DBG_GAME, DBG_INFO, GetName() << " select " << Skill::Secondary::String( sec1.Skill() ) << " or " << Skill::Secondary::String( sec2.Skill() ) ); - const Skill::Secondary * selected = NULL; + + Skill::Secondary selected; if ( autoselect ) { - if ( Skill::Secondary::UNKNOWN == sec1.Skill() || Skill::Secondary::UNKNOWN == sec2.Skill() ) { - if ( Skill::Secondary::UNKNOWN != sec1.Skill() ) - selected = &sec1; - else if ( Skill::Secondary::UNKNOWN != sec2.Skill() ) - selected = &sec2; + if ( sec1.isValid() && sec2.isValid() ) { + selected = Rand::GetWithSeed( 0, 1, seeds.seedSecondaySkillRandomChoose ) ? sec1 : sec2; + } + else { + selected = sec1.isValid() ? sec1 : sec2; } - else if ( Skill::Secondary::UNKNOWN != sec1.Skill() && Skill::Secondary::UNKNOWN != sec2.Skill() ) - selected = ( Rand::Get( 0, 1 ) ? &sec1 : &sec2 ); } else { AGG::PlaySound( M82::NWHEROLV ); int result = Dialog::LevelUpSelectSkill( name, Skill::Primary::String( primary ), sec1, sec2, *this ); if ( Skill::Secondary::UNKNOWN != result ) - selected = result == sec2.Skill() ? &sec2 : &sec1; + selected = result == sec2.Skill() ? sec2 : sec1; } // level up sec. skill - if ( selected ) { - DEBUG_LOG( DBG_GAME, DBG_INFO, GetName() << ", selected: " << Skill::Secondary::String( selected->Skill() ) ); - Skill::Secondary * secs = secondary_skills.FindSkill( selected->Skill() ); + if ( selected.isValid() ) { + DEBUG_LOG( DBG_GAME, DBG_INFO, GetName() << ", selected: " << Skill::Secondary::String( selected.Skill() ) ); + Skill::Secondary * secs = secondary_skills.FindSkill( selected.Skill() ); if ( secs ) secs->NextLevel(); else - secondary_skills.AddSkill( Skill::Secondary( selected->Skill(), Skill::Level::BASIC ) ); + secondary_skills.AddSkill( Skill::Secondary( selected.Skill(), Skill::Level::BASIC ) ); // post action - switch ( selected->Skill() ) { - case Skill::Secondary::SCOUTING: + if ( selected.Skill() == Skill::Secondary::SCOUTING ) { Scoute(); - break; - - default: - break; } } } @@ -2028,6 +2029,39 @@ bool AllHeroes::HaveTwoFreemans( void ) const return 2 <= std::count_if( begin(), end(), []( const Heroes * hero ) { return hero->isFreeman(); } ); } +HeroSeedsForLevelUp Heroes::GetSeedsForLevelUp() const +{ + /* We generate seeds based on the hero and global world map seed + * The idea is that, we want the skill selection to be randomized at each map restart, + * but deterministic for a given hero. + * We also want the available skills to change depending on current skills/stats of the hero, + * to avoid giving out the same skills/stats at each level up. We can't use the level field for this, as it + * doesn't change when we level up several levels at once. + * We also need to generate different seeds for each possible call to the random number generator, + * in order to avoid always drawing the same random number at level-up: otherwise this + * would mean that for all possible games, the 2nd secondary + * skill would always be the same once the 1st one is selected. + * */ + + size_t hash = world.GetMapSeed(); + hashCombine( hash, hid ); + hashCombine( hash, race ); + hashCombine( hash, attack ); + hashCombine( hash, defense ); + hashCombine( hash, power ); + hashCombine( hash, knowledge ); + for ( int skillId = Skill::Secondary::PATHFINDING; skillId <= Skill::Secondary::ESTATES; ++skillId ) { + hashCombine( hash, GetLevelSkill( skillId ) ); + } + + HeroSeedsForLevelUp seeds; + seeds.seedPrimarySkill = static_cast( hash ); + seeds.seedSecondaySkill1 = seeds.seedPrimarySkill + 1; + seeds.seedSecondaySkill2 = seeds.seedPrimarySkill + 2; + seeds.seedSecondaySkillRandomChoose = seeds.seedPrimarySkill + 3; + return seeds; +} + StreamBase & operator<<( StreamBase & msg, const VecHeroes & heroes ) { msg << static_cast( heroes.size() ); diff --git a/src/fheroes2/heroes/heroes.h b/src/fheroes2/heroes/heroes.h index 0638338005a..2fffb892f6d 100644 --- a/src/fheroes2/heroes/heroes.h +++ b/src/fheroes2/heroes/heroes.h @@ -46,6 +46,14 @@ namespace Interface class GameArea; } +struct HeroSeedsForLevelUp +{ + uint32_t seedPrimarySkill = 0; + uint32_t seedSecondaySkill1 = 0; + uint32_t seedSecondaySkill2 = 0; + uint32_t seedSecondaySkillRandomChoose = 0; +}; + class Heroes : public HeroBase, public ColorBase { public: @@ -327,9 +335,9 @@ class Heroes : public HeroBase, public ColorBase friend class Recruits; friend class Battle::Only; + HeroSeedsForLevelUp GetSeedsForLevelUp() const; void LevelUp( bool skipsecondary, bool autoselect = false ); - int LevelUpPrimarySkill( void ); - void LevelUpSecondarySkill( int, bool autoselect = false ); + void LevelUpSecondarySkill( const HeroSeedsForLevelUp & seeds, int primary, bool autoselect = false ); void AngleStep( int ); bool MoveStep( bool fast = false ); static void MoveStep( Heroes &, s32 to, bool newpos ); diff --git a/src/fheroes2/heroes/skill.cpp b/src/fheroes2/heroes/skill.cpp index 52ae476dc41..2bbaa73e7f4 100644 --- a/src/fheroes2/heroes/skill.cpp +++ b/src/fheroes2/heroes/skill.cpp @@ -39,7 +39,7 @@ namespace Skill { int SecondaryGetWeightSkillFromRace( int race, int skill ); - int SecondaryPriorityFromRace( int, const std::vector & ); + int SecondaryPriorityFromRace( int, const std::vector &, uint32_t seed ); const int secskills[] = {Secondary::PATHFINDING, Secondary::ARCHERY, Secondary::LOGISTICS, Secondary::SCOUTING, Secondary::DIPLOMACY, Secondary::NAVIGATION, Secondary::LEADERSHIP, @@ -113,7 +113,7 @@ int Skill::Primary::GetInitialSpell( int race ) return ptr ? ptr->initial_spell : 0; } -int Skill::Primary::LevelUp( int race, int level ) +int Skill::Primary::LevelUp( int race, int level, uint32_t seed ) { Rand::Queue percents( MAXPRIMARYSKILL ); @@ -133,7 +133,7 @@ int Skill::Primary::LevelUp( int race, int level ) } } - int result = percents.Size() ? percents.Get() : UNKNOWN; + int result = percents.Size() ? percents.GetWithSeed( seed ) : UNKNOWN; switch ( result ) { case ATTACK: @@ -744,19 +744,19 @@ int Skill::SecondaryGetWeightSkillFromRace( int race, int skill ) return 0; } -int Skill::SecondaryPriorityFromRace( int race, const std::vector & exclude ) +int Skill::SecondaryPriorityFromRace( int race, const std::vector & exclude, uint32_t seed ) { Rand::Queue parts( MAXSECONDARYSKILL ); for ( u32 ii = 0; ii < ARRAY_COUNT( secskills ); ++ii ) - if ( exclude.empty() || exclude.end() == std::find( exclude.begin(), exclude.end(), secskills[ii] ) ) + if ( exclude.end() == std::find( exclude.begin(), exclude.end(), secskills[ii] ) ) parts.Push( secskills[ii], SecondaryGetWeightSkillFromRace( race, secskills[ii] ) ); - return parts.Size() ? parts.Get() : Secondary::UNKNOWN; + return parts.Size() ? parts.GetWithSeed( seed ) : Secondary::UNKNOWN; } /* select secondary skills for level up */ -void Skill::SecSkills::FindSkillsForLevelUp( int race, Secondary & sec1, Secondary & sec2 ) const +void Skill::SecSkills::FindSkillsForLevelUp( int race, uint32_t seedSkill1, uint32_t seedSkill2, Secondary & sec1, Secondary & sec2 ) const { std::vector exclude_skills; exclude_skills.reserve( MAXSECONDARYSKILL + HEROESMAXSKILL ); @@ -773,11 +773,11 @@ void Skill::SecSkills::FindSkillsForLevelUp( int race, Secondary & sec1, Seconda exclude_skills.push_back( secskills[ii] ); } - sec1.SetSkill( SecondaryPriorityFromRace( race, exclude_skills ) ); + sec1.SetSkill( SecondaryPriorityFromRace( race, exclude_skills, seedSkill1 ) ); if ( Secondary::UNKNOWN != sec1.Skill() ) { exclude_skills.push_back( sec1.Skill() ); - sec2.SetSkill( SecondaryPriorityFromRace( race, exclude_skills ) ); + sec2.SetSkill( SecondaryPriorityFromRace( race, exclude_skills, seedSkill2 ) ); sec1.SetLevel( GetLevel( sec1.Skill() ) ); sec2.SetLevel( GetLevel( sec2.Skill() ) ); diff --git a/src/fheroes2/heroes/skill.h b/src/fheroes2/heroes/skill.h index 8fe3492da6b..4abead4a467 100644 --- a/src/fheroes2/heroes/skill.h +++ b/src/fheroes2/heroes/skill.h @@ -122,7 +122,7 @@ namespace Skill int GetLevel( int skill ) const; u32 GetValues( int skill ) const; void AddSkill( const Skill::Secondary & ); - void FindSkillsForLevelUp( int race, Secondary &, Secondary & ) const; + void FindSkillsForLevelUp( int race, uint32_t seedSkill1, uint32_t seedSkill2, Secondary &, Secondary & ) const; void FillMax( const Skill::Secondary & ); Secondary * FindSkill( int ); std::string String( void ) const; @@ -164,7 +164,7 @@ namespace Skill virtual bool isCaptain( void ) const; virtual bool isHeroes( void ) const; - int LevelUp( int race, int level ); + int LevelUp( int race, int level, uint32_t seed ); static const char * String( int ); static std::string StringDescription( int, const Heroes * ); diff --git a/src/fheroes2/system/settings.h b/src/fheroes2/system/settings.h index ba357329d3b..9837967acbc 100644 --- a/src/fheroes2/system/settings.h +++ b/src/fheroes2/system/settings.h @@ -32,6 +32,7 @@ #include "maps_fileinfo.h" #include "players.h" +#define FORMAT_VERSION_091_RELEASE 9100 #define FORMAT_VERSION_090_RELEASE 9001 #define FORMAT_VERSION_084_RELEASE 9000 #define FORMAT_VERSION_082_RELEASE 8200 @@ -40,7 +41,7 @@ #define FORMAT_VERSION_3255 3255 #define LAST_FORMAT_VERSION FORMAT_VERSION_3255 -#define CURRENT_FORMAT_VERSION FORMAT_VERSION_090_RELEASE // TODO: update this value for a new release +#define CURRENT_FORMAT_VERSION FORMAT_VERSION_091_RELEASE // TODO: update this value for a new release enum { diff --git a/src/fheroes2/world/world.cpp b/src/fheroes2/world/world.cpp index 6715e38fd00..3621c0303a3 100644 --- a/src/fheroes2/world/world.cpp +++ b/src/fheroes2/world/world.cpp @@ -261,6 +261,9 @@ void World::Defaults( void ) vec_heroes.Init(); vec_castles.Init(); + + // map seed is random and persisted on saves + _seed = Rand::Get( std::numeric_limits::max() ); } void World::Reset( void ) @@ -299,6 +302,8 @@ void World::Reset( void ) heroes_cond_wins = Heroes::UNKNOWN; heroes_cond_loss = Heroes::UNKNOWN; + + _seed = 0; } /* new maps */ @@ -1066,6 +1071,11 @@ void World::PostLoad() ComputeStaticAnalysis(); } +uint32_t World::GetMapSeed() const +{ + return _seed; +} + StreamBase & operator<<( StreamBase & msg, const CapturedObject & obj ) { return msg << obj.objcol << obj.guardians << obj.split; @@ -1192,7 +1202,7 @@ StreamBase & operator<<( StreamBase & msg, const World & w ) const Size & sz = w; return msg << sz << w.vec_tiles << w.vec_heroes << w.vec_castles << w.vec_kingdoms << w.vec_rumors << w.vec_eventsday << w.map_captureobj << w.ultimate_artifact - << w.day << w.week << w.month << w.week_current << w.week_next << w.heroes_cond_wins << w.heroes_cond_loss << w.map_actions << w.map_objects; + << w.day << w.week << w.month << w.week_current << w.week_next << w.heroes_cond_wins << w.heroes_cond_loss << w.map_actions << w.map_objects << w._seed; } StreamBase & operator>>( StreamBase & msg, World & w ) @@ -1202,6 +1212,14 @@ StreamBase & operator>>( StreamBase & msg, World & w ) msg >> sz >> w.vec_tiles >> w.vec_heroes >> w.vec_castles >> w.vec_kingdoms >> w.vec_rumors >> w.vec_eventsday >> w.map_captureobj >> w.ultimate_artifact >> w.day >> w.week >> w.month >> w.week_current >> w.week_next >> w.heroes_cond_wins >> w.heroes_cond_loss >> w.map_actions >> w.map_objects; + if ( Game::GetLoadVersion() >= FORMAT_VERSION_091_RELEASE ) { + msg >> w._seed; + } + else { + // For old versions, generate a different seed at each map loading + w._seed = Rand::Get( std::numeric_limits::max() ); + } + w.PostLoad(); // heroes postfix diff --git a/src/fheroes2/world/world.h b/src/fheroes2/world/world.h index f98d15f3294..82a70cc443e 100644 --- a/src/fheroes2/world/world.h +++ b/src/fheroes2/world/world.h @@ -270,6 +270,8 @@ class World : protected Size void ComputeStaticAnalysis(); static u32 GetUniq( void ); + uint32_t GetMapSeed() const; + private: World() : Size( 0, 0 ) @@ -322,6 +324,8 @@ class World : protected Size Maps::Indexes _whirlpoolTiles; std::vector _regions; PlayerWorldPathfinder _pathfinder; + + uint32_t _seed; }; StreamBase & operator<<( StreamBase &, const CapturedObject & ); From ff2f106d823b4a0e5c9ea1e54bdba5828429f89a Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Tue, 16 Feb 2021 23:09:19 -0500 Subject: [PATCH 07/84] Persist AI variables in savegames (#2798) --- src/fheroes2/ai/ai.h | 19 ++++++++----------- src/fheroes2/ai/ai_base.cpp | 16 +++++++++++++--- src/fheroes2/ai/normal/ai_normal_hero.cpp | 10 +++++----- src/fheroes2/heroes/heroes.cpp | 15 ++++++--------- src/fheroes2/heroes/heroes.h | 15 +++++++++++++-- src/fheroes2/system/players.cpp | 23 +++++++++++++++++++---- src/fheroes2/system/players.h | 8 ++++++++ 7 files changed, 72 insertions(+), 34 deletions(-) diff --git a/src/fheroes2/ai/ai.h b/src/fheroes2/ai/ai.h index cefeab81c14..50505480b53 100644 --- a/src/fheroes2/ai/ai.h +++ b/src/fheroes2/ai/ai.h @@ -26,6 +26,7 @@ #include "gamedefs.h" #include "rand.h" +class StreamBase; class Funds; class Castle; class HeroBase; @@ -57,17 +58,6 @@ namespace AI EXPLORER }; - enum modes_t - { - HERO_SKIP_TURN = 0x02000000, - HERO_WAITING = 0x04000000, - HERO_MOVED = 0x08000000, - HERO_SCOUT = 0x10000000, - HERO_HUNTER = 0x20000000, - HERO_COURIER = 0x40000000, - HERO_CHAMPION = 0x80000000 - }; - const double ARMY_STRENGTH_ADVANTAGE_SMALL = 1.3; const double ARMY_STRENGTH_ADVANTAGE_MEDUIM = 1.5; const double ARMY_STRENGTH_ADVANTAGE_LARGE = 1.8; @@ -116,6 +106,10 @@ namespace AI int _personality = NONE; Base() {} + + private: + friend StreamBase & operator<<( StreamBase &, const AI::Base & ); + friend StreamBase & operator>>( StreamBase &, AI::Base & ); }; Base & Get( AI_TYPE type = NORMAL ); @@ -130,6 +124,9 @@ namespace AI bool BuildIfEnoughResources( Castle & castle, int building, uint32_t minimumMultiplicator ); uint32_t GetResourceMultiplier( const Castle & castle, uint32_t min, uint32_t max ); void ReinforceHeroInCastle( Heroes & hero, Castle & castle, const Funds & budget ); + + StreamBase & operator<<( StreamBase &, const AI::Base & ); + StreamBase & operator>>( StreamBase &, AI::Base & ); } #endif diff --git a/src/fheroes2/ai/ai_base.cpp b/src/fheroes2/ai/ai_base.cpp index 209f300fdab..92f99e99d1a 100644 --- a/src/fheroes2/ai/ai_base.cpp +++ b/src/fheroes2/ai/ai_base.cpp @@ -116,14 +116,14 @@ namespace AI bool Base::HeroesCanMove( const Heroes & hero ) { - return hero.MayStillMove() && !hero.Modes( HERO_MOVED ); + return hero.MayStillMove() && !hero.Modes( Heroes::MOVED ); } void Base::HeroTurn( Heroes & hero ) { Interface::StatusWindow & status = Interface::Basic::Get().GetStatusWindow(); - hero.ResetModes( HERO_MOVED ); + hero.ResetModes( Heroes::MOVED ); while ( Base::HeroesCanMove( hero ) ) { // turn indicator @@ -139,7 +139,7 @@ namespace AI // heroes AI turn AI::HeroesMove( hero ); - hero.SetModes( HERO_MOVED ); + hero.SetModes( Heroes::MOVED ); // turn indicator status.RedrawTurnProgress( 7 ); @@ -196,4 +196,14 @@ namespace AI // end action actions.push_back( Battle::Command( Battle::MSG_BATTLE_END_TURN, currentUnit.GetUID() ) ); } + + StreamBase & operator<<( StreamBase & msg, const AI::Base & instance ) + { + return msg << instance._personality; + } + + StreamBase & operator>>( StreamBase & msg, AI::Base & instance ) + { + return msg >> instance._personality; + } } diff --git a/src/fheroes2/ai/normal/ai_normal_hero.cpp b/src/fheroes2/ai/normal/ai_normal_hero.cpp index 0fbfe921c15..e0a2e9e6c42 100644 --- a/src/fheroes2/ai/normal/ai_normal_hero.cpp +++ b/src/fheroes2/ai/normal/ai_normal_hero.cpp @@ -338,13 +338,13 @@ namespace AI if ( hero->Modes( Heroes::PATROL ) ) { if ( hero->GetSquarePatrol() == 0 ) { DEBUG_LOG( DBG_AI, DBG_TRACE, hero->GetName() << " standing still. Skip turn." ); - hero->SetModes( AI::HERO_MOVED ); + hero->SetModes( Heroes::MOVED ); continue; } } - hero->ResetModes( AI::HERO_WAITING | AI::HERO_MOVED | AI::HERO_SKIP_TURN ); + hero->ResetModes( Heroes::WAITING | Heroes::MOVED | Heroes::SKIPPED_TURN ); if ( !hero->MayStillMove() ) { - hero->SetModes( AI::HERO_MOVED ); + hero->SetModes( Heroes::MOVED ); } else { availableHeroes.emplace_back(); @@ -393,7 +393,7 @@ namespace AI for ( size_t i = 0; i < availableHeroes.size(); ) { if ( !availableHeroes[i].hero->MayStillMove() ) { - availableHeroes[i].hero->SetModes( AI::HERO_MOVED ); + availableHeroes[i].hero->SetModes( Heroes::MOVED ); availableHeroes.erase( availableHeroes.begin() + i ); continue; } @@ -404,7 +404,7 @@ namespace AI for ( HeroToMove & heroInfo : availableHeroes ) { if ( !heroInfo.hero->MayStillMove() ) { - heroInfo.hero->SetModes( AI::HERO_MOVED ); + heroInfo.hero->SetModes( Heroes::MOVED ); } } } diff --git a/src/fheroes2/heroes/heroes.cpp b/src/fheroes2/heroes/heroes.cpp index afb051f8bd3..f49d489b8ae 100644 --- a/src/fheroes2/heroes/heroes.cpp +++ b/src/fheroes2/heroes/heroes.cpp @@ -2096,17 +2096,10 @@ StreamBase & operator<<( StreamBase & msg, const Heroes & hero ) return msg << base << // heroes hero.name << col << hero.killer_color << hero.experience << hero.move_point_scale << hero.secondary_skills << hero.army << hero.hid << hero.portrait - << hero.race << hero.save_maps_object << hero.path << hero.direction << hero.sprite_index << hero.patrol_center << hero.patrol_square << hero.visit_object; + << hero.race << hero.save_maps_object << hero.path << hero.direction << hero.sprite_index << hero.patrol_center << hero.patrol_square << hero.visit_object + << hero._lastGroundRegion; } -enum deprecated_t -{ - AIWAITING = 0x00000002, - HUNTER = 0x00000010, - SCOUTER = 0x00000020, - STUPID = 0x00000040 -}; - StreamBase & operator>>( StreamBase & msg, Heroes & hero ) { HeroBase & base = hero; @@ -2115,6 +2108,10 @@ StreamBase & operator>>( StreamBase & msg, Heroes & hero ) msg >> base >> hero.name >> col >> hero.killer_color >> hero.experience >> hero.move_point_scale >> hero.secondary_skills >> hero.army >> hero.hid >> hero.portrait >> hero.race >> hero.save_maps_object >> hero.path >> hero.direction >> hero.sprite_index >> hero.patrol_center >> hero.patrol_square >> hero.visit_object; + if ( Game::GetLoadVersion() >= FORMAT_VERSION_091_RELEASE ) { + msg >> hero._lastGroundRegion; + } + hero.army.SetCommander( &hero ); return msg; } diff --git a/src/fheroes2/heroes/heroes.h b/src/fheroes2/heroes/heroes.h index 2fffb892f6d..19953cc28f0 100644 --- a/src/fheroes2/heroes/heroes.h +++ b/src/fheroes2/heroes/heroes.h @@ -165,7 +165,19 @@ class Heroes : public HeroBase, public ColorBase VISIONS = 0x00004000, PATROL = 0x00008000, CUSTOMARMY = 0x00010000, - CUSTOMSKILLS = 0x00020000 + CUSTOMSKILLS = 0x00020000, + SKIPPED_TURN = 0x00040000, + WAITING = 0x00080000, + MOVED = 0x00100000 + }; + + // Values are set by BitModes; shared with previous enum + enum class Role + { + SCOUT = 0x01000000, + HUNTER = 0x02000000, + COURIER = 0x04000000, + CHAMPION = 0x08000000 }; Heroes(); @@ -380,7 +392,6 @@ class Heroes : public HeroBase, public ColorBase int patrol_square; std::list visit_object; - // persist this value later uint32_t _lastGroundRegion = 0; mutable int _alphaValue; diff --git a/src/fheroes2/system/players.cpp b/src/fheroes2/system/players.cpp index e1734dd6488..94459c023f5 100644 --- a/src/fheroes2/system/players.cpp +++ b/src/fheroes2/system/players.cpp @@ -26,6 +26,7 @@ #include "game.h" #include "logging.h" #include "maps_fileinfo.h" +#include "normal/ai_normal.h" #include "players.h" #include "race.h" #include "world.h" @@ -86,6 +87,7 @@ Player::Player( int col ) , race( Race::NONE ) , friends( col ) , id( World::GetUniq() ) + , _ai( std::make_shared() ) { name = Color::String( color ); } @@ -130,6 +132,11 @@ int Player::GetID( void ) const return id; } +std::string Player::GetPersonalityString() const +{ + return _ai->GetPersonalityString(); +} + bool Player::isID( u32 id2 ) const { return id2 == id; @@ -226,14 +233,22 @@ StreamBase & operator<<( StreamBase & msg, const Player & player ) { const BitModes & modes = player; - return msg << modes << player.id << player.control << player.color << player.race << player.friends << player.name << player.focus; + assert( player._ai != nullptr ); + msg << modes << player.id << player.control << player.color << player.race << player.friends << player.name << player.focus << *player._ai; + return msg; } StreamBase & operator>>( StreamBase & msg, Player & player ) { BitModes & modes = player; - return msg >> modes >> player.id >> player.control >> player.color >> player.race >> player.friends >> player.name >> player.focus; + msg >> modes >> player.id >> player.control >> player.color >> player.race >> player.friends >> player.name >> player.focus; + if ( Game::GetLoadVersion() >= FORMAT_VERSION_091_RELEASE ) { + assert( player._ai ); + msg >> *player._ai; + } + + return msg; } Players::Players() @@ -455,11 +470,11 @@ std::string Players::String( void ) const switch ( ( *it )->GetControl() ) { case CONTROL_AI | CONTROL_HUMAN: - os << "ai|human"; + os << "ai|human, " << ( *it )->GetPersonalityString(); break; case CONTROL_AI: - os << "ai"; + os << "ai, " << ( *it )->GetPersonalityString(); break; case CONTROL_HUMAN: diff --git a/src/fheroes2/system/players.h b/src/fheroes2/system/players.h index 7084ef7ad49..46793cf7272 100644 --- a/src/fheroes2/system/players.h +++ b/src/fheroes2/system/players.h @@ -23,6 +23,7 @@ #ifndef H2PLAYERS_H #define H2PLAYERS_H +#include #include #include @@ -34,6 +35,11 @@ namespace Maps class FileInfo; } +namespace AI +{ + class Base; +} + class Castle; class Heroes; @@ -122,6 +128,7 @@ class Player : public BitModes, public Control int GetRace( void ) const; int GetFriends( void ) const; int GetID( void ) const; + std::string GetPersonalityString() const; const std::string & GetName( void ) const; Focus & GetFocus( void ); @@ -138,6 +145,7 @@ class Player : public BitModes, public Control std::string name; u32 id; Focus focus; + std::shared_ptr _ai; }; StreamBase & operator<<( StreamBase &, const Player & ); From c0e4aa59b218f8ea5cadb8f0c13d5f6611f9e821 Mon Sep 17 00:00:00 2001 From: eos428 <38433056+eos428@users.noreply.github.com> Date: Wed, 17 Feb 2021 11:19:52 +0700 Subject: [PATCH 08/84] Add days passed to Campaign SaveData and fix unsaved CampaignID (#2793) --- src/fheroes2/campaign/campaign_savedata.cpp | 18 ++++++++++++++++-- src/fheroes2/campaign/campaign_savedata.h | 7 +++++++ src/fheroes2/game/game_campaign.cpp | 4 +++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/fheroes2/campaign/campaign_savedata.cpp b/src/fheroes2/campaign/campaign_savedata.cpp index 7ef5b710b83..ff2d57cfb32 100644 --- a/src/fheroes2/campaign/campaign_savedata.cpp +++ b/src/fheroes2/campaign/campaign_savedata.cpp @@ -19,7 +19,9 @@ ***************************************************************************/ #include "campaign_savedata.h" +#include "game.h" #include "serialize.h" +#include "settings.h" #include #include @@ -30,6 +32,7 @@ namespace Campaign , _earnedCampaignAwards() , _currentScenarioID( 0 ) , _campaignID( 0 ) + , _daysPassed( 0 ) , _currentScenarioBonus() {} @@ -66,21 +69,32 @@ namespace Campaign _finishedMaps.emplace_back( _currentScenarioID ); } + void CampaignSaveData::addDaysPassed( const uint32_t days ) + { + _daysPassed += days; + } + void CampaignSaveData::reset() { _finishedMaps.clear(); _earnedCampaignAwards.clear(); _currentScenarioID = 0; _campaignID = 0; + _daysPassed = 0; } StreamBase & operator<<( StreamBase & msg, const Campaign::CampaignSaveData & data ) { - return msg << data._earnedCampaignAwards << data._currentScenarioID << data._currentScenarioBonus << data._finishedMaps; + return msg << data._earnedCampaignAwards << data._currentScenarioID << data._currentScenarioBonus << data._finishedMaps << data._campaignID << data._daysPassed; } StreamBase & operator>>( StreamBase & msg, Campaign::CampaignSaveData & data ) { - return msg >> data._earnedCampaignAwards >> data._currentScenarioID >> data._currentScenarioBonus >> data._finishedMaps; + msg >> data._earnedCampaignAwards >> data._currentScenarioID >> data._currentScenarioBonus >> data._finishedMaps; + + if ( Game::GetLoadVersion() >= FORMAT_VERSION_091_RELEASE ) + msg >> data._campaignID >> data._daysPassed; + + return msg; } } diff --git a/src/fheroes2/campaign/campaign_savedata.h b/src/fheroes2/campaign/campaign_savedata.h index 44036c3b6cf..0999ab10ff5 100644 --- a/src/fheroes2/campaign/campaign_savedata.h +++ b/src/fheroes2/campaign/campaign_savedata.h @@ -65,12 +65,18 @@ namespace Campaign return _finishedMaps.empty(); } + uint32_t getDaysPassed() const + { + return _daysPassed; + } + void setCurrentScenarioBonus( const ScenarioBonusData & bonus ); void setCurrentScenarioID( const int scenarioID ); void setCampaignID( const int campaignID ); void addCurrentMapToFinished(); void addCampaignAward( const std::string & award ); void reset(); + void addDaysPassed( const uint32_t days ); static CampaignSaveData & Get(); @@ -82,6 +88,7 @@ namespace Campaign std::vector _earnedCampaignAwards; // should have its own data format int _currentScenarioID; int _campaignID; + uint32_t _daysPassed; ScenarioBonusData _currentScenarioBonus; }; } diff --git a/src/fheroes2/game/game_campaign.cpp b/src/fheroes2/game/game_campaign.cpp index 8f7da7890a6..ab9db0f4cb4 100644 --- a/src/fheroes2/game/game_campaign.cpp +++ b/src/fheroes2/game/game_campaign.cpp @@ -425,6 +425,8 @@ int Game::CompleteCampaignScenario() Campaign::CampaignSaveData & saveData = Campaign::CampaignSaveData::Get(); saveData.addCurrentMapToFinished(); + saveData.addDaysPassed( world.CountDay() ); + const int lastCompletedScenarioID = saveData.getLastCompletedScenarioID(); const Campaign::CampaignData & campaignData = GetCampaignData( saveData.getCampaignID() ); @@ -504,7 +506,7 @@ int Game::SelectCampaignScenario() for ( uint32_t i = 0; i < bonusChoiceCount; ++i ) buttonChoices.button( i ).draw(); - Text textDaysSpent( "0", Font::BIG ); + Text textDaysSpent( std::to_string( campaignSaveData.getDaysPassed() ), Font::BIG ); textDaysSpent.Blit( top.x + 574 + textDaysSpent.w() / 2, top.y + 31 ); DrawCampaignScenarioDescription( scenario, top ); From 5d0ea5707b713afc8aa96584b9551e45f9567485 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Wed, 17 Feb 2021 12:20:57 +0800 Subject: [PATCH 09/84] Speed up apt-get step for Travis-CI and GitHub Actions (#2812) --- .github/workflows/scan-build.yml | 7 +------ .travis.yml | 29 +++++------------------------ 2 files changed, 6 insertions(+), 30 deletions(-) diff --git a/.github/workflows/scan-build.yml b/.github/workflows/scan-build.yml index dd2b8767f1e..e4086b5948e 100644 --- a/.github/workflows/scan-build.yml +++ b/.github/workflows/scan-build.yml @@ -16,12 +16,7 @@ jobs: - name: install SDL 2 and clang-tools run: | sudo apt-get update - sudo apt-get install -y libsdl2-dev - sudo apt-get install -y libsdl2-ttf-dev - sudo apt-get install -y libsdl2-mixer-dev - sudo apt-get install -y libsdl2-image-dev - sudo apt-get install -y gettext - sudo apt-get install -y clang-tools + sudo apt-get install -y libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev libsdl2-image-dev gettext clang-tools - name: compile run: scan-build -v make -j 2 env: diff --git a/.travis.yml b/.travis.yml index 2e36e39276b..d0b9e036d18 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,10 +21,7 @@ matrix: name: "SonarCloud analysis" before_install: - sudo apt-get update - - sudo apt-get install -y libsdl2-dev - - sudo apt-get install -y libsdl2-ttf-dev - - sudo apt-get install -y libsdl2-mixer-dev - - sudo apt-get install -y libsdl2-image-dev + - sudo apt-get install -y libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev libsdl2-image-dev - export FHEROES2_STRICT_COMPILATION="ON" script: # Wraps the compilation with the Build Wrapper to generate configuration (used @@ -37,10 +34,7 @@ matrix: name: "Ubuntu (Linux) with SDL 1.2" before_install: - sudo apt-get update - - sudo apt-get install -y libsdl1.2-dev - - sudo apt-get install -y libsdl-ttf2.0-dev - - sudo apt-get install -y libsdl-mixer1.2-dev - - sudo apt-get install -y libsdl-image1.2-dev + - sudo apt-get install -y libsdl1.2-dev libsdl-ttf2.0-dev libsdl-mixer1.2-dev libsdl-image1.2-dev - export FHEROES2_SDL1="ON" FHEROES2_STRICT_COMPILATION="ON" before_deploy: - cp doc/README.txt . @@ -62,10 +56,7 @@ matrix: name: "Ubuntu (Linux) with SDL 2.0" before_install: - sudo apt-get update - - sudo apt-get install -y libsdl2-dev - - sudo apt-get install -y libsdl2-ttf-dev - - sudo apt-get install -y libsdl2-mixer-dev - - sudo apt-get install -y libsdl2-image-dev + - sudo apt-get install -y libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev libsdl2-image-dev - export FHEROES2_STRICT_COMPILATION="ON" before_deploy: - cp doc/README.txt . @@ -88,12 +79,7 @@ matrix: name: "Ubuntu (Linux) ARM with SDL 1.2" before_install: - sudo apt-get update - - sudo apt-get install -y libsdl1.2-dev - - sudo apt-get install -y libsdl-ttf2.0-dev - - sudo apt-get install -y libsdl-mixer1.2-dev - - sudo apt-get install -y libsdl-image1.2-dev - - sudo apt-get install -y gettext - - sudo apt-get install -y zip + - sudo apt-get install -y libsdl1.2-dev libsdl-ttf2.0-dev libsdl-mixer1.2-dev libsdl-image1.2-dev gettext zip - export FHEROES2_SDL1="ON" FHEROES2_STRICT_COMPILATION="ON" before_deploy: - cp doc/README.txt . @@ -116,12 +102,7 @@ matrix: name: "Ubuntu (Linux) ARM with SDL 2.0" before_install: - sudo apt-get update - - sudo apt-get install -y libsdl2-dev - - sudo apt-get install -y libsdl2-ttf-dev - - sudo apt-get install -y libsdl2-mixer-dev - - sudo apt-get install -y libsdl2-image-dev - - sudo apt-get install -y gettext - - sudo apt-get install -y zip + - sudo apt-get install -y libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev libsdl2-image-dev gettext zip - export FHEROES2_STRICT_COMPILATION="ON" before_deploy: - cp doc/README.txt . From b123a9f3fa543516b16eb34778ff2b72accc7e62 Mon Sep 17 00:00:00 2001 From: Taras <11291116+Northfear@users.noreply.github.com> Date: Fri, 19 Feb 2021 01:54:54 +0200 Subject: [PATCH 10/84] PS Vita support in endian_h2.h (#2819) --- src/engine/endian_h2.h | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/engine/endian_h2.h b/src/engine/endian_h2.h index ff870b7bf0d..922df65728d 100644 --- a/src/engine/endian_h2.h +++ b/src/engine/endian_h2.h @@ -54,6 +54,20 @@ #define be32toh( x ) OSSwapBigToHostInt32( x ) #define le32toh( x ) OSSwapLittleToHostInt32( x ) +#elif defined( FHEROES2_VITA ) +#define BIG_ENDIAN 4321 +#define LITTLE_ENDIAN 1234 +#define BYTE_ORDER LITTLE_ENDIAN + +#define htobe16( x ) __builtin_bswap16( x ) +#define htole16( x ) ( x ) +#define be16toh( x ) __builtin_bswap16( x ) +#define le16toh( x ) ( x ) +#define htobe32( x ) __builtin_bswap32( x ) +#define htole32( x ) ( x ) +#define be32toh( x ) __builtin_bswap32( x ) +#define le32toh( x ) ( x ) + #elif defined( __SWITCH__ ) #include #define LITTLE_ENDIAN _LITTLE_ENDIAN From 2781fe331261ec73aae81f6ac2f0b8ab9b48e779 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Sat, 20 Feb 2021 16:48:15 +0800 Subject: [PATCH 11/84] Speed up file reading (#2827) and reduce memory usage --- src/engine/agg_file.cpp | 29 ++++++++++------------------- src/engine/agg_file.h | 5 +---- src/engine/serialize.cpp | 18 +++++++++++------- src/fheroes2/agg/agg.cpp | 4 ++-- 4 files changed, 24 insertions(+), 32 deletions(-) diff --git a/src/engine/agg_file.cpp b/src/engine/agg_file.cpp index de91decc451..e0c4d0cfae8 100644 --- a/src/engine/agg_file.cpp +++ b/src/engine/agg_file.cpp @@ -53,7 +53,7 @@ namespace fheroes2 fileEntries.getLE32(); // skip CRC (?) part const uint32_t fileOffset = fileEntries.getLE32(); const uint32_t fileSize = fileEntries.getLE32(); - _files[name] = std::make_pair( fileSize, fileOffset ); + _files.emplace( name, std::make_pair( fileSize, fileOffset ) ); } if ( _files.size() != count ) { _files.clear(); @@ -62,27 +62,18 @@ namespace fheroes2 return !_stream.fail(); } - const std::vector & AGGFile::read( const std::string & fileName ) + std::vector AGGFile::read( const std::string & fileName ) { - if ( _key != fileName ) { - auto it = _files.find( fileName ); - if ( it != _files.end() ) { - _key = fileName; - const auto & fileParams = it->second; - if ( fileParams.first > 0 ) { - _stream.seek( fileParams.second ); - _body = _stream.getRaw( fileParams.first ); - } - else { - _body.clear(); - } - } - else { - _key.clear(); - _body.clear(); + auto it = _files.find( fileName ); + if ( it != _files.end() ) { + const auto & fileParams = it->second; + if ( fileParams.first > 0 ) { + _stream.seek( fileParams.second ); + return _stream.getRaw( fileParams.first ); } } - return _body; + + return std::vector(); } } diff --git a/src/engine/agg_file.h b/src/engine/agg_file.h index aab02582623..58dc4aa6f96 100644 --- a/src/engine/agg_file.h +++ b/src/engine/agg_file.h @@ -36,16 +36,13 @@ namespace fheroes2 bool isGood() const; bool open( const std::string & fileName ); - const std::vector & read( const std::string & fileName ); + std::vector read( const std::string & fileName ); private: static const size_t _maxFilenameSize = 15; // 8.3 ASCIIZ file name + 2-bytes padding StreamFile _stream; std::map > _files; - - std::string _key; - std::vector _body; }; struct ICNHeader diff --git a/src/engine/serialize.cpp b/src/engine/serialize.cpp index d32f5ff3b82..0ae2e297d04 100644 --- a/src/engine/serialize.cpp +++ b/src/engine/serialize.cpp @@ -668,14 +668,18 @@ void StreamFile::putLE32( uint32_t val ) std::vector StreamFile::getRaw( size_t sz ) { - std::vector v( sz ? sz : sizeg(), 0 ); - if ( _file && !v.empty() ) { - const size_t count = std::fread( &v[0], v.size(), 1, _file ); - if ( count != 1 ) { - setfail( true ); - v.clear(); - } + const size_t chunkSize = sz > 0 ? sz : sizeg(); + if ( chunkSize == 0 || !_file ) { + return std::vector(); } + + std::vector v( sz ? sz : sizeg() ); + const size_t count = std::fread( &v[0], chunkSize, 1, _file ); + if ( count != 1 ) { + setfail( true ); + v.clear(); + } + return v; } diff --git a/src/fheroes2/agg/agg.cpp b/src/fheroes2/agg/agg.cpp index 879041e55d3..bd6ae77c84d 100644 --- a/src/fheroes2/agg/agg.cpp +++ b/src/fheroes2/agg/agg.cpp @@ -101,7 +101,7 @@ namespace AGG void LoadFNT( void ); bool ReadDataDir( void ); - const std::vector & ReadChunk( const std::string & key, bool ignoreExpansion = false ); + std::vector ReadChunk( const std::string & key, bool ignoreExpansion = false ); /* return letter sprite */ // Surface GetUnicodeLetter( u32 ch, u32 ft ) @@ -158,7 +158,7 @@ bool AGG::ReadDataDir( void ) return heroes2_agg.isGood(); } -const std::vector & AGG::ReadChunk( const std::string & key, bool ignoreExpansion ) +std::vector AGG::ReadChunk( const std::string & key, bool ignoreExpansion ) { if ( !ignoreExpansion && heroes2x_agg.isGood() ) { const std::vector & buf = heroes2x_agg.read( key ); From 809cf02a9cbea1a65c7300259d13c02a2c2d7101 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Sat, 20 Feb 2021 23:02:33 +0800 Subject: [PATCH 12/84] Add resizable window support on SDL2 (#2829) --- src/engine/screen.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/engine/screen.cpp b/src/engine/screen.cpp index 9895483e759..a0e0bfa9082 100644 --- a/src/engine/screen.cpp +++ b/src/engine/screen.cpp @@ -496,6 +496,9 @@ namespace flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; #endif } + else { + flags |= SDL_WINDOW_RESIZABLE; + } _window = SDL_CreateWindow( "", _prevWindowPos.x, _prevWindowPos.y, width_, height_, flags ); if ( _window == NULL ) { From 01ed21f079576a6746e274cabded2dd6a566bc2f Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Sat, 20 Feb 2021 20:51:34 -0500 Subject: [PATCH 13/84] Refactor battle AI code to prepare for future changes (#2806) --- src/fheroes2/ai/normal/ai_normal.h | 40 +- src/fheroes2/ai/normal/ai_normal_battle.cpp | 708 +++++++++++--------- 2 files changed, 415 insertions(+), 333 deletions(-) diff --git a/src/fheroes2/ai/normal/ai_normal.h b/src/fheroes2/ai/normal/ai_normal.h index 9244cf20fc5..a19d6083315 100644 --- a/src/fheroes2/ai/normal/ai_normal.h +++ b/src/fheroes2/ai/normal/ai_normal.h @@ -36,6 +36,43 @@ namespace AI std::vector validObjects; }; + struct BattleTargetPair + { + int cell = -1; + const Battle::Unit * unit = nullptr; + }; + + class BattlePlanner + { + public: + Battle::Actions planUnitTurn( Battle::Arena & arena, const Battle::Unit & currentUnit ); + void analyzeBattleState( Battle::Arena & arena, const Battle::Unit & currentUnit ); + + // decision-making helpers + bool isUnitFaster( const Battle::Unit & currentUnit, const Battle::Unit & target ) const; + bool isHeroWorthSaving( const Heroes * hero ) const; + bool isCommanderCanSpellcast( const Battle::Arena & arena, const HeroBase * commander ) const; + bool checkRetreatCondition( double myArmy, double enemy ) const; + + private: + // to be exposed later once every BattlePlanner will be re-initialized at combat start + Battle::Actions berserkTurn( Battle::Arena & arena, const Battle::Unit & currentUnit ); + Battle::Actions archerDecision( Battle::Arena & arena, const Battle::Unit & currentUnit ); + BattleTargetPair meleeUnitOffense( Battle::Arena & arena, const Battle::Unit & currentUnit ); + BattleTargetPair meleeUnitDefense( Battle::Arena & arena, const Battle::Unit & currentUnit ); + Battle::Actions forceSpellcastBeforeRetreat( Battle::Arena & arena, const HeroBase * commander ); + + // turn variables that wouldn't persist + int _myColor = Color::NONE; + double _myArmyStrength = 0; + double _enemyArmyStrength = 0; + double _myShooterStr = 0; + double _enemyShooterStr = 0; + int _highestDamageExpected = 0; + bool _attackingCastle = false; + bool _defendingCastle = false; + }; + class Normal : public Base { public: @@ -60,8 +97,7 @@ namespace AI std::vector _mapObjects; std::vector _regions; AIWorldPathfinder _pathfinder; - - void berserkTurn( Battle::Arena & arena, const Battle::Unit & currentUnit, Battle::Actions & actions ); + BattlePlanner _battlePlanner; }; } diff --git a/src/fheroes2/ai/normal/ai_normal_battle.cpp b/src/fheroes2/ai/normal/ai_normal_battle.cpp index eb73bfcf85b..69a3f8035d2 100644 --- a/src/fheroes2/ai/normal/ai_normal_battle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_battle.cpp @@ -43,71 +43,6 @@ namespace AI const double STRENGTH_DISTANCE_FACTOR = 5.0; const std::vector underWallsIndicies = {7, 28, 49, 72, 95}; - bool isHeroWorthSaving( const Heroes * hero ) - { - return hero && ( hero->GetLevel() > 2 || !hero->GetBagArtifacts().empty() ); - } - - bool CheckCommanderCanSpellcast( const Arena & arena, const HeroBase * commander ) - { - return commander && ( !commander->isControlHuman() || Settings::Get().BattleAutoSpellcast() ) && commander->HaveSpellBook() - && !commander->Modes( Heroes::SPELLCASTED ) && !arena.isSpellcastDisabled(); - } - - bool CheckBattleRetreat( double myArmy, double enemy ) - { - // FIXME: more sophisticated logic to see if remaining units are under threat - // Consider taking speed/turn order into account as well - // Pass in ( const Units & friendly, const Units & enemies ) instead - - // Retreat if remaining army strength is 10% of enemy's army - return myArmy * 10 < enemy; - } - - bool CheckIfUnitIsFaster( const Unit & currentUnit, const Unit & target ) - { - if ( currentUnit.isFlying() == target.isFlying() ) - return currentUnit.GetSpeed() > target.GetSpeed(); - return currentUnit.isFlying(); - } - - void ForceSpellcastBeforeRetreat( Arena & arena, const HeroBase * commander, Actions & actions ) - { - if ( CheckCommanderCanSpellcast( arena, commander ) ) { - const std::vector allSpells = commander->GetSpells(); - int bestSpell = -1; - double bestHeuristic = 0; - int targetIdx = -1; - - const Units friendly( arena.GetForce( commander->GetColor() ), true ); - const Units enemies( arena.GetForce( commander->GetColor(), true ), true ); - - const int spellPower = commander->GetPower(); - for ( const Spell & spell : allSpells ) { - if ( !commander->HaveSpellPoints( spell ) ) - continue; - - if ( spell.isCombat() && spell.isDamage() && spell.isSingleTarget() ) { - const uint32_t totalDamage = spell.Damage() * spellPower; - for ( const Unit * enemy : enemies ) { - const double spellHeuristic - = enemy->GetMonsterStrength() * enemy->HowManyWillKilled( totalDamage * ( 100 - enemy->GetMagicResist( spell, spellPower ) ) / 100 ); - - if ( spellHeuristic > bestHeuristic ) { - bestHeuristic = spellHeuristic; - bestSpell = spell.GetID(); - targetIdx = enemy->GetHeadIndex(); - } - } - } - } - - if ( bestSpell != -1 ) { - actions.push_back( Battle::Command( MSG_BATTLE_CAST, bestSpell, targetIdx ) ); - } - } - } - void Normal::HeroesPreBattle( HeroBase & hero ) { // Optimize troops placement before the battle @@ -152,7 +87,6 @@ namespace AI slotOrder = {0, 2, 1, 3, 4}; break; case 2: - // 1, 5 or 4 -> 3, 2, 5 slotOrder = {0, 4, 2, 1, 3}; break; case 3: @@ -185,73 +119,218 @@ namespace AI } } - void Normal::BattleTurn( Arena & arena, const Unit & currentUnit, Actions & actions ) + bool BattlePlanner::isHeroWorthSaving( const Heroes * hero ) const + { + return hero && ( hero->GetLevel() > 2 || !hero->GetBagArtifacts().empty() ); + } + + bool BattlePlanner::isCommanderCanSpellcast( const Arena & arena, const HeroBase * commander ) const + { + return commander && ( !commander->isControlHuman() || Settings::Get().BattleAutoSpellcast() ) && commander->HaveSpellBook() + && !commander->Modes( Heroes::SPELLCASTED ) && !arena.isSpellcastDisabled(); + } + + bool BattlePlanner::checkRetreatCondition( double myArmy, double enemy ) const + { + // FIXME: more sophisticated logic to see if remaining units are under threat + // Consider taking speed/turn order into account as well + // Pass in ( const Units & friendly, const Units & enemies ) instead + + // Retreat if remaining army strength is 10% of enemy's army + return myArmy * 10 < enemy; + } + + bool BattlePlanner::isUnitFaster( const Unit & currentUnit, const Unit & target ) const + { + if ( currentUnit.isFlying() == target.isFlying() ) + return currentUnit.GetSpeed() > target.GetSpeed(); + return currentUnit.isFlying(); + } + + Actions BattlePlanner::forceSpellcastBeforeRetreat( Arena & arena, const HeroBase * commander ) + { + Actions result; + if ( !isCommanderCanSpellcast( arena, commander ) ) { + return result; + } + + const std::vector allSpells = commander->GetSpells(); + int bestSpell = -1; + double bestHeuristic = 0; + int targetIdx = -1; + + const Units friendly( arena.GetForce( commander->GetColor() ), true ); + const Units enemies( arena.GetForce( commander->GetColor(), true ), true ); + + const int spellPower = commander->GetPower(); + for ( const Spell & spell : allSpells ) { + if ( !commander->HaveSpellPoints( spell ) ) + continue; + + // TODO: add mass spells + if ( spell.isCombat() && spell.isDamage() && spell.isSingleTarget() ) { + const uint32_t totalDamage = spell.Damage() * spellPower; + for ( const Unit * enemy : enemies ) { + const double spellHeuristic + = enemy->GetMonsterStrength() * enemy->HowManyWillKilled( totalDamage * ( 100 - enemy->GetMagicResist( spell, spellPower ) ) / 100 ); + + if ( spellHeuristic > bestHeuristic ) { + bestHeuristic = spellHeuristic; + bestSpell = spell.GetID(); + targetIdx = enemy->GetHeadIndex(); + } + } + } + } + + if ( bestSpell != -1 ) { + result.emplace_back( MSG_BATTLE_CAST, bestSpell, targetIdx ); + } + return result; + } + + Actions BattlePlanner::planUnitTurn( Arena & arena, const Unit & currentUnit ) { if ( currentUnit.Modes( SP_BERSERKER ) != 0 ) { - berserkTurn( arena, currentUnit, actions ); - return; + return berserkTurn( arena, currentUnit ); } - const int myColor = currentUnit.GetColor(); - const int myHeadIndex = currentUnit.GetHeadIndex(); - const uint32_t currentUnitMoveRange = currentUnit.isFlying() ? MAXU16 : currentUnit.GetSpeed(); + Actions actions; - DEBUG_LOG( DBG_BATTLE, DBG_TRACE, currentUnit.GetName() << " start their turn. Side: " << myColor ); + // Step 1. Analyze current battle state and update variables + analyzeBattleState( arena, currentUnit ); + const Force & enemyForce = arena.GetForce( _myColor, true ); const HeroBase * commander = currentUnit.GetCommander(); - const Force & friendlyForce = arena.GetForce( myColor ); - const Force & enemyForce = arena.GetForce( myColor, true ); + + DEBUG_LOG( DBG_BATTLE, DBG_TRACE, currentUnit.GetName() << " start their turn. Side: " << _myColor ); + + // When we have in 10 times stronger army than the enemy we could consider it as an overpowered and we most likely will win. + const bool myOverpoweredArmy = _myArmyStrength > _enemyArmyStrength * 10; + const double enemyArcherRatio = _enemyShooterStr / _enemyArmyStrength; + + const bool defensiveTactics = enemyArcherRatio < 0.75 && ( _defendingCastle || _myShooterStr > _enemyShooterStr ) && !myOverpoweredArmy; + DEBUG_LOG( DBG_BATTLE, DBG_TRACE, + "Tactic " << defensiveTactics << " chosen. Archers: " << _myShooterStr << ", vs enemy " << _enemyShooterStr << " ratio is " << enemyArcherRatio ); + + // Step 2. Check retreat/surrender condition + const Heroes * actualHero = dynamic_cast( commander ); + if ( actualHero && arena.CanRetreatOpponent( _myColor ) && isHeroWorthSaving( actualHero ) && checkRetreatCondition( _myArmyStrength, _enemyArmyStrength ) ) { + // Cast maximum damage spell + actions = forceSpellcastBeforeRetreat( arena, commander ); + + actions.emplace_back( MSG_BATTLE_RETREAT ); + actions.emplace_back( MSG_BATTLE_END_TURN, currentUnit.GetUID() ); + return actions; + } + + // Step 3. Calculate spell heuristics + + // Hero should conserve spellpoints if fighting against monsters or AI and has advantage + if ( !( myOverpoweredArmy && enemyForce.GetControl() == CONTROL_AI ) && isCommanderCanSpellcast( arena, commander ) ) { + // 1. For damage spells - maximum amount of enemy threat lost + // 2. For buffs - friendly unit strength gained + // 3. For debuffs - enemy unit threat lost + // 4. For dispell, resurrect and cure - amount of unit strength recovered + // 5. For antimagic - based on enemy hero spellcasting abilities multiplied by friendly unit strength + + // 6. Cast best spell with highest heuristic on target pointer saved + + // Temporary: force damage spell + actions = forceSpellcastBeforeRetreat( arena, commander ); + } + + // Step 4. Current unit decision tree + const size_t actionsSize = actions.size(); + + if ( currentUnit.isArchers() ) { + const Actions & archerActions = archerDecision( arena, currentUnit ); + actions.insert( actions.end(), archerActions.begin(), archerActions.end() ); + } + else { + // Melee unit decision tree (both flyers and walkers) + Board & board = *Battle::Arena::GetBoard(); + board.SetPositionQuality( currentUnit ); + + // Determine unit target/cell to move + BattleTargetPair target; + + if ( defensiveTactics ) { + target = meleeUnitDefense( arena, currentUnit ); + } + else { + target = meleeUnitOffense( arena, currentUnit ); + } + + // Melee unit final stage - add actions to the queue + DEBUG_LOG( DBG_BATTLE, DBG_INFO, "Melee phase end, targetCell is " << target.cell ); + + if ( target.cell != -1 ) { + if ( currentUnit.GetHeadIndex() != target.cell ) + actions.emplace_back( MSG_BATTLE_MOVE, currentUnit.GetUID(), target.cell ); + + if ( target.unit ) { + actions.emplace_back( MSG_BATTLE_ATTACK, currentUnit.GetUID(), target.unit->GetUID(), target.unit->GetHeadIndex(), 0 ); + DEBUG_LOG( DBG_BATTLE, DBG_INFO, + currentUnit.GetName() << " melee offense, focus enemy " << target.unit->GetName() + << " threat level: " << target.unit->GetScoreQuality( currentUnit ) ); + } + } + // else skip + } + + // no action was taken - skip + if ( actions.size() == actionsSize ) { + actions.emplace_back( MSG_BATTLE_SKIP, currentUnit.GetUID(), true ); + } + + return actions; + } + + void BattlePlanner::analyzeBattleState( Arena & arena, const Unit & currentUnit ) + { + _myColor = currentUnit.GetColor(); + const Force & friendlyForce = arena.GetForce( _myColor ); + const Force & enemyForce = arena.GetForce( _myColor, true ); // This should filter out all invalid units const Units friendly( friendlyForce, true ); const Units enemies( enemyForce, true ); - // const size_t enemiesCount = enemies.size(); - // Step 1. Friendly and enemy army analysis - double myArmyStrength = 0; - double enemyArmyStrength = 0; - double myShooterStr = 0; - double enemyShooterStr = 0; - // double averageAllyDefense = 0; - // double averageEnemyAttack = 0; - int highestDamageExpected = 0; + // Friendly and enemy army analysis + _myArmyStrength = 0; + _enemyArmyStrength = 0; + _myShooterStr = 0; + _enemyShooterStr = 0; + _highestDamageExpected = 0; for ( Units::const_iterator it = enemies.begin(); it != enemies.end(); ++it ) { const Unit & unit = **it; const double unitStr = unit.GetStrength(); - enemyArmyStrength += unitStr; + _enemyArmyStrength += unitStr; if ( unit.isArchers() ) { - enemyShooterStr += unitStr; + _enemyShooterStr += unitStr; } const int dmg = unit.CalculateMaxDamage( currentUnit ); - if ( dmg > highestDamageExpected ) - highestDamageExpected = dmg; - - // averageEnemyAttack += unit.GetAttack(); + if ( dmg > _highestDamageExpected ) + _highestDamageExpected = dmg; } for ( Units::const_iterator it = friendly.begin(); it != friendly.end(); ++it ) { const Unit & unit = **it; const double unitStr = unit.GetStrength(); - myArmyStrength += unitStr; + _myArmyStrength += unitStr; if ( unit.isArchers() ) { - myShooterStr += unitStr; + _myShooterStr += unitStr; } - - // averageAllyDefense += unit.GetDefense(); } - const double enemyArcherRatio = enemyShooterStr / enemyArmyStrength; - // Will be used for better unit strength heuristic - // averageAllyDefense = ( enemiesCount > 0 ) ? averageAllyDefense / enemiesCount : 1; - // averageEnemyAttack = ( enemiesCount > 0 ) ? averageEnemyAttack / enemiesCount : 1; - - // Step 2. Add castle siege (and battle arena) modifiers - bool attackingCastle = false; - bool defendingCastle = false; + // Add castle siege (and battle arena) modifiers + _attackingCastle = false; + _defendingCastle = false; const Castle * castle = Arena::GetCastle(); if ( castle ) { const bool attackerIgnoresCover = arena.GetForce1().GetCommander()->HasArtifact( Artifact::GOLDEN_BOW ); @@ -263,291 +342,249 @@ namespace AI towerStr += getTowerStrength( Arena::GetTower( TWR_RIGHT ) ); DEBUG_LOG( DBG_BATTLE, DBG_TRACE, "- Castle strength: " << towerStr ); - if ( myColor == castle->GetColor() ) { - defendingCastle = true; - myShooterStr += towerStr; + if ( _myColor == castle->GetColor() ) { + _defendingCastle = true; + _myShooterStr += towerStr; if ( !attackerIgnoresCover ) - enemyShooterStr /= 2; + _enemyShooterStr /= 2; } else { - attackingCastle = true; - enemyShooterStr += towerStr; + _attackingCastle = true; + _enemyShooterStr += towerStr; if ( !attackerIgnoresCover ) - myShooterStr /= 2; + _myShooterStr /= 2; } } + } - // When we have in 10 times stronger army than the enemy we could consider it as an overpowered and we most likely will win. - const bool myOverpoweredArmy = myArmyStrength > enemyArmyStrength * 10; - - const bool defensiveTactics = enemyArcherRatio < 0.75 && ( defendingCastle || myShooterStr > enemyShooterStr ) && !myOverpoweredArmy; - DEBUG_LOG( DBG_BATTLE, DBG_TRACE, - "Tactic " << defensiveTactics << " chosen. Archers: " << myShooterStr << ", vs enemy " << enemyShooterStr << " ratio is " << enemyArcherRatio ); - - const double attackDistanceModifier = enemyArmyStrength / STRENGTH_DISTANCE_FACTOR; - const double defenceDistanceModifier = myArmyStrength / STRENGTH_DISTANCE_FACTOR; + Actions BattlePlanner::archerDecision( Arena & arena, const Unit & currentUnit ) + { + Actions actions; + const Units enemies( arena.GetForce( _myColor, true ), true ); + const Unit * target = NULL; + int targetCell = -1; - // Step 3. Check retreat/surrender condition - const Heroes * actualHero = dynamic_cast( commander ); - if ( actualHero && arena.CanRetreatOpponent( myColor ) && isHeroWorthSaving( actualHero ) && CheckBattleRetreat( myArmyStrength, enemyArmyStrength ) ) { - // Cast maximum damage spell - ForceSpellcastBeforeRetreat( arena, commander, actions ); + if ( currentUnit.isHandFighting() ) { + // Current ranged unit is blocked by the enemy - actions.push_back( Command( MSG_BATTLE_RETREAT ) ); - actions.push_back( Command( MSG_BATTLE_END_TURN, currentUnit.GetUID() ) ); - return; - } + // force archer to fight back by setting initial expectation to lowest possible (if we're losing battle) + int bestOutcome = ( _myArmyStrength < _enemyArmyStrength ) ? -_highestDamageExpected : 0; + bool canOutrunEnemy = true; - // Step 4. Calculate spell heuristics + const Indexes & adjacentEnemies = Board::GetAdjacentEnemies( currentUnit ); + for ( const int cell : adjacentEnemies ) { + const Unit * enemy = Board::GetCell( cell )->GetUnit(); + if ( enemy ) { + const int archerMeleeDmg = currentUnit.GetDamage( *enemy ); + const int damageDiff = archerMeleeDmg - enemy->CalculateRetaliationDamage( archerMeleeDmg ); - // Hero should conserve spellpoints if fighting against monsters or AI and has advantage - if ( !( myOverpoweredArmy && enemyForce.GetControl() == CONTROL_AI ) && CheckCommanderCanSpellcast( arena, commander ) ) { - // 1. For damage spells - maximum amount of enemy threat lost - // 2. For buffs - friendly unit strength gained - // 3. For debuffs - enemy unit threat lost - // 4. For dispell, resurrect and cure - amount of unit strength recovered - // 5. For antimagic - based on enemy hero spellcasting abilities multiplied by friendly unit strength + if ( bestOutcome < damageDiff ) { + bestOutcome = damageDiff; + target = enemy; + targetCell = cell; + } - // 6. Cast best spell with highest heuristic on target pointer saved + // try to determine if it's worth running away (canOutrunEnemy stays true ONLY if all enemies are slower) + if ( canOutrunEnemy && !isUnitFaster( currentUnit, *enemy ) ) + canOutrunEnemy = false; + } + else { + DEBUG_LOG( DBG_BATTLE, DBG_WARN, "Board::GetAdjacentEnemies returned a cell " << cell << " that does not contain a unit!" ); + } + } - // Temporary: force damage spell - ForceSpellcastBeforeRetreat( arena, commander, actions ); + if ( target && targetCell != -1 ) { + // attack selected target + DEBUG_LOG( DBG_BATTLE, DBG_INFO, currentUnit.GetName() << " archer deciding to fight back: " << bestOutcome ); + actions.emplace_back( MSG_BATTLE_ATTACK, currentUnit.GetUID(), target->GetUID(), targetCell, 0 ); + } + else if ( canOutrunEnemy ) { + // Kiting enemy + // Search for a safe spot unit can move away + DEBUG_LOG( DBG_BATTLE, DBG_INFO, currentUnit.GetName() << " archer kiting enemy" ); + } + // Worst case scenario - Skip turn } + else { + // Normal ranged attack: focus the highest value unit + double highestStrength = 0; - // Step 5. Current unit decision tree - const size_t actionsSize = actions.size(); - const Unit * target = NULL; - int targetCell = -1; + for ( const Unit * enemy : enemies ) { + const double attackPriority = enemy->GetScoreQuality( currentUnit ); - if ( currentUnit.isArchers() ) { - // Ranged unit decision tree - if ( currentUnit.isHandFighting() ) { - // Current ranged unit is blocked by the enemy + if ( highestStrength < attackPriority ) { + highestStrength = attackPriority; + target = enemy; + DEBUG_LOG( DBG_BATTLE, DBG_TRACE, "- Set priority on " << enemy->GetName() << " value " << attackPriority ); + } + } - // force archer to fight back by setting initial expectation to lowest possible (if we're losing battle) - int bestOutcome = ( myArmyStrength < enemyArmyStrength ) ? -highestDamageExpected : 0; - bool canOutrunEnemy = true; + if ( target ) { + actions.emplace_back( MSG_BATTLE_ATTACK, currentUnit.GetUID(), target->GetUID(), target->GetHeadIndex(), 0 ); - const Indexes & adjacentEnemies = Board::GetAdjacentEnemies( currentUnit ); - for ( const int cell : adjacentEnemies ) { - const Unit * enemy = Board::GetCell( cell )->GetUnit(); - if ( enemy ) { - const int archerMeleeDmg = currentUnit.GetDamage( *enemy ); - const int damageDiff = archerMeleeDmg - enemy->CalculateRetaliationDamage( archerMeleeDmg ); + DEBUG_LOG( DBG_BATTLE, DBG_INFO, + currentUnit.GetName() << " archer focusing enemy " << target->GetName() << " threat level: " << target->GetScoreQuality( currentUnit ) ); + } + } - if ( bestOutcome < damageDiff ) { - bestOutcome = damageDiff; - target = enemy; - targetCell = cell; - } + return actions; + } - // try to determine if it's worth running away (canOutrunEnemy stays true ONLY if all enemies are slower) - if ( canOutrunEnemy && !CheckIfUnitIsFaster( currentUnit, *enemy ) ) - canOutrunEnemy = false; - } - else { - DEBUG_LOG( DBG_BATTLE, DBG_WARN, "Board::GetAdjacentEnemies returned a cell " << cell << " that does not contain a unit!" ); - } - } + BattleTargetPair BattlePlanner::meleeUnitOffense( Arena & arena, const Unit & currentUnit ) + { + BattleTargetPair target; + const Units enemies( arena.GetForce( _myColor, true ), true ); - if ( target && targetCell != -1 ) { - // attack selected target - DEBUG_LOG( DBG_BATTLE, DBG_INFO, currentUnit.GetName() << " archer deciding to fight back: " << bestOutcome ); - actions.push_back( Battle::Command( MSG_BATTLE_ATTACK, currentUnit.GetUID(), target->GetUID(), targetCell, 0 ) ); - } - else if ( canOutrunEnemy ) { - // Kiting enemy - // Search for a safe spot unit can move away - DEBUG_LOG( DBG_BATTLE, DBG_INFO, currentUnit.GetName() << " archer kiting enemy" ); + const uint32_t currentUnitMoveRange = currentUnit.isFlying() ? MAXU16 : currentUnit.GetSpeed(); + const double attackDistanceModifier = _enemyArmyStrength / STRENGTH_DISTANCE_FACTOR; + + double maxPriority = attackDistanceModifier * ARENASIZE * -1; + int highestValue = 0; + + for ( const Unit * enemy : enemies ) { + const Indexes & around = Board::GetAroundIndexes( *enemy ); + for ( const int cell : around ) { + const int quality = Board::GetCell( cell )->GetQuality(); + const uint32_t dist = arena.CalculateMoveDistance( cell ); + if ( arena.hexIsPassable( cell ) && dist <= currentUnitMoveRange && highestValue < quality ) { + highestValue = quality; + target.unit = enemy; + target.cell = cell; + break; } - // Worst case scenario - Skip turn } - else { - // Normal ranged attack: focus the highest value unit - double highestStrength = 0; - for ( const Unit * enemy : enemies ) { - const double attackPriority = enemy->GetScoreQuality( currentUnit ); + // For walking units that don't have a target within reach, pick based on distance priority + if ( target.unit == nullptr ) { + // move node pair consists of move hex index and distance + const std::pair move = arena.CalculateMoveToUnit( *enemy ); - if ( highestStrength < attackPriority ) { - highestStrength = attackPriority; - target = enemy; - DEBUG_LOG( DBG_BATTLE, DBG_TRACE, "- Set priority on " << enemy->GetName() << " value " << attackPriority ); - } + const double unitPriority = enemy->GetScoreQuality( currentUnit ) - move.second * attackDistanceModifier; + if ( unitPriority > maxPriority ) { + maxPriority = unitPriority; + target.cell = move.first; } + } + } - if ( target ) { - actions.push_back( Battle::Command( MSG_BATTLE_ATTACK, currentUnit.GetUID(), target->GetUID(), target->GetHeadIndex(), 0 ) ); + // Walkers: move closer to the castle walls during siege + if ( _attackingCastle && target.cell == -1 ) { + uint32_t shortestDist = MAXU16; - DEBUG_LOG( DBG_BATTLE, DBG_INFO, - currentUnit.GetName() << " archer focusing enemy " << target->GetName() << " threat level: " << target->GetScoreQuality( currentUnit ) ); + for ( const int wallIndex : underWallsIndicies ) { + const uint32_t dist = arena.CalculateMoveDistance( wallIndex ); + if ( dist < shortestDist ) { + shortestDist = dist; + target.cell = wallIndex; } } + DEBUG_LOG( DBG_BATTLE, DBG_INFO, "Walker unit moving towards castle walls " << currentUnit.GetName() << " cell " << target.cell ); } - else { - // Melee unit decision tree (both flyers and walkers) - Board & board = *Battle::Arena::GetBoard(); - board.SetPositionQuality( currentUnit ); - if ( defensiveTactics ) { - // Melee unit - Defensive action - double maxArcherValue = defenceDistanceModifier * ARENASIZE * -1; - double maxEnemyValue = attackDistanceModifier * ARENASIZE * -1; - double maxEnemyThreat = 0; + return target; + } - // 1. Check if there's a target within our half of the battlefield - for ( const Unit * enemy : enemies ) { - const std::pair move = arena.CalculateMoveToUnit( *enemy ); + BattleTargetPair BattlePlanner::meleeUnitDefense( Arena & arena, const Unit & currentUnit ) + { + BattleTargetPair target; - // Allow to move only within our half of the battlefield. If in castle make sure to stay inside. - const bool isSafeToMove = ( !defendingCastle && move.second <= ARENAW / 2 ) || ( defendingCastle && Board::isCastleIndex( move.first ) ); + const Units friendly( arena.GetForce( _myColor ), true ); + const Units enemies( arena.GetForce( _myColor, true ), true ); - if ( move.first != -1 && isSafeToMove ) { - const double enemyValue = enemy->GetStrength() - move.second * attackDistanceModifier; + const uint32_t currentUnitMoveRange = currentUnit.isFlying() ? MAXU16 : currentUnit.GetSpeed(); + const int myHeadIndex = currentUnit.GetHeadIndex(); - // Pick highest value unit if there's multiple - if ( maxEnemyValue < enemyValue ) { - maxEnemyValue = enemyValue; - target = enemy; - targetCell = move.first; - } - } - } + const double attackDistanceModifier = _enemyArmyStrength / STRENGTH_DISTANCE_FACTOR; + const double defenceDistanceModifier = _myArmyStrength / STRENGTH_DISTANCE_FACTOR; - // 2. Check if our archer units are under threat - overwrite target and protect - for ( const Unit * unitToDefend : friendly ) { - if ( unitToDefend->GetUID() != currentUnit.GetUID() && unitToDefend->isArchers() ) { - const int headIndexToDefend = unitToDefend->GetHeadIndex(); - const std::pair move = arena.CalculateMoveToUnit( *unitToDefend ); - const uint32_t distanceToUnit = ( move.first != -1 ) ? move.second : Board::GetDistance( myHeadIndex, headIndexToDefend ); - const double archerValue = unitToDefend->GetStrength() - distanceToUnit * defenceDistanceModifier; - - DEBUG_LOG( DBG_AI, DBG_TRACE, unitToDefend->GetName() << " archer value " << archerValue << " distance: " << distanceToUnit ); - - // 3. Search for enemy units blocking our archers within range move - const Indexes & adjacentEnemies = Board::GetAdjacentEnemies( *unitToDefend ); - for ( const int cell : adjacentEnemies ) { - const Unit * enemy = Board::GetCell( cell )->GetUnit(); - if ( enemy ) { - const double enemyThreat = enemy->GetScoreQuality( currentUnit ); - const std::pair moveToEnemy = arena.CalculateMoveToUnit( *enemy ); - const bool canReach = moveToEnemy.first != -1 && moveToEnemy.second <= currentUnitMoveRange; - const bool hadAnotherTarget = target != NULL; - - DEBUG_LOG( DBG_BATTLE, DBG_TRACE, " - Found enemy, cell " << cell << " threat " << enemyThreat << " distance " << moveToEnemy.second ); - - // Composite priority criteria: - // Primary - Enemy is within move range - // Secondary - Archer unit value - // Tertiary - Enemy unit threat - if ( ( canReach != hadAnotherTarget && canReach ) - || ( canReach == hadAnotherTarget - && ( maxArcherValue < archerValue - || ( std::fabs( maxArcherValue - archerValue ) < 0.001 && maxEnemyThreat < enemyThreat ) ) ) ) { - targetCell = moveToEnemy.first; - target = enemy; - maxArcherValue = archerValue; - maxEnemyThreat = enemyThreat; - DEBUG_LOG( DBG_BATTLE, DBG_TRACE, - " - Target selected " << enemy->GetName() << " cell " << targetCell << " archer value " << archerValue ); - } - } - else { - DEBUG_LOG( DBG_BATTLE, DBG_WARN, "Board::GetAdjacentEnemies returned a cell " << cell << " that does not contain a unit!" ); - } - } + double maxArcherValue = defenceDistanceModifier * ARENASIZE * -1; + double maxEnemyValue = attackDistanceModifier * ARENASIZE * -1; + double maxEnemyThreat = 0; - // 4. No enemies found anywhere - move in closer to the friendly ranged unit - if ( !target && maxArcherValue < archerValue ) { - targetCell = move.first; - maxArcherValue = archerValue; - } - } - } + // 1. Check if there's a target within our half of the battlefield + for ( const Unit * enemy : enemies ) { + const std::pair move = arena.CalculateMoveToUnit( *enemy ); - if ( target ) { - DEBUG_LOG( DBG_BATTLE, DBG_INFO, currentUnit.GetName() << " defending against " << target->GetName() << " threat level: " << maxEnemyThreat ); - } - else if ( targetCell != -1 ) { - DEBUG_LOG( DBG_BATTLE, DBG_INFO, currentUnit.GetName() << " protecting friendly archer, moving to " << targetCell ); + // Allow to move only within our half of the battlefield. If in castle make sure to stay inside. + const bool isSafeToMove = ( !_defendingCastle && move.second <= ARENAW / 2 ) || ( _defendingCastle && Board::isCastleIndex( move.first ) ); + + if ( move.first != -1 && isSafeToMove ) { + const double enemyValue = enemy->GetStrength() - move.second * attackDistanceModifier; + + // Pick highest value unit if there's multiple + if ( maxEnemyValue < enemyValue ) { + maxEnemyValue = enemyValue; + target.unit = enemy; + target.cell = move.first; } } - else { - // Melee unit - Offensive action - double maxPriority = attackDistanceModifier * ARENASIZE * -1; - int highestValue = 0; + } - for ( const Unit * enemy : enemies ) { - const Indexes & around = Board::GetAroundIndexes( *enemy ); - for ( const int cell : around ) { - const int quality = Board::GetCell( cell )->GetQuality(); - const uint32_t dist = arena.CalculateMoveDistance( cell ); - if ( arena.hexIsPassable( cell ) && dist <= currentUnitMoveRange && highestValue < quality ) { - highestValue = quality; - target = enemy; - targetCell = cell; - break; - } - } + // 2. Check if our archer units are under threat - overwrite target and protect + for ( const Unit * unitToDefend : friendly ) { + if ( unitToDefend->GetUID() != currentUnit.GetUID() && unitToDefend->isArchers() ) { + const int headIndexToDefend = unitToDefend->GetHeadIndex(); + const std::pair move = arena.CalculateMoveToUnit( *unitToDefend ); + const uint32_t distanceToUnit = ( move.first != -1 ) ? move.second : Board::GetDistance( myHeadIndex, headIndexToDefend ); + const double archerValue = unitToDefend->GetStrength() - distanceToUnit * defenceDistanceModifier; - // For walking units that don't have a target within reach, pick based on distance priority - if ( target == NULL ) { - // move node pair consists of move hex index and distance - const std::pair move = arena.CalculateMoveToUnit( *enemy ); + DEBUG_LOG( DBG_AI, DBG_TRACE, unitToDefend->GetName() << " archer value " << archerValue << " distance: " << distanceToUnit ); - const double unitPriority = enemy->GetScoreQuality( currentUnit ) - move.second * attackDistanceModifier; - if ( unitPriority > maxPriority ) { - maxPriority = unitPriority; - targetCell = move.first; + // 3. Search for enemy units blocking our archers within range move + const Indexes & adjacentEnemies = Board::GetAdjacentEnemies( *unitToDefend ); + for ( const int cell : adjacentEnemies ) { + const Unit * enemy = Board::GetCell( cell )->GetUnit(); + if ( enemy ) { + const double enemyThreat = enemy->GetScoreQuality( currentUnit ); + const std::pair moveToEnemy = arena.CalculateMoveToUnit( *enemy ); + const bool canReach = moveToEnemy.first != -1 && moveToEnemy.second <= currentUnitMoveRange; + const bool hadAnotherTarget = target.unit != NULL; + + DEBUG_LOG( DBG_BATTLE, DBG_TRACE, " - Found enemy, cell " << cell << " threat " << enemyThreat << " distance " << moveToEnemy.second ); + + // Composite priority criteria: + // Primary - Enemy is within move range + // Secondary - Archer unit value + // Tertiary - Enemy unit threat + if ( ( canReach != hadAnotherTarget && canReach ) + || ( canReach == hadAnotherTarget + && ( maxArcherValue < archerValue || ( std::fabs( maxArcherValue - archerValue ) < 0.001 && maxEnemyThreat < enemyThreat ) ) ) ) { + target.cell = moveToEnemy.first; + target.unit = enemy; + maxArcherValue = archerValue; + maxEnemyThreat = enemyThreat; + DEBUG_LOG( DBG_BATTLE, DBG_TRACE, " - Target selected " << enemy->GetName() << " cell " << target.cell << " archer value " << archerValue ); } } - } - - // Walkers: move closer to the castle walls during siege - if ( attackingCastle && targetCell == -1 ) { - uint32_t shortestDist = MAXU16; - - for ( const int wallIndex : underWallsIndicies ) { - const uint32_t dist = arena.CalculateMoveDistance( wallIndex ); - if ( dist < shortestDist ) { - shortestDist = dist; - targetCell = wallIndex; - } + else { + DEBUG_LOG( DBG_BATTLE, DBG_WARN, "Board::GetAdjacentEnemies returned a cell " << cell << " that does not contain a unit!" ); } - DEBUG_LOG( DBG_BATTLE, DBG_INFO, "Walker unit moving towards castle walls " << currentUnit.GetName() << " cell " << targetCell ); } - } - - // Melee unit final stage - action target should be determined already, add actions to the queue - DEBUG_LOG( DBG_BATTLE, DBG_INFO, "Melee phase end, targetCell is " << targetCell ); - if ( targetCell != -1 ) { - if ( myHeadIndex != targetCell ) - actions.push_back( Battle::Command( MSG_BATTLE_MOVE, currentUnit.GetUID(), targetCell ) ); - - if ( target ) { - actions.push_back( Battle::Command( MSG_BATTLE_ATTACK, currentUnit.GetUID(), target->GetUID(), target->GetHeadIndex(), 0 ) ); - DEBUG_LOG( DBG_BATTLE, DBG_INFO, - currentUnit.GetName() << " melee offense, focus enemy " << target->GetName() - << " threat level: " << target->GetScoreQuality( currentUnit ) ); + // 4. No enemies found anywhere - move in closer to the friendly ranged unit + if ( !target.unit && maxArcherValue < archerValue ) { + target.cell = move.first; + maxArcherValue = archerValue; } } - // else skip } - // no action was taken - skip - if ( actions.size() == actionsSize ) { - actions.push_back( Command( MSG_BATTLE_SKIP, currentUnit.GetUID(), true ) ); + if ( target.unit ) { + DEBUG_LOG( DBG_BATTLE, DBG_INFO, currentUnit.GetName() << " defending against " << target.unit->GetName() << " threat level: " << maxEnemyThreat ); + } + else if ( target.cell != -1 ) { + DEBUG_LOG( DBG_BATTLE, DBG_INFO, currentUnit.GetName() << " protecting friendly archer, moving to " << target.cell ); } - actions.push_back( Battle::Command( MSG_BATTLE_END_TURN, currentUnit.GetUID() ) ); + return target; } - void Normal::berserkTurn( Arena & arena, const Unit & currentUnit, Actions & actions ) + Actions BattlePlanner::berserkTurn( Arena & arena, const Unit & currentUnit ) { assert( currentUnit.Modes( SP_BERSERKER ) ); + Actions actions; - Board & board = *Battle::Arena::GetBoard(); + Board & board = *arena.GetBoard(); const uint32_t currentUnitUID = currentUnit.GetUID(); const std::vector nearestUnits = board.GetNearestTroops( ¤tUnit, std::vector() ); @@ -556,7 +593,7 @@ namespace AI const uint32_t targetUnitUID = nearestUnits[i]->GetUID(); const int32_t targetUnitHead = nearestUnits[i]->GetHeadIndex(); if ( currentUnit.isArchers() && !currentUnit.isHandFighting() ) { - actions.push_back( Battle::Command( MSG_BATTLE_ATTACK, currentUnitUID, targetUnitUID, targetUnitHead, 0 ) ); + actions.emplace_back( MSG_BATTLE_ATTACK, currentUnitUID, targetUnitUID, targetUnitHead, 0 ); break; } else { @@ -571,15 +608,24 @@ namespace AI if ( targetCell != -1 ) { if ( currentUnit.GetHeadIndex() != targetCell ) - actions.push_back( Battle::Command( MSG_BATTLE_MOVE, currentUnitUID, targetCell ) ); + actions.emplace_back( MSG_BATTLE_MOVE, currentUnitUID, targetCell ); - actions.push_back( Battle::Command( MSG_BATTLE_ATTACK, currentUnitUID, targetUnitUID, targetUnitHead, 0 ) ); + actions.emplace_back( MSG_BATTLE_ATTACK, currentUnitUID, targetUnitUID, targetUnitHead, 0 ); break; } } } } - actions.push_back( Battle::Command( MSG_BATTLE_END_TURN, currentUnitUID ) ); + actions.emplace_back( MSG_BATTLE_END_TURN, currentUnitUID ); + return actions; + } + + void Normal::BattleTurn( Arena & arena, const Unit & currentUnit, Actions & actions ) + { + const Actions & plannedActions = _battlePlanner.planUnitTurn( arena, currentUnit ); + actions.insert( actions.end(), plannedActions.begin(), plannedActions.end() ); + + actions.emplace_back( MSG_BATTLE_END_TURN, currentUnit.GetUID() ); } } From c812d3bec7bb4ca6d408ea6db58d984b2732592d Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Sun, 21 Feb 2021 10:39:45 +0800 Subject: [PATCH 14/84] Add asynchronous execution of MIDI calls (#2828) SDL Mixer can execute tasks only in one thread. In this case some systems face huge delays during gameplay. To avoid such horror we are shooting asynchronous tasks for cases when we don't expect music to be executed immediately. close #1008 --- src/Makefile | 2 +- src/fheroes2/agg/agg.cpp | 390 +++++++++++++++---- src/fheroes2/agg/agg.h | 8 +- src/fheroes2/ai/ai_base.cpp | 2 +- src/fheroes2/ai/ai_hero_action.cpp | 2 +- src/fheroes2/ai/normal/ai_normal_kingdom.cpp | 2 +- src/fheroes2/battle/battle_arena.cpp | 2 +- src/fheroes2/castle/castle_dialog.cpp | 2 +- src/fheroes2/game/game.cpp | 40 +- src/fheroes2/game/game_credits.cpp | 2 +- src/fheroes2/game/game_highscores.cpp | 2 +- src/fheroes2/game/game_loadgame.cpp | 4 +- src/fheroes2/game/game_mainmenu.cpp | 2 +- src/fheroes2/game/game_newgame.cpp | 4 +- src/fheroes2/game/game_scenarioinfo.cpp | 2 +- src/fheroes2/game/game_startgame.cpp | 2 +- src/fheroes2/gui/interface_focus.cpp | 4 +- src/fheroes2/heroes/heroes_move.cpp | 2 +- 18 files changed, 365 insertions(+), 109 deletions(-) diff --git a/src/Makefile b/src/Makefile index 348219fbab6..c46cb84a429 100644 --- a/src/Makefile +++ b/src/Makefile @@ -12,7 +12,7 @@ endif CFLAGS := $(CFLAGS) -fsigned-char CXXFLAGS := -std=c++11 $(CFLAGS) LDFLAGS := $(LDFLAGS) -LIBS := +LIBS := -pthread CFLAGS_TP := $(CFLAGS) CXXFLAGS_TP := $(CXXFLAGS) diff --git a/src/fheroes2/agg/agg.cpp b/src/fheroes2/agg/agg.cpp index bd6ae77c84d..c16d06ba2c7 100644 --- a/src/fheroes2/agg/agg.cpp +++ b/src/fheroes2/agg/agg.cpp @@ -21,9 +21,13 @@ ***************************************************************************/ #include +#include +#include #include #include +#include #include +#include #include #include "agg.h" @@ -101,7 +105,12 @@ namespace AGG void LoadFNT( void ); bool ReadDataDir( void ); - std::vector ReadChunk( const std::string & key, bool ignoreExpansion = false ); + std::vector ReadChunk( const std::string & key, bool ignoreExpansion = false ); + std::vector ReadMusicChunk( const std::string & key, const bool ignoreExpansion = false ); + + void PlayMusicInternally( const int mus, const bool loop ); + void PlaySoundInternally( const int m82 ); + void LoadLOOPXXSoundsInternally( const std::vector & vols ); /* return letter sprite */ // Surface GetUnicodeLetter( u32 ch, u32 ft ) @@ -127,6 +136,180 @@ namespace AGG // // return fnt_cache[ch].sfs[0]; // } + + fheroes2::AGGFile g_midiHeroes2AGG; + fheroes2::AGGFile g_midiHeroes2xAGG; + + // SDL MIDI player is single threaded library which requires a lot of time for some long midi compositions. + // This leads to a situation of short application freeze while a hero crosses terrains or ending a battle. + // The only way to avoid this is to fire MIDI requests asynchronously and synchronize them if needed. + class AsyncSoundManager + { + public: + AsyncSoundManager() + : _exitFlag( 0 ) + , _runFlag( 1 ) + {} + + ~AsyncSoundManager() + { + if ( _worker ) { + _mutex.lock(); + + _exitFlag = 1; + _runFlag = 1; + _workerNotification.notify_all(); + + _mutex.unlock(); + + _worker->join(); + _worker.reset(); + } + } + + void pushMusic( const int musicId, const bool isLooped ) + { + _createThreadIfNeeded(); + + std::lock_guard mutexLock( _mutex ); + + while ( !_musicTasks.empty() ) { + _musicTasks.pop(); + } + + _musicTasks.emplace( musicId, isLooped ); + _runFlag = 1; + _workerNotification.notify_all(); + } + + void pushSound( const int m82Sound ) + { + _createThreadIfNeeded(); + + std::lock_guard mutexLock( _mutex ); + + _soundTasks.emplace( m82Sound ); + _runFlag = 1; + _workerNotification.notify_all(); + } + + void pushLoopSound( const std::vector & vols ) + { + _createThreadIfNeeded(); + + std::lock_guard mutexLock( _mutex ); + + _loopSoundTasks.emplace( vols ); + _runFlag = 1; + _workerNotification.notify_all(); + } + + void sync() + { + std::lock_guard mutexLock( _mutex ); + + while ( !_musicTasks.empty() ) { + _musicTasks.pop(); + } + + while ( !_soundTasks.empty() ) { + _soundTasks.pop(); + } + + while ( !_loopSoundTasks.empty() ) { + _loopSoundTasks.pop(); + } + } + + // This mutex is used to avoid access to global objects and classes related to SDL Mixer. + std::mutex & resourceMutex() + { + return _resourceMutex; + } + + private: + std::unique_ptr _worker; + std::mutex _mutex; + + std::condition_variable _workerNotification; + std::condition_variable _masterNotification; + + std::queue > _musicTasks; + std::queue _soundTasks; + std::queue > _loopSoundTasks; + + uint8_t _exitFlag; + uint8_t _runFlag; + + std::mutex _resourceMutex; + + void _createThreadIfNeeded() + { + if ( !_worker ) { + _runFlag = 1; + _worker.reset( new std::thread( AsyncSoundManager::_workerThread, this ) ); + + std::unique_lock mutexLock( _mutex ); + _masterNotification.wait( mutexLock, [&] { return _runFlag == 0; } ); + } + } + + static void _workerThread( AsyncSoundManager * manager ) + { + assert( manager != nullptr ); + + manager->_mutex.lock(); + manager->_runFlag = 0; + manager->_masterNotification.notify_one(); + manager->_mutex.unlock(); + + while ( manager->_exitFlag == 0 ) { + std::unique_lock mutexLock( manager->_mutex ); + manager->_workerNotification.wait( mutexLock, [&] { return manager->_runFlag == 1; } ); + mutexLock.unlock(); + + if ( manager->_exitFlag ) + break; + + manager->_mutex.lock(); + + if ( !manager->_soundTasks.empty() ) { + const int m82Sound = manager->_soundTasks.back(); + manager->_soundTasks.pop(); + + manager->_mutex.unlock(); + + PlaySoundInternally( m82Sound ); + } + else if ( !manager->_loopSoundTasks.empty() ) { + const std::vector vols = manager->_loopSoundTasks.back(); + manager->_loopSoundTasks.pop(); + + manager->_mutex.unlock(); + + LoadLOOPXXSoundsInternally( vols ); + } + else if ( !manager->_musicTasks.empty() ) { + const std::pair musicInfo = manager->_musicTasks.back(); + + while ( !manager->_musicTasks.empty() ) { + manager->_musicTasks.pop(); + } + + manager->_mutex.unlock(); + + PlayMusicInternally( musicInfo.first, musicInfo.second ); + } + else { + manager->_runFlag = 0; + + manager->_mutex.unlock(); + } + } + } + }; + + AsyncSoundManager g_asyncSoundManager; } /* read data directory */ @@ -147,10 +330,14 @@ bool AGG::ReadDataDir( void ) // attach agg files for ( ListFiles::const_iterator it = aggs.begin(); it != aggs.end(); ++it ) { std::string lower = StringLower( *it ); - if ( std::string::npos != lower.find( "heroes2.agg" ) && !heroes2_agg.isGood() ) + if ( std::string::npos != lower.find( "heroes2.agg" ) && !heroes2_agg.isGood() ) { heroes2_agg.open( *it ); - if ( std::string::npos != lower.find( "heroes2x.agg" ) && !heroes2x_agg.isGood() ) + g_midiHeroes2AGG.open( *it ); + } + if ( std::string::npos != lower.find( "heroes2x.agg" ) && !heroes2x_agg.isGood() ) { heroes2x_agg.open( *it ); + g_midiHeroes2xAGG.open( *it ); + } } conf.SetPriceLoyaltyVersion( heroes2x_agg.isGood() ); @@ -169,6 +356,17 @@ std::vector AGG::ReadChunk( const std::string & key, bool ignoreExpansi return heroes2_agg.read( key ); } +std::vector AGG::ReadMusicChunk( const std::string & key, const bool ignoreExpansion ) +{ + if ( !ignoreExpansion && g_midiHeroes2xAGG.isGood() ) { + const std::vector & buf = g_midiHeroes2xAGG.read( key ); + if ( !buf.empty() ) + return buf; + } + + return g_midiHeroes2AGG.read( key ); +} + /* load 82M object to AGG::Cache in Audio::CVT */ void AGG::LoadWAV( int m82, std::vector & v ) { @@ -200,7 +398,7 @@ void AGG::LoadWAV( int m82, std::vector & v ) #endif DEBUG_LOG( DBG_ENGINE, DBG_TRACE, M82::GetString( m82 ) ); - const std::vector & body = ReadChunk( M82::GetString( m82 ) ); + const std::vector & body = ReadMusicChunk( M82::GetString( m82 ) ); if ( body.size() ) { #ifdef WITH_MIXER @@ -256,10 +454,11 @@ void AGG::LoadWAV( int m82, std::vector & v ) void AGG::LoadMID( int xmi, std::vector & v ) { DEBUG_LOG( DBG_ENGINE, DBG_TRACE, XMI::GetString( xmi ) ); - const std::vector & body = ReadChunk( XMI::GetString( xmi ), xmi >= XMI::MIDI_ORIGINAL_KNIGHT ); + const std::vector & body = ReadMusicChunk( XMI::GetString( xmi ), xmi >= XMI::MIDI_ORIGINAL_KNIGHT ); - if ( body.size() ) + if ( !body.empty() ) { v = Music::Xmi2Mid( body ); + } } /* return CVT */ @@ -280,88 +479,141 @@ const std::vector & AGG::GetMID( int xmi ) return v; } -void AGG::LoadLOOPXXSounds( const std::vector & vols ) +void AGG::LoadLOOPXXSounds( const std::vector & vols, bool asyncronizedCall ) +{ + if ( vols.empty() ) { + return; + } + + if ( asyncronizedCall ) { + g_asyncSoundManager.pushLoopSound( vols ); + } + else { + g_asyncSoundManager.sync(); + LoadLOOPXXSoundsInternally( vols ); + } +} + +void AGG::LoadLOOPXXSoundsInternally( const std::vector & vols ) { const Settings & conf = Settings::Get(); + if ( !conf.Sound() ) { + return; + } - if ( conf.Sound() ) { - // set volume loop sounds - for ( std::vector::const_iterator itv = vols.begin(); itv != vols.end(); ++itv ) { - int vol = *itv; - int m82 = M82::GetLOOP00XX( std::distance( vols.begin(), itv ) ); - if ( M82::UNKNOWN == m82 ) - continue; - - // find loops - std::vector::iterator itl = std::find( loop_sounds.begin(), loop_sounds.end(), m82 ); - - if ( itl != loop_sounds.end() ) { - // unused and free - if ( 0 == vol ) { - if ( Mixer::isPlaying( ( *itl ).channel ) ) { - Mixer::Pause( ( *itl ).channel ); - Mixer::Volume( ( *itl ).channel, Mixer::MaxVolume() * conf.SoundVolume() / 10 ); - Mixer::Stop( ( *itl ).channel ); - } - ( *itl ).sound = M82::UNKNOWN; - } - // used and set vols - else if ( Mixer::isPlaying( ( *itl ).channel ) ) { + std::lock_guard mutexLock( g_asyncSoundManager.resourceMutex() ); + + // set volume loop sounds + for ( std::vector::const_iterator itv = vols.begin(); itv != vols.end(); ++itv ) { + int vol = *itv; + int m82 = M82::GetLOOP00XX( std::distance( vols.begin(), itv ) ); + if ( M82::UNKNOWN == m82 ) + continue; + + // find loops + std::vector::iterator itl = std::find( loop_sounds.begin(), loop_sounds.end(), m82 ); + + if ( itl != loop_sounds.end() ) { + // unused and free + if ( 0 == vol ) { + if ( Mixer::isPlaying( ( *itl ).channel ) ) { Mixer::Pause( ( *itl ).channel ); - Mixer::Volume( ( *itl ).channel, vol * conf.SoundVolume() / 10 ); - Mixer::Resume( ( *itl ).channel ); + Mixer::Volume( ( *itl ).channel, Mixer::MaxVolume() * conf.SoundVolume() / 10 ); + Mixer::Stop( ( *itl ).channel ); } + ( *itl ).sound = M82::UNKNOWN; } - else - // new sound - if ( 0 != vol ) { - const std::vector & v = GetWAV( m82 ); - const int ch = Mixer::Play( &v[0], v.size(), -1, true ); - - if ( 0 <= ch ) { - Mixer::Pause( ch ); - Mixer::Volume( ch, vol * conf.SoundVolume() / 10 ); - Mixer::Resume( ch ); - - // find unused - itl = std::find( loop_sounds.begin(), loop_sounds.end(), static_cast( M82::UNKNOWN ) ); - - if ( itl != loop_sounds.end() ) { - ( *itl ).sound = m82; - ( *itl ).channel = ch; - } - else - loop_sounds.push_back( loop_sound_t( m82, ch ) ); - - DEBUG_LOG( DBG_ENGINE, DBG_TRACE, M82::GetString( m82 ) ); + // used and set vols + else if ( Mixer::isPlaying( ( *itl ).channel ) ) { + Mixer::Pause( ( *itl ).channel ); + Mixer::Volume( ( *itl ).channel, vol * conf.SoundVolume() / 10 ); + Mixer::Resume( ( *itl ).channel ); + } + } + else + // new sound + if ( 0 != vol ) { + const std::vector & v = GetWAV( m82 ); + const int ch = Mixer::Play( &v[0], v.size(), -1, true ); + + if ( 0 <= ch ) { + Mixer::Pause( ch ); + Mixer::Volume( ch, vol * conf.SoundVolume() / 10 ); + Mixer::Resume( ch ); + + // find unused + itl = std::find( loop_sounds.begin(), loop_sounds.end(), static_cast( M82::UNKNOWN ) ); + + if ( itl != loop_sounds.end() ) { + ( *itl ).sound = m82; + ( *itl ).channel = ch; } + else + loop_sounds.emplace_back( m82, ch ); + + DEBUG_LOG( DBG_ENGINE, DBG_TRACE, M82::GetString( m82 ) ); } } } } /* wrapper Audio::Play */ -void AGG::PlaySound( int m82 ) +void AGG::PlaySound( int m82, bool asyncronizedCall ) { - const Settings & conf = Settings::Get(); + if ( asyncronizedCall ) { + g_asyncSoundManager.pushSound( m82 ); + } + else { + g_asyncSoundManager.sync(); + PlaySoundInternally( m82 ); + } +} - if ( conf.Sound() ) { - DEBUG_LOG( DBG_ENGINE, DBG_TRACE, M82::GetString( m82 ) ); - const std::vector & v = AGG::GetWAV( m82 ); - const int ch = Mixer::Play( &v[0], v.size(), -1, false ); - Mixer::Pause( ch ); - Mixer::Volume( ch, Mixer::MaxVolume() * conf.SoundVolume() / 10 ); - Mixer::Resume( ch ); +void AGG::PlaySoundInternally( const int m82 ) +{ + const Settings & conf = Settings::Get(); + if ( !conf.Sound() ) { + return; } + + std::lock_guard mutexLock( g_asyncSoundManager.resourceMutex() ); + + DEBUG_LOG( DBG_ENGINE, DBG_TRACE, M82::GetString( m82 ) ); + const std::vector & v = AGG::GetWAV( m82 ); + const int ch = Mixer::Play( &v[0], v.size(), -1, false ); + Mixer::Pause( ch ); + Mixer::Volume( ch, Mixer::MaxVolume() * conf.SoundVolume() / 10 ); + Mixer::Resume( ch ); } /* wrapper Audio::Play */ -void AGG::PlayMusic( int mus, bool loop ) +void AGG::PlayMusic( int mus, bool loop, bool asyncronizedCall ) +{ + if ( MUS::UNUSED == mus || MUS::UNKNOWN == mus ) { + return; + } + + if ( asyncronizedCall ) { + g_asyncSoundManager.pushMusic( mus, loop ); + } + else { + g_asyncSoundManager.sync(); + PlayMusicInternally( mus, loop ); + } +} + +void AGG::PlayMusicInternally( const int mus, const bool loop ) { const Settings & conf = Settings::Get(); + if ( !conf.Music() ) { + return; + } - if ( !conf.Music() || MUS::UNUSED == mus || MUS::UNKNOWN == mus || ( Game::CurrentMusic() == mus && Music::isPlaying() ) ) + std::lock_guard mutexLock( g_asyncSoundManager.resourceMutex() ); + + if ( Game::CurrentMusic() == mus && Music::isPlaying() ) { return; + } Game::SetCurrentMusic( mus ); const std::string prefix_music( "music" ); @@ -411,7 +663,7 @@ void AGG::PlayMusic( int mus, bool loop ) // Check if music needs to be pulled from HEROES2X int xmi = XMI::UNKNOWN; if ( type == MUSIC_MIDI_EXPANSION ) { - xmi = XMI::FromMUS( mus, heroes2x_agg.isGood() ); + xmi = XMI::FromMUS( mus, g_midiHeroes2xAGG.isGood() ); } if ( XMI::UNKNOWN == xmi ) { @@ -500,8 +752,12 @@ std::vector AGG::LoadBINFRM( const char * frm_file ) return AGG::ReadChunk( frm_file ); } -void AGG::ResetMixer( void ) +void AGG::ResetMixer() { + g_asyncSoundManager.sync(); + + std::lock_guard mutexLock( g_asyncSoundManager.resourceMutex() ); + Mixer::Reset(); loop_sounds.clear(); loop_sounds.reserve( 7 ); diff --git a/src/fheroes2/agg/agg.h b/src/fheroes2/agg/agg.h index 8e50e7aeb70..a64fe3fabbf 100644 --- a/src/fheroes2/agg/agg.h +++ b/src/fheroes2/agg/agg.h @@ -38,10 +38,10 @@ namespace AGG #ifdef WITH_TTF u32 GetFontHeight( bool small ); #endif - void LoadLOOPXXSounds( const std::vector & ); - void PlaySound( int m82 ); - void PlayMusic( int mus, bool loop = true ); - void ResetMixer( void ); + void LoadLOOPXXSounds( const std::vector & vols, bool asyncronizedCall = false ); + void PlaySound( int m82, bool asyncronizedCall = false ); + void PlayMusic( int mus, bool loop = true, bool asyncronizedCall = false ); + void ResetMixer(); } namespace fheroes2 diff --git a/src/fheroes2/ai/ai_base.cpp b/src/fheroes2/ai/ai_base.cpp index 92f99e99d1a..b8d0da163c7 100644 --- a/src/fheroes2/ai/ai_base.cpp +++ b/src/fheroes2/ai/ai_base.cpp @@ -162,7 +162,7 @@ namespace AI } if ( !Settings::Get().MusicMIDI() ) - AGG::PlayMusic( MUS::COMPUTER_TURN ); + AGG::PlayMusic( MUS::COMPUTER_TURN, true, true ); Interface::StatusWindow & status = Interface::Basic::Get().GetStatusWindow(); diff --git a/src/fheroes2/ai/ai_hero_action.cpp b/src/fheroes2/ai/ai_hero_action.cpp index f6c12b87891..e32fd67e9c8 100644 --- a/src/fheroes2/ai/ai_hero_action.cpp +++ b/src/fheroes2/ai/ai_hero_action.cpp @@ -457,7 +457,7 @@ namespace AI AI::Get().HeroesActionComplete( hero ); // reset if during an action music was stopped - AGG::PlayMusic( MUS::COMPUTER_TURN ); + AGG::PlayMusic( MUS::COMPUTER_TURN, true, true ); } void AIToHeroes( Heroes & hero, s32 dst_index ) diff --git a/src/fheroes2/ai/normal/ai_normal_kingdom.cpp b/src/fheroes2/ai/normal/ai_normal_kingdom.cpp index 8e5c357d255..a2dc693d3a1 100644 --- a/src/fheroes2/ai/normal/ai_normal_kingdom.cpp +++ b/src/fheroes2/ai/normal/ai_normal_kingdom.cpp @@ -44,7 +44,7 @@ namespace AI Interface::StatusWindow & status = Interface::Basic::Get().GetStatusWindow(); status.RedrawTurnProgress( 0 ); - AGG::PlayMusic( MUS::COMPUTER_TURN ); + AGG::PlayMusic( MUS::COMPUTER_TURN, true, true ); KingdomHeroes & heroes = kingdom.GetHeroes(); KingdomCastles & castles = kingdom.GetCastles(); diff --git a/src/fheroes2/battle/battle_arena.cpp b/src/fheroes2/battle/battle_arena.cpp index b7adff9b80c..893e5ade8b1 100644 --- a/src/fheroes2/battle/battle_arena.cpp +++ b/src/fheroes2/battle/battle_arena.cpp @@ -419,7 +419,7 @@ void Battle::Arena::Turns( void ) DEBUG_LOG( DBG_BATTLE, DBG_TRACE, current_turn ); if ( interface && conf.Music() && !Music::isPlaying() ) - AGG::PlayMusic( MUS::GetBattleRandom() ); + AGG::PlayMusic( MUS::GetBattleRandom(), true, true ); army1->NewTurn(); army2->NewTurn(); diff --git a/src/fheroes2/castle/castle_dialog.cpp b/src/fheroes2/castle/castle_dialog.cpp index 8215fe8b83f..fdf278e3e07 100644 --- a/src/fheroes2/castle/castle_dialog.cpp +++ b/src/fheroes2/castle/castle_dialog.cpp @@ -305,7 +305,7 @@ int Castle::OpenDialog( bool readonly ) buttonNextCastle.draw(); buttonExit.draw(); - AGG::PlayMusic( MUS::FromRace( race ) ); + AGG::PlayMusic( MUS::FromRace( race ), true, true ); LocalEvent & le = LocalEvent::Get(); cursor.Show(); diff --git a/src/fheroes2/game/game.cpp b/src/fheroes2/game/game.cpp index 9c1444c876e..8abff3f133d 100644 --- a/src/fheroes2/game/game.cpp +++ b/src/fheroes2/game/game.cpp @@ -237,31 +237,31 @@ u32 & Game::CastleAnimationFrame( void ) /* play all sound from focus area game */ void Game::EnvironmentSoundMixer( void ) { - const Point abs_pt( Interface::GetFocusCenter() ); - const Settings & conf = Settings::Get(); + if ( !Settings::Get().Sound() ) { + return; + } - if ( conf.Sound() ) { - std::fill( reserved_vols.begin(), reserved_vols.end(), 0 ); - - // scan 4x4 square from focus - for ( s32 yy = abs_pt.y - 3; yy <= abs_pt.y + 3; ++yy ) { - for ( s32 xx = abs_pt.x - 3; xx <= abs_pt.x + 3; ++xx ) { - if ( Maps::isValidAbsPoint( xx, yy ) ) { - const u32 channel = GetMixerChannelFromObject( world.GetTiles( xx, yy ) ); - if ( channel < reserved_vols.size() ) { - // calculation volume - const int length = std::max( std::abs( xx - abs_pt.x ), std::abs( yy - abs_pt.y ) ); - const int volume = ( 2 < length ? 4 : ( 1 < length ? 8 : ( 0 < length ? 12 : 16 ) ) ) * Mixer::MaxVolume() / 16; - - if ( volume > reserved_vols[channel] ) - reserved_vols[channel] = volume; - } + const Point abs_pt( Interface::GetFocusCenter() ); + std::fill( reserved_vols.begin(), reserved_vols.end(), 0 ); + + // scan 4x4 square from focus + for ( s32 yy = abs_pt.y - 3; yy <= abs_pt.y + 3; ++yy ) { + for ( s32 xx = abs_pt.x - 3; xx <= abs_pt.x + 3; ++xx ) { + if ( Maps::isValidAbsPoint( xx, yy ) ) { + const u32 channel = GetMixerChannelFromObject( world.GetTiles( xx, yy ) ); + if ( channel < reserved_vols.size() ) { + // calculation volume + const int length = std::max( std::abs( xx - abs_pt.x ), std::abs( yy - abs_pt.y ) ); + const int volume = ( 2 < length ? 4 : ( 1 < length ? 8 : ( 0 < length ? 12 : 16 ) ) ) * Mixer::MaxVolume() / 16; + + if ( volume > reserved_vols[channel] ) + reserved_vols[channel] = volume; } } } - - AGG::LoadLOOPXXSounds( reserved_vols ); } + + AGG::LoadLOOPXXSounds( reserved_vols, true ); } u32 Game::GetMixerChannelFromObject( const Maps::Tiles & tile ) diff --git a/src/fheroes2/game/game_credits.cpp b/src/fheroes2/game/game_credits.cpp index 1c5b64237eb..14af1ff6f38 100644 --- a/src/fheroes2/game/game_credits.cpp +++ b/src/fheroes2/game/game_credits.cpp @@ -117,7 +117,7 @@ void Game::ShowCredits() const fheroes2::Sprite & goblin = fheroes2::AGG::GetICN( ICN::GOBLIN, 27 ); fheroes2::Blit( goblin, display, screenOffset.x + ( display.DEFAULT_WIDTH - goblin.width() ) / 2, screenOffset.y + ( display.DEFAULT_HEIGHT - goblin.height() ) / 2 ); - AGG::PlayMusic( MUS::VICTORY ); + AGG::PlayMusic( MUS::VICTORY, true, true ); LocalEvent & le = LocalEvent::Get(); while ( le.HandleEvents() ) { diff --git a/src/fheroes2/game/game_highscores.cpp b/src/fheroes2/game/game_highscores.cpp index 741b745d57d..1c3507b404f 100644 --- a/src/fheroes2/game/game_highscores.cpp +++ b/src/fheroes2/game/game_highscores.cpp @@ -199,7 +199,7 @@ int Game::HighScores() cursor.SetThemes( cursor.POINTER ); Mixer::Pause(); - AGG::PlayMusic( MUS::MAINMENU ); + AGG::PlayMusic( MUS::MAINMENU, true, true ); hgs.Load( stream.str().c_str() ); const fheroes2::Sprite & back = fheroes2::AGG::GetICN( ICN::HSBKG, 0 ); diff --git a/src/fheroes2/game/game_loadgame.cpp b/src/fheroes2/game/game_loadgame.cpp index eb6cfc4b939..1010b5ee779 100644 --- a/src/fheroes2/game/game_loadgame.cpp +++ b/src/fheroes2/game/game_loadgame.cpp @@ -134,7 +134,7 @@ int Game::LoadMulti( void ) int Game::LoadGame( void ) { Mixer::Pause(); - AGG::PlayMusic( MUS::MAINMENU ); + AGG::PlayMusic( MUS::MAINMENU, true, true ); fheroes2::Display & display = fheroes2::Display::instance(); Cursor & cursor = Cursor::Get(); @@ -232,7 +232,7 @@ int Game::LoadStandard( void ) int Game::DisplayLoadGameDialog() { Mixer::Pause(); - AGG::PlayMusic( MUS::MAINMENU ); + AGG::PlayMusic( MUS::MAINMENU, true, true ); // cursor Cursor & cursor = Cursor::Get(); cursor.SetThemes( cursor.POINTER ); diff --git a/src/fheroes2/game/game_mainmenu.cpp b/src/fheroes2/game/game_mainmenu.cpp index b4cb64084d3..9be916379cc 100644 --- a/src/fheroes2/game/game_mainmenu.cpp +++ b/src/fheroes2/game/game_mainmenu.cpp @@ -45,7 +45,7 @@ int Game::MainMenu( bool isFirstGameRun ) { Mixer::Pause(); - AGG::PlayMusic( MUS::MAINMENU ); + AGG::PlayMusic( MUS::MAINMENU, true, true ); Settings & conf = Settings::Get(); diff --git a/src/fheroes2/game/game_newgame.cpp b/src/fheroes2/game/game_newgame.cpp index 23efb494d11..0222c91a000 100644 --- a/src/fheroes2/game/game_newgame.cpp +++ b/src/fheroes2/game/game_newgame.cpp @@ -108,7 +108,7 @@ int Game::NewCampaign() campaignSaveData.setCampaignID( chosenCampaign ); campaignSaveData.setCurrentScenarioID( 0 ); - AGG::PlayMusic( MUS::VICTORY ); + AGG::PlayMusic( MUS::VICTORY, true, true ); return Game::SELECT_CAMPAIGN_SCENARIO; } @@ -176,7 +176,7 @@ int Game::NewNetwork( void ) int Game::NewGame( void ) { Mixer::Pause(); - AGG::PlayMusic( MUS::MAINMENU ); + AGG::PlayMusic( MUS::MAINMENU, true, true ); Settings & conf = Settings::Get(); // reset last save name diff --git a/src/fheroes2/game/game_scenarioinfo.cpp b/src/fheroes2/game/game_scenarioinfo.cpp index 33686d342a5..0fbd49ffe74 100644 --- a/src/fheroes2/game/game_scenarioinfo.cpp +++ b/src/fheroes2/game/game_scenarioinfo.cpp @@ -92,7 +92,7 @@ int Game::ScenarioInfo( void ) { Settings & conf = Settings::Get(); - AGG::PlayMusic( MUS::MAINMENU ); + AGG::PlayMusic( MUS::MAINMENU, true, true ); MapsFileInfoList lists; if ( !PrepareMapsFileInfoList( lists, ( conf.IsGameType( Game::TYPE_MULTI ) ) ) ) { diff --git a/src/fheroes2/game/game_startgame.cpp b/src/fheroes2/game/game_startgame.cpp index c3474bc9698..b6d0fe17b07 100644 --- a/src/fheroes2/game/game_startgame.cpp +++ b/src/fheroes2/game/game_startgame.cpp @@ -650,7 +650,7 @@ int Interface::Basic::HumanTurn( bool isload ) if ( 1 < world.CountWeek() && world.BeginWeek() ) { const int currentMusic = Game::CurrentMusic(); ShowNewWeekDialog(); - AGG::PlayMusic( currentMusic, true ); + AGG::PlayMusic( currentMusic, true, true ); } // show event day diff --git a/src/fheroes2/gui/interface_focus.cpp b/src/fheroes2/gui/interface_focus.cpp index a7d35db1cc7..92a6ced836f 100644 --- a/src/fheroes2/gui/interface_focus.cpp +++ b/src/fheroes2/gui/interface_focus.cpp @@ -57,8 +57,8 @@ void Interface::Basic::SetFocus( Heroes * hero ) const int heroIndexPos = hero->GetIndex(); if ( !Game::ChangeMusicDisabled() && heroIndexPos >= 0 ) { - AGG::PlayMusic( MUS::FromGround( world.GetTiles( heroIndexPos ).GetGround() ) ); Game::EnvironmentSoundMixer(); + AGG::PlayMusic( MUS::FromGround( world.GetTiles( heroIndexPos ).GetGround() ), true, true ); } } } @@ -83,8 +83,8 @@ void Interface::Basic::SetFocus( Castle * castle ) gameArea.SetCenter( castle->GetCenter() ); statusWindow.SetState( StatusType::STATUS_FUNDS ); - AGG::PlayMusic( MUS::FromGround( world.GetTiles( castle->GetIndex() ).GetGround() ) ); Game::EnvironmentSoundMixer(); + AGG::PlayMusic( MUS::FromGround( world.GetTiles( castle->GetIndex() ).GetGround() ), true, true ); } } diff --git a/src/fheroes2/heroes/heroes_move.cpp b/src/fheroes2/heroes/heroes_move.cpp index 34c684176b2..7ee1dc04844 100644 --- a/src/fheroes2/heroes/heroes_move.cpp +++ b/src/fheroes2/heroes/heroes_move.cpp @@ -76,7 +76,7 @@ void PlayWalkSound( int ground ) } if ( wav != M82::UNKNOWN ) - AGG::PlaySound( wav ); + AGG::PlaySound( wav, true ); } bool ReflectSprite( int from ) From 5a9d06ca6b4e8df798ad756ad75d8dda13cd00a8 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Sun, 21 Feb 2021 17:17:10 +0800 Subject: [PATCH 15/84] Add multiple compilation flags (#2811) to make code even better --- src/Makefile | 2 +- src/engine/localevent.cpp | 6 +++-- src/engine/rand.h | 15 +++++------ src/engine/tools.cpp | 7 ----- src/engine/tools.h | 1 - src/fheroes2/agg/bin_info.cpp | 2 +- src/fheroes2/agg/icn.cpp | 2 +- src/fheroes2/ai/normal/ai_normal_kingdom.cpp | 4 +-- src/fheroes2/army/army.cpp | 11 +++++--- src/fheroes2/battle/battle_arena.cpp | 2 +- src/fheroes2/battle/battle_catapult.cpp | 3 +-- src/fheroes2/battle/battle_troop.cpp | 2 +- src/fheroes2/castle/mageguild.cpp | 2 +- src/fheroes2/dialog/dialog_armyinfo.cpp | 9 +++++++ src/fheroes2/dialog/dialog_selectitems.cpp | 2 +- src/fheroes2/game/game_static.cpp | 2 +- src/fheroes2/gui/interface_focus.cpp | 17 +++++++----- src/fheroes2/heroes/heroes.cpp | 12 ++++----- src/fheroes2/heroes/heroes.h | 2 +- src/fheroes2/heroes/skill.cpp | 2 +- src/fheroes2/monster/monster.cpp | 4 +-- src/fheroes2/resource/artifact.cpp | 2 +- src/fheroes2/spell/spell.cpp | 2 +- src/fheroes2/world/world.cpp | 23 ++++++---------- src/fheroes2/world/world_loadmap.cpp | 28 ++++++++++---------- 25 files changed, 82 insertions(+), 82 deletions(-) diff --git a/src/Makefile b/src/Makefile index c46cb84a429..64eeac557b5 100644 --- a/src/Makefile +++ b/src/Makefile @@ -18,7 +18,7 @@ CFLAGS_TP := $(CFLAGS) CXXFLAGS_TP := $(CXXFLAGS) # Add -Wconversion -Wsign-conversion flags back once we fix all other warnings! -CFLAGS := $(CXXFLAGS) -Wall -Wextra -Wpedantic -Wfloat-conversion -Wshadow -Wfloat-equal -Wredundant-decls +CFLAGS := $(CXXFLAGS) -Wall -Wextra -Wpedantic -Wfloat-conversion -Wshadow -Wfloat-equal -Wredundant-decls -Wdouble-promotion -Wunused -Wuninitialized ifdef FHEROES2_STRICT_COMPILATION CFLAGS := $(CFLAGS) -Werror diff --git a/src/engine/localevent.cpp b/src/engine/localevent.cpp index a5fce5f4602..04109d026fe 100644 --- a/src/engine/localevent.cpp +++ b/src/engine/localevent.cpp @@ -1153,8 +1153,10 @@ void LocalEvent::HandleTouchEvent( const SDL_TouchFingerEvent & event ) SetModes( MOUSE_MOTION ); - _emulatedPointerPosX = ( screenResolution.width * event.x - windowRect.x ) * ( static_cast( gameSurfaceRes.width ) / windowRect.width ); - _emulatedPointerPosY = ( screenResolution.height * event.y - windowRect.y ) * ( static_cast( gameSurfaceRes.height ) / windowRect.height ); + _emulatedPointerPosX + = static_cast( screenResolution.width * event.x - windowRect.x ) * ( static_cast( gameSurfaceRes.width ) / windowRect.width ); + _emulatedPointerPosY + = static_cast( screenResolution.height * event.y - windowRect.y ) * ( static_cast( gameSurfaceRes.height ) / windowRect.height ); mouse_cu.x = static_cast( _emulatedPointerPosX ); mouse_cu.y = static_cast( _emulatedPointerPosY ); diff --git a/src/engine/rand.h b/src/engine/rand.h index 1690346a8e7..10ca741a013 100644 --- a/src/engine/rand.h +++ b/src/engine/rand.h @@ -22,6 +22,7 @@ #ifndef H2RAND_H #define H2RAND_H +#include #include #include #include @@ -36,24 +37,22 @@ namespace Rand uint32_t GetWithSeed( uint32_t from, uint32_t to, uint32_t seed ); template - const T * Get( const std::vector & vec ) + const T & Get( const std::vector & vec ) { - if ( vec.empty() ) - return nullptr; + assert( !vec.empty() ); const uint32_t id = Rand::Get( static_cast( vec.size() - 1 ) ); - return &vec[id]; + return vec[id]; } template - const T * Get( const std::list & list ) + const T & Get( const std::list & list ) { - if ( list.empty() ) - return nullptr; + assert( !list.empty() ); typename std::list::const_iterator it = list.begin(); std::advance( it, Rand::Get( static_cast( list.size() - 1 ) ) ); - return &( *it ); + return *it; } typedef std::pair ValuePercent; diff --git a/src/engine/tools.cpp b/src/engine/tools.cpp index 7fc3894febb..a13ae57749a 100644 --- a/src/engine/tools.cpp +++ b/src/engine/tools.cpp @@ -92,13 +92,6 @@ std::string GetStringShort( int value ) return std::to_string( value ); } -std::string GetString( double value, u8 prec ) -{ - std::ostringstream stream; - stream << std::setprecision( prec ) << value; - return stream.str(); -} - std::string GetHexString( int value, int width ) { std::ostringstream stream; diff --git a/src/engine/tools.h b/src/engine/tools.h index 0216a26b111..ed5493a5763 100644 --- a/src/engine/tools.h +++ b/src/engine/tools.h @@ -29,7 +29,6 @@ #include "types.h" std::string GetStringShort( int ); -std::string GetString( double, u8 ); std::string GetHexString( int value, int width = 8 ); int GetInt( const std::string & ); diff --git a/src/fheroes2/agg/bin_info.cpp b/src/fheroes2/agg/bin_info.cpp index 693085e24d9..c1a8b35b309 100644 --- a/src/fheroes2/agg/bin_info.cpp +++ b/src/fheroes2/agg/bin_info.cpp @@ -353,7 +353,7 @@ namespace Bin_Info return 0; for ( size_t id = 0u; id < angles.size() - 1; ++id ) { - if ( angle >= ( angles[id] + angles[id + 1] ) / 2 ) + if ( angle >= static_cast( angles[id] + angles[id + 1] ) / 2.0 ) return id; } return angles.size() - 1; diff --git a/src/fheroes2/agg/icn.cpp b/src/fheroes2/agg/icn.cpp index aa685bd4925..fdb698054ad 100644 --- a/src/fheroes2/agg/icn.cpp +++ b/src/fheroes2/agg/icn.cpp @@ -1651,7 +1651,7 @@ int ICN::PORTxxxx( int heroId ) case Heroes::JARKONAS: return ICN::PORT0070; - case Heroes::SANDYSANDY: + case Heroes::DEBUG_HERO: return ICN::PORT0059; default: diff --git a/src/fheroes2/ai/normal/ai_normal_kingdom.cpp b/src/fheroes2/ai/normal/ai_normal_kingdom.cpp index a2dc693d3a1..5655c507100 100644 --- a/src/fheroes2/ai/normal/ai_normal_kingdom.cpp +++ b/src/fheroes2/ai/normal/ai_normal_kingdom.cpp @@ -166,7 +166,7 @@ namespace AI } } - size_t heroLimit = world.w() / Maps::SMALL + 1; + int32_t heroLimit = world.w() / Maps::SMALL + 1; if ( _personality == EXPLORER ) heroLimit++; if ( slowEarlyGame ) @@ -183,7 +183,7 @@ namespace AI VecCastles sortedCastleList( castles ); sortedCastleList.SortByBuildingValue(); - if ( heroes.size() < heroLimit ) { + if ( heroes.size() < static_cast( heroLimit ) ) { // safe to cast as heroLimit is > 0 Recruits & rec = kingdom.GetRecruits(); Castle * recruitmentCastle = NULL; int lowestHeroCount = heroLimit; diff --git a/src/fheroes2/army/army.cpp b/src/fheroes2/army/army.cpp index 46fc45fef42..bf9cb73b021 100644 --- a/src/fheroes2/army/army.cpp +++ b/src/fheroes2/army/army.cpp @@ -371,7 +371,7 @@ u32 Troops::GetUniqueCount( void ) const monsters.insert( troop->GetID() ); } - return monsters.size(); + return static_cast( monsters.size() ); // safe to cast as usually the army has no more than 5 monsters } double Troops::GetStrength() const @@ -1013,7 +1013,8 @@ bool Army::isSpreadFormat( void ) const int Army::GetColor( void ) const { - return GetCommander() ? GetCommander()->GetColor() : color; + const HeroBase * currentCommander = GetCommander(); + return currentCommander != nullptr ? currentCommander->GetColor() : color; } void Army::SetColor( int cl ) @@ -1043,7 +1044,8 @@ int Army::GetRace( void ) const int Army::GetLuck( void ) const { - return GetCommander() ? GetCommander()->GetLuck() : GetLuckModificator( NULL ); + const HeroBase * currentCommander = GetCommander(); + return currentCommander != nullptr ? currentCommander->GetLuck() : GetLuckModificator( NULL ); } int Army::GetLuckModificator( const std::string * ) const @@ -1053,7 +1055,8 @@ int Army::GetLuckModificator( const std::string * ) const int Army::GetMorale( void ) const { - return GetCommander() ? GetCommander()->GetMorale() : GetMoraleModificator( NULL ); + const HeroBase * currentCommander = GetCommander(); + return currentCommander != nullptr ? currentCommander->GetMorale() : GetMoraleModificator( NULL ); } // TODO:: need optimize diff --git a/src/fheroes2/battle/battle_arena.cpp b/src/fheroes2/battle/battle_arena.cpp index 893e5ade8b1..c3c4a2b7133 100644 --- a/src/fheroes2/battle/battle_arena.cpp +++ b/src/fheroes2/battle/battle_arena.cpp @@ -98,7 +98,7 @@ int GetCovr( int ground ) break; } - return covrs.empty() ? ICN::UNKNOWN : *Rand::Get( covrs ); + return covrs.empty() ? ICN::UNKNOWN : Rand::Get( covrs ); } StreamBase & Battle::operator<<( StreamBase & msg, const TargetInfo & t ) diff --git a/src/fheroes2/battle/battle_catapult.cpp b/src/fheroes2/battle/battle_catapult.cpp index 7604948351a..1699be66892 100644 --- a/src/fheroes2/battle/battle_catapult.cpp +++ b/src/fheroes2/battle/battle_catapult.cpp @@ -150,8 +150,7 @@ int Battle::Catapult::GetTarget( const std::vector & values ) const return static_cast( CAT_MISS ); } else { - const uint32_t * targetId = Rand::Get( targets ); - return targetId ? *targetId : targets.front(); + return Rand::Get( targets ); } } diff --git a/src/fheroes2/battle/battle_troop.cpp b/src/fheroes2/battle/battle_troop.cpp index 6a8c54feadc..51089a08519 100644 --- a/src/fheroes2/battle/battle_troop.cpp +++ b/src/fheroes2/battle/battle_troop.cpp @@ -559,7 +559,7 @@ u32 Battle::Unit::CalculateDamageUnit( const Unit & enemy, double dmg ) const r += Spell( Spell::DRAGONSLAYER ).ExtraValue(); // Attack bonus is 20% to 300% - dmg *= 1 + ( 0 < r ? 0.1f * std::min( r, 20 ) : 0.05f * std::max( r, -16 ) ); + dmg *= 1 + ( 0 < r ? 0.1 * std::min( r, 20 ) : 0.05 * std::max( r, -16 ) ); return static_cast( dmg ) < 1 ? 1 : static_cast( dmg ); } diff --git a/src/fheroes2/castle/mageguild.cpp b/src/fheroes2/castle/mageguild.cpp index f7bcbff413c..ddcda689191 100644 --- a/src/fheroes2/castle/mageguild.cpp +++ b/src/fheroes2/castle/mageguild.cpp @@ -129,7 +129,7 @@ Spell GetUniqueSpellCompatibility( const SpellStorage & spells, const int race, v.push_back( spell ); } - return v.size() ? *Rand::Get( v ) : Spell( Spell::NONE ); + return v.size() ? Rand::Get( v ) : Spell( Spell::NONE ); } Spell GetCombatSpellCompatibility( int race, int lvl ) diff --git a/src/fheroes2/dialog/dialog_armyinfo.cpp b/src/fheroes2/dialog/dialog_armyinfo.cpp index dfa0931dbe9..8a8e5c5bddf 100644 --- a/src/fheroes2/dialog/dialog_armyinfo.cpp +++ b/src/fheroes2/dialog/dialog_armyinfo.cpp @@ -39,6 +39,8 @@ #include "ui_button.h" #include "world.h" +#include + namespace { const int offsetXAmountBox = 80; @@ -67,6 +69,13 @@ namespace int32_t offset; int32_t space; }; + + std::string GetString( const float value, const uint8_t prec ) + { + std::ostringstream stream; + stream << std::setprecision( prec ) << value; + return stream.str(); + } } void DrawMonsterStats( const fheroes2::Point & dst, const Troop & troop ); diff --git a/src/fheroes2/dialog/dialog_selectitems.cpp b/src/fheroes2/dialog/dialog_selectitems.cpp index cb9f53a1ee4..b2df92e1557 100644 --- a/src/fheroes2/dialog/dialog_selectitems.cpp +++ b/src/fheroes2/dialog/dialog_selectitems.cpp @@ -403,7 +403,7 @@ int Dialog::SelectHeroes( int cur ) Cursor & cursor = Cursor::Get(); LocalEvent & le = LocalEvent::Get(); - std::vector heroes( static_cast( Heroes::SANDYSANDY ), Heroes::UNKNOWN ); + std::vector heroes( static_cast( Heroes::DEBUG_HERO ), Heroes::UNKNOWN ); cursor.Hide(); cursor.SetThemes( cursor.POINTER ); diff --git a/src/fheroes2/game/game_static.cpp b/src/fheroes2/game/game_static.cpp index 58c789d8c03..0a41cb13a7c 100644 --- a/src/fheroes2/game/game_static.cpp +++ b/src/fheroes2/game/game_static.cpp @@ -361,7 +361,7 @@ StreamBase & GameStatic::operator>>( StreamBase & msg, const Data & /*obj*/ ) bool GameStatic::isCustomMonsterUpgradeOption() { - return std::fabs( monsterUpgradeRatio - 1.0f ) > 0.001; + return std::fabs( monsterUpgradeRatio - 1.0f ) > 0.001f; } float GameStatic::GetMonsterUpgradeRatio() diff --git a/src/fheroes2/gui/interface_focus.cpp b/src/fheroes2/gui/interface_focus.cpp index 92a6ced836f..3785f3ad5bb 100644 --- a/src/fheroes2/gui/interface_focus.cpp +++ b/src/fheroes2/gui/interface_focus.cpp @@ -177,17 +177,20 @@ Army * Interface::GetFocusArmy() { Player * player = Settings::Get().GetPlayers().GetCurrent(); - if ( player == NULL ) - return NULL; + if ( player == nullptr ) + return nullptr; - if ( player->GetFocus().GetHeroes() ) { - return &player->GetFocus().GetHeroes()->GetArmy(); + Heroes * focusedHero = player->GetFocus().GetHeroes(); + if ( focusedHero != nullptr ) { + return &focusedHero->GetArmy(); } - else if ( player->GetFocus().GetCastle() ) { - return &player->GetFocus().GetCastle()->GetArmy(); + + Castle * focusedCastle = player->GetFocus().GetCastle(); + if ( focusedCastle != nullptr ) { + return &focusedCastle->GetArmy(); } - return NULL; + return nullptr; } Point Interface::GetFocusCenter( void ) diff --git a/src/fheroes2/heroes/heroes.cpp b/src/fheroes2/heroes/heroes.cpp index f49d489b8ae..3e5dd63ea5d 100644 --- a/src/fheroes2/heroes/heroes.cpp +++ b/src/fheroes2/heroes/heroes.cpp @@ -81,7 +81,7 @@ const char * Heroes::GetName( int id ) _( "Solmyr" ), _( "Dainwin" ), _( "Mog" ), _( "Uncle Ivan" ), _( "Joseph" ), _( "Gallavant" ), _( "Elderian" ), _( "Ceallach" ), _( "Drakonia" ), _( "Martine" ), _( "Jarkonas" ), // debug - "SandySandy", "Unknown"}; + "Debug Hero", "Unknown"}; return names[id]; } @@ -284,7 +284,7 @@ Heroes::Heroes( int heroid, int rc ) case JARKONAS: break; - case SANDYSANDY: + case DEBUG_HERO: army.Clean(); army.JoinTroop( Monster::BLACK_DRAGON, 2 ); army.JoinTroop( Monster::RED_DRAGON, 3 ); @@ -1713,9 +1713,9 @@ const fheroes2::Sprite & Heroes::GetPortrait( int id, int type ) case PORT_BIG: return fheroes2::AGG::GetICN( ICN::PORTxxxx( id ), 0 ); case PORT_MEDIUM: - return Heroes::SANDYSANDY > id ? fheroes2::AGG::GetICN( ICN::PORTMEDI, id + 1 ) : fheroes2::AGG::GetICN( ICN::PORTMEDI, BAX + 1 ); + return Heroes::DEBUG_HERO > id ? fheroes2::AGG::GetICN( ICN::PORTMEDI, id + 1 ) : fheroes2::AGG::GetICN( ICN::PORTMEDI, BAX + 1 ); case PORT_SMALL: - return Heroes::SANDYSANDY > id ? fheroes2::AGG::GetICN( ICN::MINIPORT, id ) : fheroes2::AGG::GetICN( ICN::MINIPORT, BAX ); + return Heroes::DEBUG_HERO > id ? fheroes2::AGG::GetICN( ICN::MINIPORT, id ) : fheroes2::AGG::GetICN( ICN::MINIPORT, BAX ); default: break; } @@ -1897,7 +1897,7 @@ void AllHeroes::Init( void ) push_back( new Heroes( loyalty ? Heroes::JARKONAS : Heroes::UNKNOWN, Race::BARB ) ); // devel - push_back( new Heroes( IS_DEVEL() ? Heroes::SANDYSANDY : Heroes::UNKNOWN, Race::WRLK ) ); + push_back( new Heroes( IS_DEVEL() ? Heroes::DEBUG_HERO : Heroes::UNKNOWN, Race::WRLK ) ); push_back( new Heroes( Heroes::UNKNOWN, Race::KNGT ) ); } @@ -2008,7 +2008,7 @@ Heroes * AllHeroes::GetFreeman( int race ) const return NULL; } - return at( *Rand::Get( freeman_heroes ) ); + return at( Rand::Get( freeman_heroes ) ); } void AllHeroes::Scoute( int colors ) const diff --git a/src/fheroes2/heroes/heroes.h b/src/fheroes2/heroes/heroes.h index 19953cc28f0..173109afe84 100644 --- a/src/fheroes2/heroes/heroes.h +++ b/src/fheroes2/heroes/heroes.h @@ -139,7 +139,7 @@ class Heroes : public HeroBase, public ColorBase MARTINE, JARKONAS, // debugger - SANDYSANDY, + DEBUG_HERO, UNKNOWN }; diff --git a/src/fheroes2/heroes/skill.cpp b/src/fheroes2/heroes/skill.cpp index 2bbaa73e7f4..ff8444f2619 100644 --- a/src/fheroes2/heroes/skill.cpp +++ b/src/fheroes2/heroes/skill.cpp @@ -359,7 +359,7 @@ int Skill::Secondary::RandForWitchsHut( void ) v.push_back( WISDOM ); } - return v.empty() ? UNKNOWN : *Rand::Get( v ); + return v.empty() ? UNKNOWN : Rand::Get( v ); } /* index sprite from SECSKILL */ diff --git a/src/fheroes2/monster/monster.cpp b/src/fheroes2/monster/monster.cpp index e67b56d26fb..1a3436bffc9 100644 --- a/src/fheroes2/monster/monster.cpp +++ b/src/fheroes2/monster/monster.cpp @@ -1341,7 +1341,7 @@ Monster Monster::Rand( level_t level ) monstersVec[monster.GetRandomUnitLevel() - LEVEL0 - 1].push_back( monster ); } } - return *Rand::Get( monstersVec[level - LEVEL0 - 1] ); + return Rand::Get( monstersVec[level - LEVEL0 - 1] ); } u32 Monster::Rand4WeekOf( void ) @@ -2036,7 +2036,7 @@ void RandomMonsterAnimation::increment() // make sure both are empty to avoid leftovers in case of mismatch _offsetSet.clear(); - const int moveId = *Rand::Get( _validMoves ); + const int moveId = Rand::Get( _validMoves ); if ( moveId == Monster_Info::STATIC ) { const u32 counter = Rand::Get( 10, 20 ); diff --git a/src/fheroes2/resource/artifact.cpp b/src/fheroes2/resource/artifact.cpp index 36e37c792fa..1dfd3292865 100644 --- a/src/fheroes2/resource/artifact.cpp +++ b/src/fheroes2/resource/artifact.cpp @@ -644,7 +644,7 @@ int Artifact::Rand( level_t lvl ) v.push_back( art ); } - int res = v.size() ? *Rand::Get( v ) : Artifact::UNKNOWN; + int res = v.size() ? Rand::Get( v ) : Artifact::UNKNOWN; artifacts[res].bits |= ART_RNDUSED; return res; diff --git a/src/fheroes2/spell/spell.cpp b/src/fheroes2/spell/spell.cpp index a52ea085468..b5d52a6117b 100644 --- a/src/fheroes2/spell/spell.cpp +++ b/src/fheroes2/spell/spell.cpp @@ -571,7 +571,7 @@ Spell Spell::Rand( int lvl, bool adv ) if ( ( ( adv && !spell.isCombat() ) || ( !adv && spell.isCombat() ) ) && lvl == spell.Level() && spell.isEnabled() ) v.push_back( spell ); } - return v.size() ? *Rand::Get( v ) : Spell( Spell::NONE ); + return v.size() ? Rand::Get( v ) : Spell( Spell::NONE ); } Spell Spell::RandCombat( int lvl ) diff --git a/src/fheroes2/world/world.cpp b/src/fheroes2/world/world.cpp index 3621c0303a3..ce20b3b2945 100644 --- a/src/fheroes2/world/world.cpp +++ b/src/fheroes2/world/world.cpp @@ -508,7 +508,7 @@ void World::pickRumor() const std::string * current = _rumor; while ( current == _rumor ) { // vec_rumors always contain values - _rumor = Rand::Get( vec_rumors ); + _rumor = &Rand::Get( vec_rumors ); } } @@ -668,11 +668,10 @@ s32 World::NextTeleport( s32 index ) const const MapsIndexes teleports = GetTeleportEndPoints( index ); if ( teleports.empty() ) { DEBUG_LOG( DBG_GAME, DBG_WARN, "not found" ); + return index; } - const int32_t * randValue = Rand::Get( teleports ); - - return randValue != nullptr ? *randValue : index; + return Rand::Get( teleports ); } MapsIndexes World::GetWhirlpoolEndPoints( s32 center ) const @@ -700,7 +699,7 @@ MapsIndexes World::GetWhirlpoolEndPoints( s32 center ) const uniqs.push_back( uniq ); } - return uniq_whirlpools[*Rand::Get( uniqs )]; + return uniq_whirlpools[Rand::Get( uniqs )]; } return MapsIndexes(); @@ -712,11 +711,10 @@ s32 World::NextWhirlpool( s32 index ) const const MapsIndexes whilrpools = GetWhirlpoolEndPoints( index ); if ( whilrpools.empty() ) { DEBUG_LOG( DBG_GAME, DBG_WARN, "is full" ); + return index; } - const int32_t * randValue = Rand::Get( whilrpools ); - - return randValue != nullptr ? *randValue : index; + return Rand::Get( whilrpools ); } /* return message from sign */ @@ -865,15 +863,10 @@ std::string World::DateString( void ) const return os.str(); } -bool IsObeliskOnMaps( const Maps::Tiles & tile ) -{ - return MP2::OBJ_OBELISK == tile.GetObject( false ); -} - u32 World::CountObeliskOnMaps( void ) { - u32 res = std::count_if( vec_tiles.begin(), vec_tiles.end(), IsObeliskOnMaps ); - return res ? res : 6; + const size_t res = std::count_if( vec_tiles.begin(), vec_tiles.end(), []( const Maps::Tiles & tile ) { return MP2::OBJ_OBELISK == tile.GetObject( false ); } ); + return res > 0 ? static_cast( res ) : 6; } void World::ActionForMagellanMaps( int color ) diff --git a/src/fheroes2/world/world_loadmap.cpp b/src/fheroes2/world/world_loadmap.cpp index 434bdd37b27..65121f2c479 100644 --- a/src/fheroes2/world/world_loadmap.cpp +++ b/src/fheroes2/world/world_loadmap.cpp @@ -935,7 +935,7 @@ bool World::LoadMapMP2( const std::string & filename ) return false; // endof - const u32 endof_mp2 = fs.size(); + const size_t endof_mp2 = fs.size(); fs.seek( endof_mp2 - 4 ); // read uniq @@ -987,15 +987,15 @@ bool World::LoadMapMP2( const std::string & filename ) return false; } + const int32_t worldSize = w() * h(); + // seek to ADDONS block - fs.skip( w() * h() * SIZEOFMP2TILE ); + fs.skip( worldSize * SIZEOFMP2TILE ); // read all addons std::vector vec_mp2addons( fs.getLE32() /* count mp2addon_t */ ); - for ( std::vector::iterator it = vec_mp2addons.begin(); it != vec_mp2addons.end(); ++it ) { - MP2::mp2addon_t & mp2addon = *it; - + for ( MP2::mp2addon_t & mp2addon : vec_mp2addons ) { mp2addon.indexAddon = fs.getLE16(); mp2addon.objectNameN1 = fs.get() * 2; mp2addon.indexNameN1 = fs.get(); @@ -1007,18 +1007,17 @@ bool World::LoadMapMP2( const std::string & filename ) mp2addon.editorObjectOverlay = fs.getLE32(); } - const u32 endof_addons = fs.tell(); + const size_t endof_addons = fs.tell(); DEBUG_LOG( DBG_GAME, DBG_INFO, "read all tiles addons, tellg: " << endof_addons ); // offset data fs.seek( MP2OFFSETDATA ); - vec_tiles.resize( w() * h() ); + vec_tiles.resize( worldSize ); // read all tiles - for ( MapsTiles::iterator it = vec_tiles.begin(); it != vec_tiles.end(); ++it ) { - const size_t index = std::distance( vec_tiles.begin(), it ); - Maps::Tiles & tile = *it; + for ( int32_t i = 0; i < worldSize; ++i ) { + Maps::Tiles & tile = vec_tiles[i]; MP2::mp2tile_t mp2tile; @@ -1042,7 +1041,7 @@ bool World::LoadMapMP2( const std::string & filename ) case MP2::OBJ_EVENT: case MP2::OBJ_SPHINX: case MP2::OBJ_JAIL: - vec_object.push_back( index ); + vec_object.push_back( i ); break; default: break; @@ -1054,7 +1053,7 @@ bool World::LoadMapMP2( const std::string & filename ) mp2tile.editorObjectLink = fs.getLE32(); mp2tile.editorObjectOverlay = fs.getLE32(); - tile.Init( index, mp2tile ); + tile.Init( i, mp2tile ); // load all addon for current tils while ( offsetAddonsBlock ) { @@ -1534,7 +1533,7 @@ void World::ProcessNewMap() if ( !kingdom.GetCastles().empty() ) { const Castle * castle = kingdom.GetCastles().front(); const Point & cp = castle->GetCenter(); - Heroes * hero = vec_heroes.Get( Heroes::SANDYSANDY ); + Heroes * hero = vec_heroes.Get( Heroes::DEBUG_HERO ); if ( hero && !world.GetTiles( cp.x, cp.y + 1 ).GetHeroes() ) { hero->Recruit( castle->GetColor(), Point( cp.x, cp.y + 1 ) ); @@ -1562,7 +1561,7 @@ void World::ProcessNewMap() } if ( pools.size() ) { - const s32 pos = *Rand::Get( pools ); + const s32 pos = Rand::Get( pools ); ultimate_artifact.Set( pos, Artifact::Rand( Artifact::ART_ULTIMATE ) ); ultimate_pos = Maps::GetPoint( pos ); } @@ -1612,6 +1611,7 @@ void World::ProcessNewMap() vec_rumors.emplace_back( _( "The bones of Lord Slayer are buried in the foundation of the arena." ) ); vec_rumors.emplace_back( _( "A Black Dragon will take out a Titan any day of the week." ) ); vec_rumors.emplace_back( _( "He told her: Yada yada yada... and then she said: Blah, blah, blah..." ) ); + vec_rumors.emplace_back( _( "An unknown force is being ressurected..." ) ); vec_rumors.emplace_back( _( "Check the newest version of game at\nhttps://github.com/ihhub/\nfheroes2/releases" ) ); } From 4fef5444f4da000ece6ad9871472f84c44d57703 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Sun, 21 Feb 2021 18:01:25 +0800 Subject: [PATCH 16/84] Speed up MacOS buils (#2831) --- .github/workflows/osx_pull_request.yml | 10 ++-------- .github/workflows/osx_release.yml | 10 ++-------- src/Makefile.osx | 4 ++++ src/dist/Makefile | 2 +- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/.github/workflows/osx_pull_request.yml b/.github/workflows/osx_pull_request.yml index 577cc9e59a7..4c7e6b7ab2e 100644 --- a/.github/workflows/osx_pull_request.yml +++ b/.github/workflows/osx_pull_request.yml @@ -13,10 +13,7 @@ jobs: fetch-depth: 50 - name: install SDL 1 run: | - brew install sdl - brew install sdl_ttf - brew install sdl_mixer - brew install sdl_image + brew install sdl sdl_ttf sdl_mixer sdl_image - name: compile run: make -j 2 env: @@ -31,10 +28,7 @@ jobs: fetch-depth: 50 - name: install SDL 2 run: | - brew install sdl2 - brew install sdl2_ttf - brew install sdl2_mixer - brew install sdl2_image + brew install sdl2 sdl2_ttf sdl2_mixer sdl2_image - name: compile run: make -j 2 env: diff --git a/.github/workflows/osx_release.yml b/.github/workflows/osx_release.yml index 06da61ab901..c0bc0b6abd5 100644 --- a/.github/workflows/osx_release.yml +++ b/.github/workflows/osx_release.yml @@ -13,10 +13,7 @@ jobs: fetch-depth: 50 - name: install SDL 1 run: | - brew install sdl - brew install sdl_ttf - brew install sdl_mixer - brew install sdl_image + brew install sdl sdl_ttf sdl_mixer sdl_image - name: compile run: make -j 2 env: @@ -45,10 +42,7 @@ jobs: fetch-depth: 50 - name: install SDL 2 run: | - brew install sdl2 - brew install sdl2_ttf - brew install sdl2_mixer - brew install sdl2_image + brew install sdl2 sdl2_ttf sdl2_mixer sdl2_image - name: compile run: make -j 2 env: diff --git a/src/Makefile.osx b/src/Makefile.osx index f34baea23db..79f9d7a3b70 100644 --- a/src/Makefile.osx +++ b/src/Makefile.osx @@ -1,5 +1,9 @@ AR := ar + +ifndef CXX CXX := g++ +endif + LINK := libtool SIMPLE_LINK_FLAGS := -dynamic -undefined suppress -flat_namespace diff --git a/src/dist/Makefile b/src/dist/Makefile index 4d0933218d9..54d8f3b8686 100644 --- a/src/dist/Makefile +++ b/src/dist/Makefile @@ -59,7 +59,7 @@ $(RES): $(ICOFILE) VPATH := $(SRCDIRLIST) %.o: %.cpp - @echo "cxx: $@" + @echo "$(CXX): $@" @$(CXX) -c -MD $(addprefix -I, $(SRCDIRLIST)) $< $(CFLAGS) include $(wildcard *.d) From 904e2ec0b6b5ea254b167bf3148c3d7b2a784871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mantas=20Kriau=C4=8Di=C5=ABnas?= Date: Sun, 21 Feb 2021 10:08:45 +0000 Subject: [PATCH 17/84] Speed up SDL installation via scripts (#2805) --- script/linux/install_sdl_1.sh | 5 +---- script/linux/install_sdl_2.sh | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/script/linux/install_sdl_1.sh b/script/linux/install_sdl_1.sh index 455d0d20346..a27aec5b9a0 100755 --- a/script/linux/install_sdl_1.sh +++ b/script/linux/install_sdl_1.sh @@ -7,7 +7,4 @@ sudo apt-get remove libsdl2-ttf-dev sudo apt-get remove libsdl2-dev # Install SDL 1.2 -sudo apt-get install -y libsdl1.2-dev -sudo apt-get install -y libsdl-ttf2.0-dev -sudo apt-get install -y libsdl-mixer1.2-dev -sudo apt-get install -y libsdl-image1.2-dev +sudo apt-get install -y libsdl1.2-dev libsdl-ttf2.0-dev libsdl-mixer1.2-dev libsdl-image1.2-dev diff --git a/script/linux/install_sdl_2.sh b/script/linux/install_sdl_2.sh index f1bcac568da..6423dc8c6d2 100755 --- a/script/linux/install_sdl_2.sh +++ b/script/linux/install_sdl_2.sh @@ -7,7 +7,4 @@ sudo apt-get remove libsdl-ttf2.0-dev sudo apt-get remove libsdl1.2-dev # Install SDL 2 -sudo apt-get install -y libsdl2-dev -sudo apt-get install -y libsdl2-ttf-dev -sudo apt-get install -y libsdl2-mixer-dev -sudo apt-get install -y libsdl2-image-dev +sudo apt-get install -y libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev libsdl2-image-dev From 7a2d5fab92cd543f72fe4a7b4351d713060fdf93 Mon Sep 17 00:00:00 2001 From: 0UserName <46850587+0UserName@users.noreply.github.com> Date: Sun, 21 Feb 2021 16:09:03 +0300 Subject: [PATCH 18/84] Remember scenario difficulty while restarting or choosing a new scenario (#2796) close #2656 --- src/fheroes2/castle/castle.cpp | 4 ++-- src/fheroes2/dialog/dialog_gameinfo.cpp | 2 +- src/fheroes2/game/game.cpp | 13 ++++++++++++- src/fheroes2/game/game.h | 3 +++ src/fheroes2/game/game_scenarioinfo.cpp | 9 ++++----- src/fheroes2/heroes/heroes.cpp | 2 +- src/fheroes2/kingdom/kingdom.cpp | 4 ++-- src/fheroes2/maps/maps.cpp | 3 +-- src/fheroes2/monster/monster.cpp | 3 ++- src/fheroes2/system/settings.cpp | 3 +-- src/fheroes2/world/world_loadmap.cpp | 4 ++-- 11 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/fheroes2/castle/castle.cpp b/src/fheroes2/castle/castle.cpp index f82ae4b999f..410a3f48f99 100644 --- a/src/fheroes2/castle/castle.cpp +++ b/src/fheroes2/castle/castle.cpp @@ -161,7 +161,7 @@ void Castle::LoadFromMP2( StreamBuf st ) // default building building |= DWELLING_MONSTER1; u32 dwelling2 = 0; - switch ( Settings::Get().GameDifficulty() ) { + switch ( Game::getDifficulty() ) { case Difficulty::EASY: dwelling2 = 75; break; @@ -517,7 +517,7 @@ void Castle::ActionNewWeek( void ) growth += GetGrownWel2(); if ( isControlAI() ) - growth = static_cast( growth * Difficulty::GetUnitGrowthBonus( Settings::Get().GameDifficulty() ) ); + growth = static_cast( growth * Difficulty::GetUnitGrowthBonus( Game::getDifficulty() ) ); // neutral town: half population (normal for begin month) if ( isNeutral && !world.BeginMonth() ) diff --git a/src/fheroes2/dialog/dialog_gameinfo.cpp b/src/fheroes2/dialog/dialog_gameinfo.cpp index 91b4ea0b1fa..5928a139998 100644 --- a/src/fheroes2/dialog/dialog_gameinfo.cpp +++ b/src/fheroes2/dialog/dialog_gameinfo.cpp @@ -69,7 +69,7 @@ void Dialog::GameInfo( void ) text.Set( Difficulty::String( conf.MapsDifficulty() ), Font::SMALL, 80 ); text.Blit( pt.x + 50, pt.y + 80 ); - text.Set( Difficulty::String( conf.GameDifficulty() ), Font::SMALL, 80 ); + text.Set( Difficulty::String( Game::getDifficulty() ), Font::SMALL, 80 ); text.Blit( pt.x + 140, pt.y + 80 ); text.Set( std::to_string( Game::GetRating() ) + " %", Font::SMALL, 80 ); diff --git a/src/fheroes2/game/game.cpp b/src/fheroes2/game/game.cpp index 8abff3f133d..2f19d0e8ce9 100644 --- a/src/fheroes2/game/game.cpp +++ b/src/fheroes2/game/game.cpp @@ -105,6 +105,12 @@ namespace Game } } +// Returns the difficulty level based on the type of game. +int Game::getDifficulty() +{ + return ( ( Settings::Get().GameType() & Game::TYPE_CAMPAIGN ) != 0 ) ? Difficulty::NORMAL : Settings::Get().GameDifficulty(); +} + void Game::LoadPlayers( const std::string & mapFileName, Players & players ) { if ( lastMapFileName != mapFileName || savedPlayers.size() != players.size() ) { @@ -129,6 +135,11 @@ void Game::LoadPlayers( const std::string & mapFileName, Players & players ) } } +void Game::saveDifficulty( const int difficulty ) +{ + Settings::Get().SetGameDifficulty( difficulty ); +} + void Game::SavePlayers( const std::string & mapFileName, const Players & players ) { lastMapFileName = mapFileName; @@ -293,7 +304,7 @@ u32 Game::GetRating( void ) break; } - switch ( conf.GameDifficulty() ) { + switch ( Game::getDifficulty() ) { case Difficulty::NORMAL: rating += 30; break; diff --git a/src/fheroes2/game/game.h b/src/fheroes2/game/game.h index 0009aa5b1e9..9cd587c6be7 100644 --- a/src/fheroes2/game/game.h +++ b/src/fheroes2/game/game.h @@ -267,7 +267,10 @@ namespace Game void OpenHeroesDialog( Heroes & hero, bool updateFocus, bool windowIsGameWorld ); void OpenCastleDialog( Castle & castle, bool updateFocus = true ); std::string GetEncodeString( const std::string & ); + // Returns the difficulty level based on the type of game. + int getDifficulty(); void LoadPlayers( const std::string & mapFileName, Players & players ); + void saveDifficulty( const int difficulty ); void SavePlayers( const std::string & mapFileName, const Players & players ); std::string GetSaveDir(); diff --git a/src/fheroes2/game/game_scenarioinfo.cpp b/src/fheroes2/game/game_scenarioinfo.cpp index 0fbd49ffe74..6e2f82513e8 100644 --- a/src/fheroes2/game/game_scenarioinfo.cpp +++ b/src/fheroes2/game/game_scenarioinfo.cpp @@ -184,7 +184,7 @@ int Game::ScenarioInfo( void ) fheroes2::MovableSprite levelCursor( ngextra ); - switch ( conf.GameDifficulty() ) { + switch ( Game::getDifficulty() ) { case Difficulty::EASY: levelCursor.setPosition( coordDifficulty[0].x, coordDifficulty[0].y ); break; @@ -241,8 +241,7 @@ int Game::ScenarioInfo( void ) playersInfo.resetSelection(); playersInfo.RedrawInfo(); RedrawRatingInfo( rating ); - levelCursor.setPosition( coordDifficulty[1].x, coordDifficulty[1].y ); - conf.SetGameDifficulty( Difficulty::NORMAL ); + levelCursor.setPosition( coordDifficulty[Game::getDifficulty()].x, coordDifficulty[Game::getDifficulty()].y ); // From 0 to 4, see: Difficulty enum buttonOk.draw(); buttonCancel.draw(); } @@ -258,7 +257,7 @@ int Game::ScenarioInfo( void ) else // click ok if ( HotKeyPressEvent( EVENT_DEFAULT_READY ) || le.MouseClickLeft( buttonOk.area() ) ) { - DEBUG_LOG( DBG_GAME, DBG_INFO, "select maps: " << conf.MapsFile() << ", difficulty: " << Difficulty::String( conf.GameDifficulty() ) ); + DEBUG_LOG( DBG_GAME, DBG_INFO, "select maps: " << conf.MapsFile() << ", difficulty: " << Difficulty::String( Game::getDifficulty() ) ); result = STARTGAME; break; } @@ -270,7 +269,7 @@ int Game::ScenarioInfo( void ) cursor.Hide(); levelCursor.setPosition( coordDifficulty[index].x, coordDifficulty[index].y ); levelCursor.redraw(); - conf.SetGameDifficulty( index ); + Game::saveDifficulty( index ); RedrawRatingInfo( rating ); cursor.Show(); display.render(); diff --git a/src/fheroes2/heroes/heroes.cpp b/src/fheroes2/heroes/heroes.cpp index 3e5dd63ea5d..c4e6c2ab97a 100644 --- a/src/fheroes2/heroes/heroes.cpp +++ b/src/fheroes2/heroes/heroes.cpp @@ -695,7 +695,7 @@ u32 Heroes::GetMaxMovePoints( void ) const point += acount * 500; if ( isControlAI() ) { - point += Difficulty::GetHeroMovementBonus( Settings::Get().GameDifficulty() ); + point += Difficulty::GetHeroMovementBonus( Game::getDifficulty() ); } return point; diff --git a/src/fheroes2/kingdom/kingdom.cpp b/src/fheroes2/kingdom/kingdom.cpp index 416e719cd86..22613a6252a 100644 --- a/src/fheroes2/kingdom/kingdom.cpp +++ b/src/fheroes2/kingdom/kingdom.cpp @@ -101,7 +101,7 @@ int Kingdom::GetRace( void ) const void Kingdom::UpdateStartingResource( void ) { - resource = GetKingdomStartingResources( Settings::Get().GameDifficulty(), isControlAI() ); + resource = GetKingdomStartingResources( Game::getDifficulty(), isControlAI() ); } bool Kingdom::isLoss( void ) const @@ -597,7 +597,7 @@ Funds Kingdom::GetIncome( int type /* INCOME_ALL */ ) const } if ( isControlAI() ) { - totalIncome.gold = static_cast( totalIncome.gold * Difficulty::GetGoldIncomeBonus( Settings::Get().GameDifficulty() ) ); + totalIncome.gold = static_cast( totalIncome.gold * Difficulty::GetGoldIncomeBonus( Game::getDifficulty() ) ); } return totalIncome; diff --git a/src/fheroes2/maps/maps.cpp b/src/fheroes2/maps/maps.cpp index 1e91b055669..033392000b3 100644 --- a/src/fheroes2/maps/maps.cpp +++ b/src/fheroes2/maps/maps.cpp @@ -333,12 +333,11 @@ void Maps::ClearFog( s32 index, int scoute, int color ) { if ( 0 != scoute && isValidAbsIndex( index ) ) { const Point center = GetPoint( index ); - const Settings & conf = Settings::Get(); // AI advantage const bool isAIPlayer = world.GetKingdom( color ).isControlAI(); if ( isAIPlayer ) { - scoute += Difficulty::GetScoutingBonus( conf.GameDifficulty() ); + scoute += Difficulty::GetScoutingBonus( Game::getDifficulty() ); } const int alliedColors = Players::GetPlayerFriends( color ); diff --git a/src/fheroes2/monster/monster.cpp b/src/fheroes2/monster/monster.cpp index 1a3436bffc9..857498f9744 100644 --- a/src/fheroes2/monster/monster.cpp +++ b/src/fheroes2/monster/monster.cpp @@ -24,6 +24,7 @@ #include "castle.h" #include "difficulty.h" +#include "game.h" #include "game_static.h" #include "icn.h" #include "logging.h" @@ -735,7 +736,7 @@ u32 Monster::GetRNDSize( bool skip_factor ) const if ( !skip_factor && Settings::Get().ExtWorldNeutralArmyDifficultyScaling() ) { uint32_t factor = 100; - switch ( Settings::Get().GameDifficulty() ) { + switch ( Game::getDifficulty() ) { case Difficulty::EASY: factor = 80; break; diff --git a/src/fheroes2/system/settings.cpp b/src/fheroes2/system/settings.cpp index 983ed3cdae4..5d90a73dd63 100644 --- a/src/fheroes2/system/settings.cpp +++ b/src/fheroes2/system/settings.cpp @@ -812,8 +812,6 @@ void Settings::SetCurrentFileInfo( const Maps::FileInfo & fi ) players.Init( current_maps_file ); - // game difficulty - game_difficulty = Difficulty::NORMAL; preferably_count_players = 0; } @@ -1210,6 +1208,7 @@ void Settings::SetGameDifficulty( int d ) { game_difficulty = d; } + void Settings::SetCurrentColor( int color ) { players.current_color = color; diff --git a/src/fheroes2/world/world_loadmap.cpp b/src/fheroes2/world/world_loadmap.cpp index 65121f2c479..5d47688e17a 100644 --- a/src/fheroes2/world/world_loadmap.cpp +++ b/src/fheroes2/world/world_loadmap.cpp @@ -180,7 +180,7 @@ TiXmlElement & operator>>( TiXmlElement & doc, Castle & town ) if ( 1 != custom3 && 1 != custom2 ) { town.building |= DWELLING_MONSTER1; u32 dwelling2 = 0; - switch ( Settings::Get().GameDifficulty() ) { + switch ( Game::getDifficulty() ) { case Difficulty::EASY: dwelling2 = 80; break; @@ -619,7 +619,7 @@ TiXmlElement & operator>>( TiXmlElement & doc, MapMonster & obj ) int mul = 4; // set random count - switch ( Settings::Get().GameDifficulty() ) { + switch ( Game::getDifficulty() ) { case Difficulty::EASY: mul = 3; break; From 0574c98343ca95d520eedeec2fe536240691c205 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Sun, 21 Feb 2021 22:30:25 +0800 Subject: [PATCH 19/84] Fix overlapped battleground objects (#2832) close #2667 --- src/fheroes2/battle/battle_board.cpp | 61 +++++++++++++++++----------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/src/fheroes2/battle/battle_board.cpp b/src/fheroes2/battle/battle_board.cpp index 890d41eed9f..22b41be0172 100644 --- a/src/fheroes2/battle/battle_board.cpp +++ b/src/fheroes2/battle/battle_board.cpp @@ -38,13 +38,41 @@ #include "translations.h" #include "world.h" -namespace Battle +namespace { - int GetObstaclePosition( void ) + int GetRandomObstaclePosition() { return Rand::Get( 3, 6 ) + ( 11 * Rand::Get( 1, 7 ) ); } + bool isTwoHexObject( const int icnId ) + { + switch ( icnId ) { + case ICN::COBJ0004: + case ICN::COBJ0005: + case ICN::COBJ0007: + case ICN::COBJ0011: + case ICN::COBJ0014: + case ICN::COBJ0015: + case ICN::COBJ0017: + case ICN::COBJ0018: + case ICN::COBJ0019: + case ICN::COBJ0020: + case ICN::COBJ0022: + case ICN::COBJ0030: + case ICN::COBJ0031: + return true; + + default: + break; + } + + return false; + } +} + +namespace Battle +{ bool IsLeftDirection( const int32_t startCellId, const int32_t endCellId, const bool prevLeftDirection ) { const int startX = startCellId % ARENAW; @@ -779,9 +807,12 @@ void Battle::Board::SetCobjObjects( const Maps::Tiles & tile ) std::random_shuffle( objs.begin(), objs.end() ); for ( size_t i = 0; i < objectsToPlace; ++i ) { - s32 dest = GetObstaclePosition(); - while ( at( dest ).GetObject() ) - dest = GetObstaclePosition(); + const bool checkRightCell = isTwoHexObject( objs[i] ); + + int32_t dest = GetRandomObstaclePosition(); + while ( at( dest ).GetObject() != 0 && ( !checkRightCell || at( dest + 1 ).GetObject() != 0 ) ) { + dest = GetRandomObstaclePosition(); + } SetCobjObject( objs[i], dest ); } @@ -791,25 +822,9 @@ void Battle::Board::SetCobjObject( int icn, s32 dst ) { at( dst ).SetObject( 0x80 + ( icn - ICN::COBJ0000 ) ); - switch ( icn ) { - case ICN::COBJ0004: - case ICN::COBJ0005: - case ICN::COBJ0007: - case ICN::COBJ0011: - case ICN::COBJ0014: - case ICN::COBJ0015: - case ICN::COBJ0017: - case ICN::COBJ0018: - case ICN::COBJ0019: - case ICN::COBJ0020: - case ICN::COBJ0022: - case ICN::COBJ0030: - case ICN::COBJ0031: + if ( isTwoHexObject( icn ) ) { + assert( at( dst + 1 ).GetObject() == 0 ); at( dst + 1 ).SetObject( 0x40 ); - break; - - default: - break; } } From 63bc39bea7245327ea987459cd13ab66faa34491 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Sun, 21 Feb 2021 22:30:37 +0800 Subject: [PATCH 20/84] Fix autosave option logic (#2833) close #1483 --- src/fheroes2/dialog/dialog_settings.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/fheroes2/dialog/dialog_settings.cpp b/src/fheroes2/dialog/dialog_settings.cpp index 5bdbc2a7001..be9ef266e5c 100644 --- a/src/fheroes2/dialog/dialog_settings.cpp +++ b/src/fheroes2/dialog/dialog_settings.cpp @@ -118,10 +118,9 @@ void SettingsListBox::ActionListSingleClick( u32 & item ) break; case Settings::GAME_AUTOSAVE_BEGIN_DAY: - if ( conf.ExtModes( Settings::GAME_AUTOSAVE_BEGIN_DAY ) ) + if ( conf.ExtModes( Settings::GAME_AUTOSAVE_BEGIN_DAY ) ) { conf.ExtSetModes( Settings::GAME_AUTOSAVE_ON ); - else - conf.ExtResetModes( Settings::GAME_AUTOSAVE_ON ); + } break; case Settings::WORLD_NEW_VERSION_WEEKOF: From 655667b7164e7c0c959a5bdcd1fc4a810bdefdcf Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Sun, 21 Feb 2021 23:01:51 +0800 Subject: [PATCH 21/84] Reduce income window area in towns (#2835) close #2825 --- src/fheroes2/castle/castle_dialog.cpp | 2 +- src/fheroes2/castle/castle_town.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fheroes2/castle/castle_dialog.cpp b/src/fheroes2/castle/castle_dialog.cpp index fdf278e3e07..ef1caf78229 100644 --- a/src/fheroes2/castle/castle_dialog.cpp +++ b/src/fheroes2/castle/castle_dialog.cpp @@ -277,7 +277,7 @@ int Castle::OpenDialog( bool readonly ) // resource const Rect & rectResource = RedrawResourcePanel( cur_pt ); - const fheroes2::Rect resActiveArea( rectResource.x, rectResource.y, rectResource.w, buttonExit.area().y - rectResource.y ); + const fheroes2::Rect resActiveArea( rectResource.x, rectResource.y, rectResource.w, buttonExit.area().y - rectResource.y - 3 ); // button swap SwapButton buttonSwap( cur_pt.x + 4, cur_pt.y + 345 ); diff --git a/src/fheroes2/castle/castle_town.cpp b/src/fheroes2/castle/castle_town.cpp index a67f57be959..3d49bd02613 100644 --- a/src/fheroes2/castle/castle_town.cpp +++ b/src/fheroes2/castle/castle_town.cpp @@ -411,7 +411,7 @@ u32 Castle::OpenTown( void ) // redraw resource panel const Rect & rectResource = RedrawResourcePanel( cur_pt ); - const fheroes2::Rect resActiveArea( rectResource.x, rectResource.y, rectResource.w, buttonExit.area().y - rectResource.y ); + const fheroes2::Rect resActiveArea( rectResource.x, rectResource.y, rectResource.w, buttonExit.area().y - rectResource.y - 3 ); cursor.Show(); display.render(); From 853e74e42d494d9db4cda95381a8f6528a6568f6 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Sun, 21 Feb 2021 23:06:48 +0800 Subject: [PATCH 22/84] Fix compilation with SDL image (#2834) close #2748 --- Makefile | 2 +- VisualStudio/SDL2.props | 2 +- src/Makefile | 2 +- src/engine/image_tool.cpp | 5 ++++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index e08c47778f0..cf46181c1fa 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ # WITHOUT_MIXER: build without SDL_mixer library # WITHOUT_AUDIOCD: disable audio CD support # WITHOUT_UNICODE: build without unicode (disable translation and ttf font) -# WITHOUT_IMAGE: build without SDL_image library (disable cache image, icn2png) +# FHEROES2_IMAGE_SUPPORT: build with SDL image support # WITHOUT_XML: skip build tinyxml, used for load alt. resources # WITH_TOOLS: build tools # WITHOUT_BUNDLED_LIBS: do not build XML third party library diff --git a/VisualStudio/SDL2.props b/VisualStudio/SDL2.props index 5ab9ecb6169..3422b900d98 100644 --- a/VisualStudio/SDL2.props +++ b/VisualStudio/SDL2.props @@ -6,7 +6,7 @@ $(MSBuildThisFileDirectory)..\..\sdl2\include;%(AdditionalIncludeDirectories) - WITH_TTF;%(PreprocessorDefinitions) + WITH_TTF;FHEROES2_IMAGE_SUPPORT;%(PreprocessorDefinitions) $(MSBuildThisFileDirectory)..\..\sdl2\lib/$(PlatformTarget);%(AdditionalLibraryDirectories) diff --git a/src/Makefile b/src/Makefile index 64eeac557b5..2343b8f6dc9 100644 --- a/src/Makefile +++ b/src/Makefile @@ -41,7 +41,7 @@ SDL_LIBS := $(SDL_LIBS) -lSDL2_mixer endif endif -ifndef WITHOUT_IMAGE +ifdef FHEROES2_IMAGE_SUPPORT CFLAGS := $(CFLAGS) -DWITH_IMAGE $(shell libpng-config --cflags) -DWITH_ZLIB ifdef FHEROES2_SDL1 SDL_LIBS := $(SDL_LIBS) -lSDL_image $(shell libpng-config --libs) -lz diff --git a/src/engine/image_tool.cpp b/src/engine/image_tool.cpp index 65523229231..73b8be1a3ce 100644 --- a/src/engine/image_tool.cpp +++ b/src/engine/image_tool.cpp @@ -24,9 +24,12 @@ #include #include +#if defined( FHEROES2_IMAGE_SUPPORT ) #if SDL_VERSION_ATLEAST( 2, 0, 0 ) +#define FHEROES2_ENABLE_PNG 1 #include #endif +#endif namespace { @@ -76,7 +79,7 @@ namespace } } -#if SDL_VERSION_ATLEAST( 2, 0, 0 ) +#if defined( FHEROES2_ENABLE_PNG ) int res = 0; const std::string pngExtension( ".png" ); if ( path.size() > pngExtension.size() && path.compare( path.size() - pngExtension.size(), pngExtension.size(), pngExtension ) ) { From 7fceaf3c1fffc52bd274c0aef9d54c91bd078f50 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Sun, 21 Feb 2021 23:36:13 +0800 Subject: [PATCH 23/84] Fix fog drawings (#2836) close #2135 --- src/fheroes2/maps/maps_tiles.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fheroes2/maps/maps_tiles.cpp b/src/fheroes2/maps/maps_tiles.cpp index c6243cbec90..8a089c5a412 100644 --- a/src/fheroes2/maps/maps_tiles.cpp +++ b/src/fheroes2/maps/maps_tiles.cpp @@ -2374,7 +2374,7 @@ void Maps::Tiles::RedrawFogs( fheroes2::Image & dst, int color, const Interface: // TIL::CLOF32 if ( DIRECTION_ALL == around ) { - const fheroes2::Image & sf = fheroes2::AGG::GetTIL( TIL::CLOF32, GetIndex() % 4, 0 ); + const fheroes2::Image & sf = fheroes2::AGG::GetTIL( TIL::CLOF32, ( mp.x + mp.y ) % 4, 0 ); gameArea.DrawTile( dst, sf, mp ); } else { @@ -2620,7 +2620,7 @@ void Maps::Tiles::RedrawFogs( fheroes2::Image & dst, int color, const Interface: // unknown else { DEBUG_LOG( DBG_GAME, DBG_WARN, "Invalid direction for fog: " << around ); - const fheroes2::Image & sf = fheroes2::AGG::GetTIL( TIL::CLOF32, GetIndex() % 4, 0 ); + const fheroes2::Image & sf = fheroes2::AGG::GetTIL( TIL::CLOF32, ( mp.x + mp.y ) % 4, 0 ); gameArea.DrawTile( dst, sf, mp ); return; } From c049b1fd53701ad8e5a8d86a07ba1dde951f2161 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Mon, 22 Feb 2021 00:04:22 +0800 Subject: [PATCH 24/84] Fix incorrect castle focus while visiting by a hero (#2837) close #1965 --- src/fheroes2/game/game_startgame.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/fheroes2/game/game_startgame.cpp b/src/fheroes2/game/game_startgame.cpp index b6d0fe17b07..c104e402e6f 100644 --- a/src/fheroes2/game/game_startgame.cpp +++ b/src/fheroes2/game/game_startgame.cpp @@ -127,6 +127,8 @@ void Game::OpenCastleDialog( Castle & castle, bool updateFocus /*= true*/ ) { Mixer::Pause(); + const bool updateCastleFocus = ( Interface::GetFocusType() == GameFocus::CASTLE ); + const Settings & conf = Settings::Get(); Kingdom & myKingdom = world.GetKingdom( conf.CurrentColor() ); const KingdomCastles & myCastles = myKingdom.GetCastles(); @@ -163,7 +165,7 @@ void Game::OpenCastleDialog( Castle & castle, bool updateFocus /*= true*/ ) if ( heroCountBefore < myKingdom.GetHeroes().size() ) { basicInterface.SetFocus( myKingdom.GetHeroes()[heroCountBefore] ); } - else if ( it != myCastles.end() ) { + else if ( it != myCastles.end() && updateCastleFocus ) { basicInterface.SetFocus( *it ); } } From 55c3731d71e6e9968eb0101a918a0b4163b66d2c Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Mon, 22 Feb 2021 00:20:41 +0800 Subject: [PATCH 25/84] Fix well's max button (#2838) close #1795 --- src/fheroes2/castle/castle_well.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/fheroes2/castle/castle_well.cpp b/src/fheroes2/castle/castle_well.cpp index 3bf68c72c2b..2f1b34c2757 100644 --- a/src/fheroes2/castle/castle_well.cpp +++ b/src/fheroes2/castle/castle_well.cpp @@ -216,7 +216,8 @@ void Castle::WellRedrawInfoArea( const Point & cur_pt, const std::vector Date: Mon, 22 Feb 2021 00:30:31 +0800 Subject: [PATCH 26/84] Fix post Daemon Cave music (#2839) close #1393 --- src/fheroes2/heroes/heroes_action.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/fheroes2/heroes/heroes_action.cpp b/src/fheroes2/heroes/heroes_action.cpp index 4a5a448579f..3f4770e611b 100644 --- a/src/fheroes2/heroes/heroes_action.cpp +++ b/src/fheroes2/heroes/heroes_action.cpp @@ -2819,6 +2819,8 @@ void ActionToDaemonCave( Heroes & hero, u32 obj, s32 dst_index ) hero.SetVisited( dst_index, Visit::GLOBAL ); } + AGG::PlayMusic( MUS::FromGround( tile.GetGround() ) ); + DEBUG_LOG( DBG_GAME, DBG_INFO, hero.GetName() ); } From 09a9f9fe6bc6fa31fefdc743ebd41b1dd987984a Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Mon, 22 Feb 2021 00:47:28 +0800 Subject: [PATCH 27/84] Don't show waiting cursor for an exhausted hero (#2840) close #1267 --- src/fheroes2/gui/interface_events.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/fheroes2/gui/interface_events.cpp b/src/fheroes2/gui/interface_events.cpp index d6dc37d8f08..4a4195d5453 100644 --- a/src/fheroes2/gui/interface_events.cpp +++ b/src/fheroes2/gui/interface_events.cpp @@ -84,7 +84,9 @@ void Interface::Basic::ShowPathOrStartMoveHero( Heroes * hero, s32 destinationId RedrawFocus(); hero->SetMove( true ); - Cursor::Get().SetThemes( Cursor::WAIT ); + if ( hero->MayStillMove() ) { + Cursor::Get().SetThemes( Cursor::WAIT ); + } } } From 295939d1ef6dc3d5a486fafa509846682a6e0f5e Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Mon, 22 Feb 2021 00:56:54 +0800 Subject: [PATCH 28/84] Add middle resolution status support (#2841) close #1164 --- src/fheroes2/gui/interface_status.cpp | 38 +++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/fheroes2/gui/interface_status.cpp b/src/fheroes2/gui/interface_status.cpp index c273c9c4444..4e7f378ba9f 100644 --- a/src/fheroes2/gui/interface_status.cpp +++ b/src/fheroes2/gui/interface_status.cpp @@ -137,6 +137,28 @@ void Interface::StatusWindow::Redraw( void ) DrawResourceInfo( 2 * stonHeight + 10 ); } } + else if ( StatusType::STATUS_UNKNOWN != _state && pos.h >= ( stonHeight * 2 + 15 ) ) { + DrawDayInfo(); + + switch ( _state ) { + case StatusType::STATUS_FUNDS: + DrawKingdomInfo( stonHeight + 5 ); + break; + case StatusType::STATUS_DAY: + case StatusType::STATUS_ARMY: + DrawArmyInfo( stonHeight + 5 ); + break; + case StatusType::STATUS_RESOURCE: + DrawResourceInfo( stonHeight + 5 ); + break; + case StatusType::STATUS_UNKNOWN: + case StatusType::STATUS_AITURN: + assert( 0 ); // we shouldn't even reach this code + break; + default: + break; + } + } else { switch ( _state ) { case StatusType::STATUS_DAY: @@ -164,12 +186,24 @@ void Interface::StatusWindow::Redraw( void ) void Interface::StatusWindow::NextState( void ) { + const int32_t areaHeight = GetArea().h; + const fheroes2::Sprite & ston = fheroes2::AGG::GetICN( Settings::Get().ExtGameEvilInterface() ? ICN::STONBAKE : ICN::STONBACK, 0 ); + const int32_t stonHeight = ston.height(); + + const bool skipDayStatus = areaHeight >= ( stonHeight * 2 + 15 ) && areaHeight < ( stonHeight * 3 + 15 ); + if ( StatusType::STATUS_DAY == _state ) _state = StatusType::STATUS_FUNDS; else if ( StatusType::STATUS_FUNDS == _state ) _state = ( GameFocus::UNSEL == GetFocusType() ? StatusType::STATUS_DAY : StatusType::STATUS_ARMY ); - else if ( StatusType::STATUS_ARMY == _state ) - _state = StatusType::STATUS_DAY; + else if ( StatusType::STATUS_ARMY == _state ) { + if ( skipDayStatus ) { + _state = StatusType::STATUS_FUNDS; + } + else { + _state = StatusType::STATUS_DAY; + } + } else if ( StatusType::STATUS_RESOURCE == _state ) _state = StatusType::STATUS_ARMY; From cfc7d9d4cc9b65477d06c85526adef2e536ed7cf Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Mon, 22 Feb 2021 20:18:21 +0800 Subject: [PATCH 29/84] Fix save loading crash for broken saves (#2845) --- src/engine/zzlib.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/engine/zzlib.cpp b/src/engine/zzlib.cpp index d0c2eebdcb1..96159bf43fb 100644 --- a/src/engine/zzlib.cpp +++ b/src/engine/zzlib.cpp @@ -90,7 +90,13 @@ bool ZStreamFile::read( const std::string & fn, size_t offset ) sf.seek( offset ); #ifdef WITH_ZLIB const u32 size0 = sf.get32(); // raw size + if ( size0 == 0 ) { + return false; + } const u32 size1 = sf.get32(); // zip size + if ( size1 == 0 ) { + return false; + } sf.skip( 4 ); // old stream format std::vector zip = sf.getRaw( size1 ); std::vector raw = zlibDecompress( &zip[0], zip.size(), size0 ); @@ -98,6 +104,9 @@ bool ZStreamFile::read( const std::string & fn, size_t offset ) seek( 0 ); #else const u32 size0 = sf.get32(); // raw size + if ( size0 == 0 ) { + return false; + } std::vector raw = sf.getRaw( size0 ); putRaw( &raw[0], raw.size() ); seek( 0 ); From 352d84afbe31713610232f106be4bcf4e7111858 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Mon, 22 Feb 2021 20:40:13 +0800 Subject: [PATCH 30/84] Change logic in monster hiring window (#2844) close #1744 close #838 close #1743 --- src/engine/image.cpp | 41 +++++++- src/engine/image.h | 2 + src/fheroes2/agg/agg.cpp | 22 ++++ src/fheroes2/agg/icn.h | 2 + src/fheroes2/dialog/dialog_recrut.cpp | 140 +++++++++++++++++++------- 5 files changed, 164 insertions(+), 43 deletions(-) diff --git a/src/engine/image.cpp b/src/engine/image.cpp index 75477aea012..f368e2df907 100644 --- a/src/engine/image.cpp +++ b/src/engine/image.cpp @@ -1470,7 +1470,7 @@ namespace fheroes2 if ( horizontally && !vertically ) { const uint8_t * imageInY = in.image() + width - 1; - const uint8_t * transformInY = out.transform() + width - 1; + const uint8_t * transformInY = in.transform() + width - 1; for ( ; imageOutY != imageOutYEnd; imageOutY += width, transformOutY += width, imageInY += width, transformInY += width ) { uint8_t * imageOutX = imageOutY; uint8_t * transformOutX = transformOutY; @@ -1485,16 +1485,18 @@ namespace fheroes2 } } else if ( !horizontally && vertically ) { - const uint8_t * imageInY = in.image() + ( height - 1 ) * width; - const uint8_t * transformInY = out.transform() + ( height - 1 ) * width; + const int32_t offsetIn = ( height - 1 ) * width; + const uint8_t * imageInY = in.image() + offsetIn; + const uint8_t * transformInY = in.transform() + offsetIn; for ( ; imageOutY != imageOutYEnd; imageOutY += width, transformOutY += width, imageInY -= width, transformInY -= width ) { memcpy( imageOutY, imageInY, static_cast( width ) ); memcpy( transformOutY, transformInY, static_cast( width ) ); } } else { - const uint8_t * imageInY = in.image() + ( height - 1 ) * width + width - 1; - const uint8_t * transformInY = out.transform() + ( height - 1 ) * width + width - 1; + const int32_t offsetIn = ( height - 1 ) * width + width - 1; + const uint8_t * imageInY = in.image() + offsetIn; + const uint8_t * transformInY = in.transform() + offsetIn; for ( ; imageOutY != imageOutYEnd; imageOutY += width, transformOutY += width, imageInY -= width, transformInY -= width ) { uint8_t * imageOutX = imageOutY; uint8_t * transformOutX = transformOutY; @@ -1807,4 +1809,33 @@ namespace fheroes2 return out; } + + void Transpose( const Image & in, Image & out ) + { + if ( in.empty() || out.empty() || in.width() != out.height() || in.height() != out.width() ) + return; + + const int32_t width = in.width(); + const int32_t height = in.height(); + + const uint8_t * imageInY = in.image(); + const uint8_t * imageInYEnd = imageInY + width * height; + uint8_t * imageOutX = out.image(); + + const uint8_t * transformInY = in.transform(); + uint8_t * transformOutX = out.transform(); + + for ( ; imageInY != imageInYEnd; imageInY += width, transformInY += width, ++imageOutX, ++transformOutX ) { + const uint8_t * imageInX = imageInY; + const uint8_t * imageInXEnd = imageInX + width; + uint8_t * imageOutY = imageOutX; + + const uint8_t * transformInX = transformInY; + uint8_t * transformOutY = transformOutX; + for ( ; imageInX != imageInXEnd; ++imageInX, ++transformInX, imageOutY += height, transformOutY += height ) { + *imageOutY = *imageInX; + *transformOutY = *transformInX; + } + } + } } diff --git a/src/engine/image.h b/src/engine/image.h index fa55080a627..cc3dfd8d05e 100644 --- a/src/engine/image.h +++ b/src/engine/image.h @@ -251,4 +251,6 @@ namespace fheroes2 void SetTransformPixel( Image & image, int32_t x, int32_t y, uint8_t value ); Image Stretch( const Image & in, int32_t inX, int32_t inY, int32_t widthIn, int32_t heightIn, int32_t widthOut, int32_t heightOut ); + + void Transpose( const Image & in, Image & out ); } diff --git a/src/fheroes2/agg/agg.cpp b/src/fheroes2/agg/agg.cpp index c16d06ba2c7..b52c552a3cc 100644 --- a/src/fheroes2/agg/agg.cpp +++ b/src/fheroes2/agg/agg.cpp @@ -1310,6 +1310,28 @@ namespace fheroes2 modified = temp; } return true; + case ICN::MONSTER_SWITCH_LEFT_ARROW: + _icnVsSprite[id].resize( 2 ); + for ( uint32_t i = 0; i < 2; ++i ) { + const Sprite & source = GetICN( ICN::RECRUIT, i ); + Sprite & out = _icnVsSprite[id][i]; + out.resize( source.height(), source.width() ); + Transpose( source, out ); + out = Flip( out, false, true ); + out.setPosition( source.y(), source.x() ); + } + return true; + case ICN::MONSTER_SWITCH_RIGHT_ARROW: + _icnVsSprite[id].resize( 2 ); + for ( uint32_t i = 0; i < 2; ++i ) { + const Sprite & source = GetICN( ICN::RECRUIT, i + 2 ); + Sprite & out = _icnVsSprite[id][i]; + out.resize( source.height(), source.width() ); + Transpose( source, out ); + out = Flip( out, false, true ); + out.setPosition( source.y(), source.x() ); + } + return true; default: break; } diff --git a/src/fheroes2/agg/icn.h b/src/fheroes2/agg/icn.h index d2826227334..c5afea3f374 100644 --- a/src/fheroes2/agg/icn.h +++ b/src/fheroes2/agg/icn.h @@ -916,6 +916,8 @@ namespace ICN TROLL2MSL, LISTBOX_EVIL, // alias to LISTBOX, but black and white colored + MONSTER_SWITCH_LEFT_ARROW, + MONSTER_SWITCH_RIGHT_ARROW, LASTICN, // just a marker, indicating end of the enumeration }; diff --git a/src/fheroes2/dialog/dialog_recrut.cpp b/src/fheroes2/dialog/dialog_recrut.cpp index a1d976367df..15d61dd6def 100644 --- a/src/fheroes2/dialog/dialog_recrut.cpp +++ b/src/fheroes2/dialog/dialog_recrut.cpp @@ -31,6 +31,8 @@ #include "text.h" #include "world.h" +#include + void RedrawCurrentInfo( const fheroes2::Point & pos, u32 result, const payment_t & paymentMonster, const payment_t & paymentCosts, const Funds & funds, const std::string & label ) { @@ -68,7 +70,7 @@ void RedrawResourceInfo( const fheroes2::Image & sres, const fheroes2::Point & p text.Blit( dst_pt.x, dst_pt.y ); } -void RedrawMonsterInfo( const fheroes2::Rect & pos, const Monster & monster, u32 available, bool label, bool showTotalSum ) +void RedrawMonsterInfo( const fheroes2::Rect & pos, const Monster & monster, u32 available, bool showTotalSum ) { fheroes2::Display & display = fheroes2::Display::instance(); const payment_t paymentMonster = monster.GetCost(); @@ -93,17 +95,20 @@ void RedrawMonsterInfo( const fheroes2::Rect & pos, const Monster & monster, u32 text.Blit( dst_pt.x, dst_pt.y ); // sprite monster - const fheroes2::Sprite & smon = fheroes2::AGG::GetICN( monster.ICNMonh(), 0 ); - dst_pt.x = pos.x + 27 + smon.x(); - dst_pt.y = pos.y + 130 - smon.height(); - fheroes2::Blit( smon, display, dst_pt.x, dst_pt.y ); + const int monsterId = monster.GetID(); + const Bin_Info::MonsterAnimInfo & monsterInfo = Bin_Info::GetMonsterInfo( monsterId ); + assert( !monsterInfo.animationFrames[Bin_Info::MonsterAnimInfo::STATIC].empty() ); - // change label - if ( label ) { - text.Set( "( change )", Font::YELLOW_SMALL ); - text.Blit( pos.x + 68 - text.w() / 2, pos.y + 80 ); + const fheroes2::Sprite & smon = fheroes2::AGG::GetICN( Monster::GetICNByMonsterID( monsterId ), monsterInfo.animationFrames[Bin_Info::MonsterAnimInfo::STATIC][0] ); + dst_pt.x = pos.x + 80 + smon.x() - ( monster.isWide() ? 22 : 0 ); + dst_pt.y = pos.y + 135 - smon.height(); + + if ( monsterId == Monster::CHAMPION ) { + ++dst_pt.x; } + fheroes2::Blit( smon, display, dst_pt.x, dst_pt.y ); + // info resource // gold const fheroes2::Sprite & sgold = fheroes2::AGG::GetICN( ICN::RESOURCE, 6 ); @@ -172,14 +177,14 @@ void RedrawMonsterInfo( const fheroes2::Rect & pos, const Monster & monster, u32 str = _( "Available: %{count}" ); StringReplace( str, "%{count}", available ); text.Set( str, Font::SMALL ); - text.Blit( pos.x + 70 - text.w() / 2, pos.y + 130 ); + text.Blit( pos.x + 80 - text.w() / 2, pos.y + 135 ); } -void RedrawStaticInfo( const fheroes2::Rect & pos, const Monster & monster, u32 available, bool label ) +void RedrawStaticInfo( const fheroes2::Rect & pos, const Monster & monster, u32 available ) { fheroes2::Blit( fheroes2::AGG::GetICN( ICN::RECRBKG, 0 ), fheroes2::Display::instance(), pos.x, pos.y ); - RedrawMonsterInfo( pos, monster, available, label, true ); + RedrawMonsterInfo( pos, monster, available, true ); // text number buy Text text; @@ -246,8 +251,7 @@ Troop Dialog::RecruitMonster( const Monster & monster0, u32 available, bool ext fheroes2::Blit( boxShadow, display, pos.x - BORDERWIDTH, pos.y + BORDERWIDTH ); fheroes2::Blit( box, display, pos.x, pos.y ); - const fheroes2::Rect rtChange( pos.x + 25, pos.y + 35, 85, 95 ); - RedrawStaticInfo( pos, monster, available, ext && monster0.GetDowngrade() != monster0 ); + RedrawStaticInfo( pos, monster, available ); // buttons fheroes2::Point dst_pt; @@ -275,6 +279,27 @@ Troop Dialog::RecruitMonster( const Monster & monster0, u32 available, bool ext const fheroes2::Rect rtWheel( pos.x + 130, pos.y + 155, 100, 30 ); + // Create monster switching arrows + fheroes2::ButtonSprite monsterSwitchLeft; + fheroes2::ButtonSprite monsterSwitchRight; + + if ( ext && monster0.GetDowngrade() != monster0 ) { + monsterSwitchLeft.setSprite( fheroes2::AGG::GetICN( ICN::MONSTER_SWITCH_LEFT_ARROW, 0 ), fheroes2::AGG::GetICN( ICN::MONSTER_SWITCH_LEFT_ARROW, 1 ) ); + monsterSwitchRight.setSprite( fheroes2::AGG::GetICN( ICN::MONSTER_SWITCH_RIGHT_ARROW, 0 ), fheroes2::AGG::GetICN( ICN::MONSTER_SWITCH_RIGHT_ARROW, 1 ) ); + + monsterSwitchLeft.setPosition( pos.x + 24, pos.y + 80 ); + monsterSwitchRight.setPosition( pos.x + 121, pos.y + 80 ); + } + else { + monsterSwitchLeft.hide(); + monsterSwitchRight.hide(); + + monsterSwitchLeft.disable(); + monsterSwitchRight.disable(); + } + + const fheroes2::Rect monsterArea( pos.x + 40, pos.y + 35, 75, 95 ); + if ( 0 == result ) { buttonOk.disable(); buttonMax.disable(); @@ -294,12 +319,19 @@ Troop Dialog::RecruitMonster( const Monster & monster0, u32 available, bool ext buttonMin.draw(); buttonUp.draw(); buttonDn.draw(); + monsterSwitchLeft.draw(); + monsterSwitchRight.draw(); cursor.Show(); display.render(); bool redraw = false; + std::vector upgrades = {monster0}; + while ( upgrades.back().GetDowngrade() != upgrades.back() ) { + upgrades.emplace_back( upgrades.back().GetDowngrade() ); + } + // str loop while ( le.HandleEvents() ) { if ( buttonOk.isEnabled() ) @@ -308,39 +340,67 @@ Troop Dialog::RecruitMonster( const Monster & monster0, u32 available, bool ext le.MousePressLeft( buttonUp.area() ) ? buttonUp.drawOnPress() : buttonUp.drawOnRelease(); le.MousePressLeft( buttonDn.area() ) ? buttonDn.drawOnPress() : buttonDn.drawOnRelease(); + le.MousePressLeft( monsterSwitchLeft.area() ) ? monsterSwitchLeft.drawOnPress() : monsterSwitchLeft.drawOnRelease(); + le.MousePressLeft( monsterSwitchRight.area() ) ? monsterSwitchRight.drawOnPress() : monsterSwitchRight.drawOnRelease(); + if ( buttonMax.isEnabled() ) le.MousePressLeft( buttonMax.area() ) ? buttonMax.drawOnPress() : buttonMax.drawOnRelease(); if ( buttonMin.isEnabled() ) le.MousePressLeft( buttonMin.area() ) ? buttonMin.drawOnPress() : buttonMin.drawOnRelease(); - if ( ext && le.MouseClickLeft( rtChange ) ) { - if ( monster != monster.GetDowngrade() ) { - monster = monster.GetDowngrade(); - max = CalculateMax( monster, kingdom, available ); - result = max; - paymentMonster = monster.GetCost(); - paymentCosts = paymentMonster * result; - redraw = true; + bool updateCost = false; + if ( ext && upgrades.size() > 1 ) { + if ( le.MouseClickLeft( monsterSwitchLeft.area() ) || le.KeyPress( KEY_LEFT ) ) { + for ( size_t i = 0; i < upgrades.size(); ++i ) { + if ( upgrades[i] == monster ) { + if ( i < upgrades.size() - 1 ) { + monster = upgrades[i + 1]; + } + else { + monster = upgrades[0]; + } + break; + } + } + updateCost = true; } - else if ( monster != monster0 ) { - monster = monster0; - max = CalculateMax( monster, kingdom, available ); - result = max; - paymentMonster = monster.GetCost(); - paymentCosts = paymentMonster * result; - redraw = true; + else if ( le.MouseClickLeft( monsterSwitchRight.area() ) || le.KeyPress( KEY_RIGHT ) ) { + for ( size_t i = 0; i < upgrades.size(); ++i ) { + if ( upgrades[i] == monster ) { + if ( i > 0 ) { + monster = upgrades[i - 1]; + } + else { + monster = upgrades.back(); + } + break; + } + } + updateCost = true; } + } - if ( result == max ) { - maxmin = SwitchMaxMinButtons( buttonMax, buttonMin, true ); - } + if ( updateCost ) { + max = CalculateMax( monster, kingdom, available ); + result = max; + paymentMonster = monster.GetCost(); + paymentCosts = paymentMonster * result; + redraw = true; + maxmin = SwitchMaxMinButtons( buttonMax, buttonMin, true ); } - if ( le.MousePressRight( rtChange ) ) { + bool skipEventCheck = false; + if ( le.MousePressRight( monsterArea ) ) { const bool isUpgradedMonster = ext && ( monster != monster.GetDowngrade() ); Dialog::ArmyInfo( Troop( isUpgradedMonster ? monster : monster.GetDowngrade(), available ), Dialog::READONLY ); redraw = true; } + else if ( le.MouseClickLeft( monsterArea ) ) { + const bool isUpgradedMonster = ext && ( monster != monster.GetDowngrade() ); + Dialog::ArmyInfo( Troop( isUpgradedMonster ? monster : monster.GetDowngrade(), available ), Dialog::READONLY | Dialog::BUTTONS ); + redraw = true; + skipEventCheck = true; + } if ( PressIntKey( max, result ) ) { paymentCosts = paymentMonster * result; @@ -355,7 +415,7 @@ Troop Dialog::RecruitMonster( const Monster & monster0, u32 available, bool ext } } - if ( ( le.MouseWheelUp( rtWheel ) || le.MouseClickLeft( buttonUp.area() ) ) && result < max ) { + if ( ( le.MouseWheelUp( rtWheel ) || le.MouseClickLeft( buttonUp.area() ) || le.KeyPress( KEY_UP ) ) && result < max ) { ++result; paymentCosts += paymentMonster; redraw = true; @@ -368,7 +428,7 @@ Troop Dialog::RecruitMonster( const Monster & monster0, u32 available, bool ext maxmin = SwitchMaxMinButtons( buttonMax, buttonMin, false ); } } - else if ( ( le.MouseWheelDn( rtWheel ) || le.MouseClickLeft( buttonDn.area() ) ) && result ) { + else if ( ( le.MouseWheelDn( rtWheel ) || le.MouseClickLeft( buttonDn.area() ) || le.KeyPress( KEY_DOWN ) ) && result ) { --result; paymentCosts -= paymentMonster; redraw = true; @@ -396,7 +456,7 @@ Troop Dialog::RecruitMonster( const Monster & monster0, u32 available, bool ext if ( redraw ) { cursor.Hide(); - RedrawStaticInfo( pos, monster, available, ext && monster0.GetDowngrade() != monster0 ); + RedrawStaticInfo( pos, monster, available ); RedrawCurrentInfo( fheroes2::Point( pos.x, pos.y ), result, paymentMonster, paymentCosts, funds, maxmin ); if ( 0 == result ) { @@ -412,6 +472,10 @@ Troop Dialog::RecruitMonster( const Monster & monster0, u32 available, bool ext buttonMax.draw(); if ( buttonMin.isEnabled() ) buttonMin.draw(); + + monsterSwitchLeft.draw(); + monsterSwitchRight.draw(); + cursor.Show(); display.render(); redraw = false; @@ -420,7 +484,7 @@ Troop Dialog::RecruitMonster( const Monster & monster0, u32 available, bool ext if ( buttonOk.isEnabled() && ( le.MouseClickLeft( buttonOk.area() ) || Game::HotKeyPressEvent( Game::EVENT_DEFAULT_READY ) ) ) break; - if ( le.MouseClickLeft( buttonCancel.area() ) || Game::HotKeyPressEvent( Game::EVENT_DEFAULT_EXIT ) ) { + if ( le.MouseClickLeft( buttonCancel.area() ) || ( Game::HotKeyPressEvent( Game::EVENT_DEFAULT_EXIT ) && !skipEventCheck ) ) { result = 0; break; } @@ -461,7 +525,7 @@ void Dialog::DwellingInfo( const Monster & monster, u32 available ) LocalEvent & le = LocalEvent::Get(); - RedrawMonsterInfo( pos, monster, available, false, false ); + RedrawMonsterInfo( pos, monster, available, false ); cursor.Show(); display.render(); From 6c357d90f15077c3b8d4407cfae9c752e09f607e Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Mon, 22 Feb 2021 20:40:29 +0800 Subject: [PATCH 31/84] Focus on hero after exiting castle window (#2846) close #1965 --- src/fheroes2/game/game_startgame.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/fheroes2/game/game_startgame.cpp b/src/fheroes2/game/game_startgame.cpp index c104e402e6f..22af7bd422c 100644 --- a/src/fheroes2/game/game_startgame.cpp +++ b/src/fheroes2/game/game_startgame.cpp @@ -166,7 +166,13 @@ void Game::OpenCastleDialog( Castle & castle, bool updateFocus /*= true*/ ) basicInterface.SetFocus( myKingdom.GetHeroes()[heroCountBefore] ); } else if ( it != myCastles.end() && updateCastleFocus ) { - basicInterface.SetFocus( *it ); + Heroes * heroInCastle = world.GetTiles( ( *it )->GetIndex() ).GetHeroes(); + if ( heroInCastle == nullptr ) { + basicInterface.SetFocus( *it ); + } + else { + basicInterface.SetFocus( heroInCastle ); + } } } basicInterface.RedrawFocus(); From 63dc2c768601cd9ddd5b017717b771564a6cd9a0 Mon Sep 17 00:00:00 2001 From: Pavel <53114202+shprotru@users.noreply.github.com> Date: Mon, 22 Feb 2021 16:36:45 +0300 Subject: [PATCH 32/84] Castle screen, animation of emerging boat always goes simultaneously with new building appeared (#2847) close #2816 --- src/fheroes2/castle/castle_building.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fheroes2/castle/castle_building.cpp b/src/fheroes2/castle/castle_building.cpp index 972f558df81..f1cc907ca90 100644 --- a/src/fheroes2/castle/castle_building.cpp +++ b/src/fheroes2/castle/castle_building.cpp @@ -210,7 +210,7 @@ void CastleRedrawCurrentBuilding( const Castle & castle, const Point & dst_pt, c if ( castle.isBuild( currentBuildId ) ) { CastleDialog::CastleRedrawBuilding( castle, dst_pt, currentBuildId, frame ); - if ( currentBuildId == BUILD_SHIPYARD ) { + if ( currentBuildId == BUILD_SHIPYARD && fadeBuilding.GetBuild() == BUILD_SHIPYARD ) { CastleDialog::CastleRedrawBuildingExtended( castle, dst_pt, currentBuildId, frame, fadeBuilding.GetAlpha() ); } else { From d200009e76eb3d98287eb137d4747a26d789312c Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Mon, 22 Feb 2021 21:44:44 +0800 Subject: [PATCH 33/84] Fix missing last lost hero condition reset (#2848) close #2843 --- src/fheroes2/castle/castle_town.cpp | 4 +++- src/fheroes2/kingdom/kingdom.cpp | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/fheroes2/castle/castle_town.cpp b/src/fheroes2/castle/castle_town.cpp index 3d49bd02613..7e529d0b17d 100644 --- a/src/fheroes2/castle/castle_town.cpp +++ b/src/fheroes2/castle/castle_town.cpp @@ -348,7 +348,9 @@ u32 Castle::OpenTown( void ) Kingdom & kingdom = GetKingdom(); Heroes * hero1 = kingdom.GetRecruits().GetHero1(); - Heroes * hero2 = kingdom.GetLastLostHero() && kingdom.GetLastLostHero() != hero1 ? kingdom.GetLastLostHero() : kingdom.GetRecruits().GetHero2(); + + Heroes * lastLostHero = kingdom.GetLastLostHero(); + Heroes * hero2 = lastLostHero && lastLostHero != hero1 ? lastLostHero : kingdom.GetRecruits().GetHero2(); std::string not_allow1_msg, not_allow2_msg; const bool allow_buy_hero1 = hero1 ? AllowBuyHero( *hero1, ¬_allow1_msg ) : false; diff --git a/src/fheroes2/kingdom/kingdom.cpp b/src/fheroes2/kingdom/kingdom.cpp index 22613a6252a..b54720f0c57 100644 --- a/src/fheroes2/kingdom/kingdom.cpp +++ b/src/fheroes2/kingdom/kingdom.cpp @@ -82,6 +82,8 @@ void Kingdom::clear( void ) heroes_cond_loss.clear(); puzzle_maps.reset(); + + ResetLastLostHero(); } int Kingdom::GetControl( void ) const From 23c65ee72f32c48fb9dbb7fc854894f823a55598 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Wed, 24 Feb 2021 20:28:17 +0800 Subject: [PATCH 34/84] Remove unused exception class (#2852) --- fheroes2.vcxproj | 4 +--- src/engine/error.cpp | 35 ---------------------------- src/engine/error.h | 32 ------------------------- src/engine/image_tool.cpp | 9 +++---- src/engine/localevent.cpp | 2 -- src/engine/tools.cpp | 1 - src/engine/zzlib.cpp | 1 - src/fheroes2/agg/agg.cpp | 1 - src/fheroes2/game/fheroes2.cpp | 8 +++---- src/fheroes2/world/world_loadmap.cpp | 1 - 10 files changed, 7 insertions(+), 87 deletions(-) delete mode 100644 src/engine/error.cpp delete mode 100644 src/engine/error.h diff --git a/fheroes2.vcxproj b/fheroes2.vcxproj index 4031d3f66ef..9ddf22cb3ea 100644 --- a/fheroes2.vcxproj +++ b/fheroes2.vcxproj @@ -177,7 +177,6 @@ - @@ -384,7 +383,6 @@ - @@ -541,4 +539,4 @@ - + \ No newline at end of file diff --git a/src/engine/error.cpp b/src/engine/error.cpp deleted file mode 100644 index 81c3abd6a8c..00000000000 --- a/src/engine/error.cpp +++ /dev/null @@ -1,35 +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 - -#include "error.h" -#include "system.h" - -/* exception */ -void Error::Except( const char *, const char * ) -{ -#ifndef ANDROID - throw Exception(); -#endif -} diff --git a/src/engine/error.h b/src/engine/error.h deleted file mode 100644 index 55772461bc6..00000000000 --- a/src/engine/error.h +++ /dev/null @@ -1,32 +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. * - ***************************************************************************/ -#ifndef H2ERROR_H -#define H2ERROR_H - -struct Error -{ - class Exception - {}; - static void Except( const char * func, const char * message ); -}; - -#endif diff --git a/src/engine/image_tool.cpp b/src/engine/image_tool.cpp index 73b8be1a3ce..fd1a77c8d79 100644 --- a/src/engine/image_tool.cpp +++ b/src/engine/image_tool.cpp @@ -35,12 +35,9 @@ namespace { std::vector PALPAlette() { - std::vector palette; - if ( palette.empty() ) { - palette.resize( 256 * 3 ); - for ( size_t i = 0; i < palette.size(); ++i ) { - palette[i] = kb_pal[i] << 2; - } + std::vector palette( 256 * 3 ); + for ( size_t i = 0; i < palette.size(); ++i ) { + palette[i] = kb_pal[i] << 2; } return palette; diff --git a/src/engine/localevent.cpp b/src/engine/localevent.cpp index 04109d026fe..524367d103c 100644 --- a/src/engine/localevent.cpp +++ b/src/engine/localevent.cpp @@ -23,7 +23,6 @@ #include "localevent.h" #include "audio_mixer.h" #include "audio_music.h" -#include "error.h" #include "pal.h" #include "screen.h" @@ -1099,7 +1098,6 @@ bool LocalEvent::HandleEvents( bool delay, bool allowExit ) #if SDL_VERSION_ATLEAST( 2, 0, 0 ) case SDL_WINDOWEVENT_CLOSE: #endif - // Error::Except(__FUNCTION__, "SDL_QUIT"); if ( allowExit ) return false; // try to perform clear exit to catch all memory leaks, for example break; diff --git a/src/engine/tools.cpp b/src/engine/tools.cpp index a13ae57749a..58bcdafcc7a 100644 --- a/src/engine/tools.cpp +++ b/src/engine/tools.cpp @@ -31,7 +31,6 @@ #include #include -#include "error.h" #include "logging.h" #include "tools.h" diff --git a/src/engine/zzlib.cpp b/src/engine/zzlib.cpp index 96159bf43fb..13fa5e05cdb 100644 --- a/src/engine/zzlib.cpp +++ b/src/engine/zzlib.cpp @@ -25,7 +25,6 @@ #include #include -#include "error.h" #include "logging.h" #include "zzlib.h" diff --git a/src/fheroes2/agg/agg.cpp b/src/fheroes2/agg/agg.cpp index b52c552a3cc..83cbfb44d95 100644 --- a/src/fheroes2/agg/agg.cpp +++ b/src/fheroes2/agg/agg.cpp @@ -39,7 +39,6 @@ #include "audio_music.h" #include "dir.h" #include "engine.h" -#include "error.h" #include "font.h" #include "game.h" #include "image_tool.h" diff --git a/src/fheroes2/game/fheroes2.cpp b/src/fheroes2/game/fheroes2.cpp index a6ac02d7942..a0bf17a6cf3 100644 --- a/src/fheroes2/game/fheroes2.cpp +++ b/src/fheroes2/game/fheroes2.cpp @@ -32,7 +32,6 @@ #include "dir.h" #include "embedded_image.h" #include "engine.h" -#include "error.h" #include "game.h" #include "game_interface.h" #include "game_video.h" @@ -260,11 +259,10 @@ int main( int argc, char ** argv ) } } } -#ifndef ANDROID - catch ( const Error::Exception & ) { - VERBOSE_LOG( std::endl << conf.String() ); + catch ( const std::exception & ex ) { + ERROR_LOG( "Exception '" << ex.what() << "' occured during application runtime." ); } -#endif + fheroes2::Display::instance().release(); return EXIT_SUCCESS; diff --git a/src/fheroes2/world/world_loadmap.cpp b/src/fheroes2/world/world_loadmap.cpp index 5d47688e17a..09a86cc642c 100644 --- a/src/fheroes2/world/world_loadmap.cpp +++ b/src/fheroes2/world/world_loadmap.cpp @@ -27,7 +27,6 @@ #include "artifact.h" #include "castle.h" #include "difficulty.h" -#include "error.h" #include "game.h" #include "game_over.h" #include "game_static.h" From 0d9bfff76fe9925fa1e9b7ef36165fd89d302ee7 Mon Sep 17 00:00:00 2001 From: vasilenkoalexey Date: Wed, 24 Feb 2021 16:33:09 +0300 Subject: [PATCH 35/84] Fix object fading animation (#2641) close #2441 close #2442 --- src/fheroes2/game/game.cpp | 94 ++++++++++++++++++------- src/fheroes2/game/game.h | 26 ++++--- src/fheroes2/game/game_startgame.cpp | 51 ++++++++++---- src/fheroes2/gui/interface_gamearea.cpp | 59 ++++++++-------- src/fheroes2/heroes/heroes_action.cpp | 2 +- src/fheroes2/heroes/heroes_spell.cpp | 5 +- src/fheroes2/maps/maps_tiles.cpp | 19 +++-- src/fheroes2/maps/maps_tiles.h | 2 +- 8 files changed, 169 insertions(+), 89 deletions(-) diff --git a/src/fheroes2/game/game.cpp b/src/fheroes2/game/game.cpp index 2f19d0e8ce9..eed2812f754 100644 --- a/src/fheroes2/game/game.cpp +++ b/src/fheroes2/game/game.cpp @@ -77,31 +77,34 @@ namespace Game namespace ObjectFadeAnimation { - Info::Info() - : object( MP2::OBJ_ZERO ) - , index( 0 ) - , tile( 0 ) - , alpha( 255 ) - , isFadeOut( true ) - {} - - Info::Info( u8 object_, u8 index_, s32 tile_, u32 alpha_, bool fadeOut ) + FadeTask::FadeTask( uint8_t object_, uint32_t objectIndex_, uint32_t animationIndex_, uint32_t fromIndex_, uint32_t toIndex_, uint32_t alpha_, bool fadeOut_, + bool fadeIn_, uint8_t objectTileset_ ) : object( object_ ) - , tile( tile_ ) + , objectIndex( objectIndex_ ) + , animationIndex( animationIndex_ ) + , fromIndex( fromIndex_ ) + , toIndex( toIndex_ ) , alpha( alpha_ ) - , isFadeOut( fadeOut ) - { - const fheroes2::Image & tileImage = world.GetTiles( tile_ ).GetTileSurface(); - surfaceSize.width = tileImage.width(); - surfaceSize.height = tileImage.height(); - - index = ICN::AnimationFrame( MP2::GetICNObject( object ), index_, 0 ); - if ( 0 == index ) { - index = index_; - } - } + , fadeOut( fadeOut_ ) + , fadeIn( fadeIn_ ) + , objectTileset( objectTileset_ ) + {} - Info removeInfo; + FadeTask::FadeTask() + : object( MP2::OBJ_ZERO ) + , objectIndex( 0 ) + , animationIndex( 0 ) + , fromIndex( 0 ) + , toIndex( 0 ) + , alpha( 0 ) + , fadeOut( false ) + , fadeIn( false ) + , objectTileset( 0 ) + + {} + + // Single instance of FadeTask. + FadeTask fadeTask; } } @@ -225,14 +228,53 @@ void Game::SetCurrentMusic( int mus ) current_music = mus; } -void Game::ObjectFadeAnimation::Set( const Info & info ) +void Game::ObjectFadeAnimation::FinishFadeTask() +{ + if ( fadeTask.object == MP2::OBJ_ZERO ) { + return; + } + + if ( fadeTask.fadeOut ) { + Maps::Tiles & tile = world.GetTiles( fadeTask.fromIndex ); + if ( tile.GetObject() == fadeTask.object ) { + tile.RemoveObjectSprite(); + tile.SetObject( MP2::OBJ_ZERO ); + } + } + + if ( fadeTask.fadeIn ) { + Maps::Tiles & tile = world.GetTiles( fadeTask.toIndex ); + if ( MP2::OBJ_BOAT == fadeTask.object ) { + tile.setBoat( Direction::RIGHT ); + } + } + + fadeTask.object = MP2::OBJ_ZERO; +} + +void Game::ObjectFadeAnimation::StartFadeTask( uint8_t object, uint32_t fromIndex, uint32_t toIndex, bool fadeOut, bool fadeIn ) { - removeInfo = info; + FinishFadeTask(); + + const Maps::Tiles & fromTile = world.GetTiles( fromIndex ); + const uint32_t alpha = fadeOut ? 255u : 0; + if ( MP2::OBJ_MONSTER == object ) { + const auto & spriteIndicies = Maps::Tiles::GetMonsterSpriteIndices( fromTile, fromTile.QuantityMonster().GetSpriteIndex() ); + fadeTask = FadeTask( object, spriteIndicies.first, spriteIndicies.second, fromIndex, toIndex, alpha, fadeOut, fadeIn, 0 ); + } + else if ( MP2::OBJ_BOAT == object ) { + fadeTask = FadeTask( object, fromTile.GetObjectSpriteIndex(), 0, fromIndex, toIndex, alpha, fadeOut, fadeIn, 0 ); + } + else { + const int icn = MP2::GetICNObject( object ); + const uint32_t animationIndex = ICN::AnimationFrame( icn, fromTile.GetObjectSpriteIndex(), Game::MapsAnimationFrame(), fromTile.GetQuantity2() ); + fadeTask = FadeTask( object, fromTile.GetObjectSpriteIndex(), animationIndex, fromIndex, toIndex, alpha, fadeOut, fadeIn, fromTile.GetObjectTileset() ); + } } -Game::ObjectFadeAnimation::Info & Game::ObjectFadeAnimation::Get() +Game::ObjectFadeAnimation::FadeTask & Game::ObjectFadeAnimation::GetFadeTask() { - return removeInfo; + return fadeTask; } u32 & Game::MapsAnimationFrame( void ) diff --git a/src/fheroes2/game/game.h b/src/fheroes2/game/game.h index 9cd587c6be7..fb222e9eda6 100644 --- a/src/fheroes2/game/game.h +++ b/src/fheroes2/game/game.h @@ -279,21 +279,29 @@ namespace Game namespace ObjectFadeAnimation { - struct Info + struct FadeTask { - Info(); - Info( u8 object_, u8 index_, s32 tile_, u32 alpha_ = 255u, bool fadeOut = true ); + FadeTask(); + + FadeTask( uint8_t object_, uint32_t objectIndex_, uint32_t animationIndex_, uint32_t fromIndex_, uint32_t toIndex_, uint32_t alpha_, bool fadeOut_, + bool fadeIn_, uint8_t objectTileset_ ); uint8_t object; - uint8_t index; - int32_t tile; + uint32_t objectIndex; + uint32_t animationIndex; + uint32_t fromIndex; + uint32_t toIndex; uint32_t alpha; - fheroes2::Size surfaceSize; - bool isFadeOut; + bool fadeOut; + bool fadeIn; + uint8_t objectTileset; }; - void Set( const Info & info ); - Info & Get(); + FadeTask & GetFadeTask(); + + void StartFadeTask( uint8_t object, uint32_t fromTile, uint32_t toTile, bool fadeOut, bool fadeIn ); + + void FinishFadeTask(); } u32 GetStep4Player( u32, u32, u32 ); diff --git a/src/fheroes2/game/game_startgame.cpp b/src/fheroes2/game/game_startgame.cpp index 22af7bd422c..9c001c4b8a8 100644 --- a/src/fheroes2/game/game_startgame.cpp +++ b/src/fheroes2/game/game_startgame.cpp @@ -21,6 +21,7 @@ ***************************************************************************/ #include +#include #include #ifdef AI @@ -993,23 +994,47 @@ int Interface::Basic::HumanTurn( bool isload ) } if ( Game::AnimateInfrequentDelay( Game::HEROES_PICKUP_DELAY ) ) { - Game::ObjectFadeAnimation::Info & fadeInfo = Game::ObjectFadeAnimation::Get(); - if ( fadeInfo.object != MP2::OBJ_ZERO ) { - if ( fadeInfo.isFadeOut && fadeInfo.alpha < 20 ) { - fadeInfo.object = MP2::OBJ_ZERO; + auto & fadeTask = Game::ObjectFadeAnimation::GetFadeTask(); + if ( MP2::OBJ_ZERO != fadeTask.object ) { + if ( fadeTask.fadeOut ) { + if ( fadeTask.alpha > 20 ) { + fadeTask.alpha -= 20; + } + else { + fadeTask.fadeOut = false; + fadeTask.alpha = 0; + Maps::Tiles & tile = world.GetTiles( fadeTask.fromIndex ); + if ( tile.GetObject() == fadeTask.object ) { + tile.RemoveObjectSprite(); + tile.SetObject( MP2::OBJ_ZERO ); + } + + if ( !fadeTask.fadeIn ) { + fadeTask.object = MP2::OBJ_ZERO; + } + } } - else if ( !fadeInfo.isFadeOut && fadeInfo.alpha > 235 ) { - Maps::Tiles & objectTile = world.GetTiles( fadeInfo.tile ); - objectTile.SetObject( fadeInfo.object ); - // TODO: we need to expand the logic to all objects. - if ( fadeInfo.object == MP2::OBJ_BOAT ) { - objectTile.SetObjectSpriteIndex( fadeInfo.index ); + else if ( fadeTask.fadeIn ) { + if ( fadeTask.alpha == 0 ) { + Maps::Tiles & tile = world.GetTiles( fadeTask.toIndex ); + if ( MP2::OBJ_BOAT == fadeTask.object ) { + tile.setBoat( Direction::RIGHT ); + } + } + + if ( fadeTask.alpha < 235 ) { + fadeTask.alpha += 20; + } + else { + fadeTask.fadeIn = false; + fadeTask.alpha = 255; + fadeTask.object = MP2::OBJ_ZERO; } - fadeInfo.object = MP2::OBJ_ZERO; } else { - fadeInfo.alpha += ( fadeInfo.isFadeOut ) ? -20 : 20; + assert( 0 ); // incorrect fading animation setup! } + gameArea.SetRedraw(); } } @@ -1033,6 +1058,8 @@ int Interface::Basic::HumanTurn( bool isload ) _( "%{color} player, you have lost your last town. If you do not conquer another town in next week, you will be eliminated." ) ); } + Game::ObjectFadeAnimation::FinishFadeTask(); + if ( GetFocusHeroes() ) { GetFocusHeroes()->ShowPath( false ); RedrawFocus(); diff --git a/src/fheroes2/gui/interface_gamearea.cpp b/src/fheroes2/gui/interface_gamearea.cpp index 914c5f580a6..97dea9d28a4 100644 --- a/src/fheroes2/gui/interface_gamearea.cpp +++ b/src/fheroes2/gui/interface_gamearea.cpp @@ -197,6 +197,23 @@ void Interface::GameArea::Redraw( fheroes2::Image & dst, int flag, bool isPuzzle } } + const auto & fadeTask = Game::ObjectFadeAnimation::GetFadeTask(); + + // fade out animation for objects only + if ( drawBottom && fadeTask.fadeOut && MP2::OBJ_ZERO != fadeTask.object && MP2::OBJ_BOAT != fadeTask.object && MP2::OBJ_MONSTER != fadeTask.object ) { + const int icn = MP2::GetICNObject( fadeTask.objectTileset ); + const Point & mp = Maps::GetPoint( fadeTask.fromIndex ); + + const fheroes2::Sprite & sprite = fheroes2::AGG::GetICN( icn, fadeTask.objectIndex ); + BlitOnTile( dst, sprite, sprite.x(), sprite.y(), mp, false, fadeTask.alpha ); + + // possible animation + if ( fadeTask.animationIndex ) { + const fheroes2::Sprite & animationSprite = fheroes2::AGG::GetICN( icn, fadeTask.animationIndex ); + BlitOnTile( dst, animationSprite, animationSprite.x(), animationSprite.y(), mp, false, fadeTask.alpha ); + } + } + // Monsters and boats. const bool drawMonstersAndBoats = ( flag & LEVEL_OBJECTS ) && !isPuzzleDraw; if ( drawMonstersAndBoats ) { @@ -205,6 +222,18 @@ void Interface::GameArea::Redraw( fheroes2::Image & dst, int flag, bool isPuzzle world.GetTiles( x, y ).RedrawMonstersAndBoat( dst, tileROI, true, *this ); } } + + // fade out animation for monsters only + if ( MP2::OBJ_MONSTER == fadeTask.object && fadeTask.fadeOut ) { + const Point & mp = Maps::GetPoint( fadeTask.fromIndex ); + const fheroes2::Sprite & sprite = fheroes2::AGG::GetICN( ICN::MINIMON, fadeTask.objectIndex ); + BlitOnTile( dst, sprite, sprite.x() + 16, sprite.y() + TILEWIDTH, mp, false, fadeTask.alpha ); + + if ( fadeTask.animationIndex ) { + const fheroes2::Sprite & animatedSprite = fheroes2::AGG::GetICN( ICN::MINIMON, fadeTask.animationIndex ); + BlitOnTile( dst, animatedSprite, animatedSprite.x() + 16, animatedSprite.y() + TILEWIDTH, mp, false, fadeTask.alpha ); + } + } } // Top layer and heroes. @@ -230,36 +259,6 @@ void Interface::GameArea::Redraw( fheroes2::Image & dst, int flag, bool isPuzzle } } - // Object fade in/fade out animation - const Game::ObjectFadeAnimation::Info & fadeInfo = Game::ObjectFadeAnimation::Get(); - if ( fadeInfo.object != MP2::OBJ_ZERO ) { - const Point & mp = Maps::GetPoint( fadeInfo.tile ); - const int icn = MP2::GetICNObject( fadeInfo.object ); - - if ( icn == ICN::MONS32 ) { - const std::pair monsterIndicies = Maps::Tiles::GetMonsterSpriteIndices( world.GetTiles( fadeInfo.tile ), fadeInfo.index ); - - // base monster sprite - if ( monsterIndicies.first >= 0 ) { - const fheroes2::Sprite & sprite = fheroes2::AGG::GetICN( ICN::MINIMON, monsterIndicies.first ); - BlitOnTile( dst, sprite, sprite.x() + 16, sprite.y() + TILEWIDTH, mp, false, fadeInfo.alpha ); - } - // animated monster part - if ( monsterIndicies.second >= 0 ) { - const fheroes2::Sprite & sprite = fheroes2::AGG::GetICN( ICN::MINIMON, monsterIndicies.second ); - BlitOnTile( dst, sprite, sprite.x() + 16, sprite.y() + TILEWIDTH, mp, false, fadeInfo.alpha ); - } - } - else if ( fadeInfo.object == MP2::OBJ_BOAT ) { - const fheroes2::Sprite & sprite = fheroes2::AGG::GetICN( ICN::BOAT32, fadeInfo.index ); - BlitOnTile( dst, sprite, sprite.x(), sprite.y() + TILEWIDTH - 11, mp, false, fadeInfo.alpha ); - } - else { - const fheroes2::Sprite & sprite = fheroes2::AGG::GetICN( icn, fadeInfo.index ); - BlitOnTile( dst, sprite, sprite.x(), sprite.y(), mp, false, fadeInfo.alpha ); - } - } - for ( const std::pair & hero : heroList ) { hero.second->Redraw( dst, hero.first.x, hero.first.y - 1, tileROI, true, *this ); } diff --git a/src/fheroes2/heroes/heroes_action.cpp b/src/fheroes2/heroes/heroes_action.cpp index 3f4770e611b..5cb82d687fa 100644 --- a/src/fheroes2/heroes/heroes_action.cpp +++ b/src/fheroes2/heroes/heroes_action.cpp @@ -313,7 +313,7 @@ void AnimationRemoveObject( const Maps::Tiles & tile ) if ( tile.GetObject() == MP2::OBJ_ZERO ) return; - Game::ObjectFadeAnimation::Set( Game::ObjectFadeAnimation::Info( tile.GetObjectTileset(), tile.GetObjectSpriteIndex(), tile.GetIndex() ) ); + Game::ObjectFadeAnimation::StartFadeTask( tile.GetObject(), tile.GetIndex(), -1, true, false ); } void RecruitMonsterFromTile( Heroes & hero, Maps::Tiles & tile, const std::string & msg, const Troop & troop, bool remove ) diff --git a/src/fheroes2/heroes/heroes_spell.cpp b/src/fheroes2/heroes/heroes_spell.cpp index a102c1aa9cb..1e2eff97445 100644 --- a/src/fheroes2/heroes/heroes_spell.cpp +++ b/src/fheroes2/heroes/heroes_spell.cpp @@ -414,10 +414,7 @@ bool ActionSpellSummonBoat( const Heroes & hero ) const s32 boat = boats[i]; if ( Maps::isValidAbsIndex( boat ) ) { if ( Rand::Get( 1, 100 ) <= chance ) { - Maps::Tiles & boatFile = world.GetTiles( boat ); - boatFile.RemoveObjectSprite(); - boatFile.SetObject( MP2::OBJ_ZERO ); - Game::ObjectFadeAnimation::Set( Game::ObjectFadeAnimation::Info( MP2::OBJ_BOAT, 18, dst_water, 0, false ) ); + Game::ObjectFadeAnimation::StartFadeTask( MP2::OBJ_BOAT, boat, dst_water, true, true ); return true; } break; diff --git a/src/fheroes2/maps/maps_tiles.cpp b/src/fheroes2/maps/maps_tiles.cpp index 8a089c5a412..10adc0fb5aa 100644 --- a/src/fheroes2/maps/maps_tiles.cpp +++ b/src/fheroes2/maps/maps_tiles.cpp @@ -1534,12 +1534,12 @@ void Maps::Tiles::RedrawMonster( fheroes2::Image & dst, const Rect & visibleTile return; const Monster & monster = QuantityMonster(); - const std::pair spriteIndicies = GetMonsterSpriteIndices( *this, monster.GetSpriteIndex() ); + const std::pair spriteIndicies = GetMonsterSpriteIndices( *this, monster.GetSpriteIndex() ); const fheroes2::Sprite & sprite = fheroes2::AGG::GetICN( ICN::MINIMON, spriteIndicies.first ); area.BlitOnTile( dst, sprite, sprite.x() + 16, sprite.y() + TILEWIDTH, mp ); - if ( spriteIndicies.second != -1 ) { + if ( spriteIndicies.second ) { const fheroes2::Sprite & animatedSprite = fheroes2::AGG::GetICN( ICN::MINIMON, spriteIndicies.second ); area.BlitOnTile( dst, animatedSprite, animatedSprite.x() + 16, animatedSprite.y() + TILEWIDTH, mp ); } @@ -1551,12 +1551,19 @@ void Maps::Tiles::RedrawBoat( fheroes2::Image & dst, const Rect & visibleTileROI if ( visibleTileROI & mp ) { const uint32_t spriteIndex = ( objectIndex == 255 ) ? 18 : objectIndex; + + const Game::ObjectFadeAnimation::FadeTask & fadeTask = Game::ObjectFadeAnimation::GetFadeTask(); + const uint32_t alpha = ( MP2::OBJ_BOAT == fadeTask.object + && ( ( fadeTask.fadeOut && fadeTask.fromIndex == maps_index ) || ( fadeTask.fadeIn && fadeTask.toIndex == maps_index ) ) ) + ? fadeTask.alpha + : 255; + if ( withShadow ) { const fheroes2::Sprite & sprite = fheroes2::AGG::GetICN( ICN::BOATSHAD, spriteIndex % 128 ); - area.BlitOnTile( dst, sprite, sprite.x(), TILEWIDTH + sprite.y() - 11, mp, ( spriteIndex > 128 ) ); + area.BlitOnTile( dst, sprite, sprite.x(), TILEWIDTH + sprite.y() - 11, mp, ( spriteIndex > 128 ), alpha ); } const fheroes2::Sprite & sprite = fheroes2::AGG::GetICN( ICN::BOAT32, spriteIndex % 128 ); - area.BlitOnTile( dst, sprite, sprite.x(), TILEWIDTH + sprite.y() - 11, mp, ( spriteIndex > 128 ) ); + area.BlitOnTile( dst, sprite, sprite.x(), TILEWIDTH + sprite.y() - 11, mp, ( spriteIndex > 128 ), alpha ); } } @@ -2303,7 +2310,7 @@ void Maps::Tiles::UpdateRNDResourceSprite( Tiles & tile ) } } -std::pair Maps::Tiles::GetMonsterSpriteIndices( const Tiles & tile, uint32_t monsterIndex ) +std::pair Maps::Tiles::GetMonsterSpriteIndices( const Tiles & tile, uint32_t monsterIndex ) { const int tileIndex = tile.GetIndex(); int attackerIndex = -1; @@ -2323,7 +2330,7 @@ std::pair Maps::Tiles::GetMonsterSpriteIndices( const Tiles & tile, ui } } - std::pair spriteIndices( monsterIndex * 9, -1 ); + std::pair spriteIndices( monsterIndex * 9, 0 ); // draw attack sprite if ( attackerIndex != -1 && !Settings::Get().ExtWorldOnlyFirstMonsterAttack() ) { diff --git a/src/fheroes2/maps/maps_tiles.h b/src/fheroes2/maps/maps_tiles.h index 35a5a7cef60..8b88105770c 100644 --- a/src/fheroes2/maps/maps_tiles.h +++ b/src/fheroes2/maps/maps_tiles.h @@ -270,7 +270,7 @@ namespace Maps static void UpdateAbandoneMineRightSprite( uint8_t & tileset, uint8_t & index ); static int GetPassable( uint32_t tileset, uint32_t index ); static std::pair ColorRaceFromHeroSprite( uint32_t heroSpriteIndex ); - static std::pair GetMonsterSpriteIndices( const Tiles & tile, uint32_t monsterIndex ); + static std::pair GetMonsterSpriteIndices( const Tiles & tile, uint32_t monsterIndex ); static void PlaceMonsterOnTile( Tiles &, const Monster &, u32 ); static void UpdateAbandoneMineSprite( Tiles & ); static void FixedPreload( Tiles & ); From 4e5b333c0d6c8ac9a3943a0e9754a082fea6858d Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Wed, 24 Feb 2021 23:42:59 +0800 Subject: [PATCH 36/84] Fix missing animation frame for wince (#2855) close #2125 --- src/fheroes2/battle/battle_interface.cpp | 32 ++++++++---------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/src/fheroes2/battle/battle_interface.cpp b/src/fheroes2/battle/battle_interface.cpp index fa5a9e249a9..cc6218a83ae 100644 --- a/src/fheroes2/battle/battle_interface.cpp +++ b/src/fheroes2/battle/battle_interface.cpp @@ -2899,10 +2899,8 @@ void Battle::Interface::RedrawActionWincesKills( TargetsInfo & targets, Unit * a deathColor = defender->GetArmyColor(); } - else + else if ( it->damage ) { // wince animation - if ( it->damage ) { - // wnce animation defender->SwitchAnimation( Monster_Info::WNCE ); AGG::PlaySound( defender->M82Wnce() ); ++finish; @@ -2930,13 +2928,14 @@ void Battle::Interface::RedrawActionWincesKills( TargetsInfo & targets, Unit * a } // targets damage animation loop - while ( le.HandleEvents() && finish != std::count_if( targets.begin(), targets.end(), TargetInfo::isFinishAnimFrame ) ) { + bool finishedAnimation = false; + while ( le.HandleEvents() && !finishedAnimation ) { CheckGlobalEvents( le ); if ( Battle::AnimateInfrequentDelay( Game::BATTLE_FRAME_DELAY ) ) { bool redrawBattleField = false; - if ( attacker != NULL ) { + if ( attacker != nullptr ) { if ( attacker->isFinishAnimFrame() ) { attacker->SwitchAnimation( Monster_Info::STATIC ); } @@ -2945,22 +2944,23 @@ void Battle::Interface::RedrawActionWincesKills( TargetsInfo & targets, Unit * a } redrawBattleField = true; - RedrawPartialStart(); } - - for ( TargetsInfo::iterator it = targets.begin(); it != targets.end(); ++it ) { - if ( ( *it ).defender ) { - if ( !redrawBattleField ) { + else { + for ( TargetsInfo::iterator it = targets.begin(); it != targets.end(); ++it ) { + if ( ( *it ).defender ) { redrawBattleField = true; - RedrawPartialStart(); + break; } } } if ( redrawBattleField ) { + RedrawPartialStart(); RedrawPartialFinish(); } + finishedAnimation = ( finish == std::count_if( targets.begin(), targets.end(), TargetInfo::isFinishAnimFrame ) ); + for ( TargetsInfo::iterator it = targets.begin(); it != targets.end(); ++it ) { if ( ( *it ).defender ) { if ( it->defender->isFinishAnimFrame() && it->defender->GetAnimationState() == Monster_Info::WNCE ) { @@ -2977,16 +2977,6 @@ void Battle::Interface::RedrawActionWincesKills( TargetsInfo & targets, Unit * a // Fade away animation for destroyed mirror images if ( mirrorImages.size() ) RedrawActionRemoveMirrorImage( mirrorImages ); - - // Set to static animation as attacker might still continue its animation - for ( TargetsInfo::iterator it = targets.begin(); it != targets.end(); ++it ) { - Unit * unit = ( *it ).defender; - if ( unit ) { - if ( unit->isFinishAnimFrame() && unit->GetAnimationState() == Monster_Info::WNCE ) { - unit->SwitchAnimation( Monster_Info::STATIC ); - } - } - } } void Battle::Interface::RedrawActionMove( Unit & unit, const Indexes & path ) From 471830c620ac1403779e74809acc2cfae317afaf Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Wed, 24 Feb 2021 23:44:05 +0800 Subject: [PATCH 37/84] Fix multiple code smells (#2853) cleaner code is always better --- src/engine/image.cpp | 27 +++++++++++------ src/engine/tools.cpp | 4 +-- src/fheroes2/agg/bin_info.cpp | 2 +- src/fheroes2/ai/normal/ai_normal_battle.cpp | 2 +- src/fheroes2/campaign/campaign_data.cpp | 4 +-- src/fheroes2/campaign/campaign_data.h | 4 +-- src/fheroes2/dialog/dialog_selectitems.cpp | 2 +- src/fheroes2/game/game_campaign.cpp | 2 +- src/fheroes2/game/game_mainmenu.cpp | 33 ++++++++++++--------- src/fheroes2/gui/cursor.cpp | 10 +++---- src/fheroes2/gui/cursor.h | 8 ++--- src/fheroes2/gui/interface_buttons.cpp | 5 ++-- src/fheroes2/maps/maps_tiles.cpp | 16 ++-------- src/fheroes2/maps/maps_tiles.h | 16 ++++++++-- 14 files changed, 72 insertions(+), 63 deletions(-) diff --git a/src/engine/image.cpp b/src/engine/image.cpp index f368e2df907..980c0935752 100644 --- a/src/engine/image.cpp +++ b/src/engine/image.cpp @@ -347,9 +347,12 @@ namespace for ( uint32_t i = 0; i < 256; ++i, ++correctorX ) { const uint8_t * palette = kb_pal + *correctorX * 3; - const int32_t offsetRed = static_cast( *( palette++ ) ) - static_cast( r ); - const int32_t offsetGreen = static_cast( *( palette++ ) ) - static_cast( g ); - const int32_t offsetBlue = static_cast( *( palette++ ) ) - static_cast( b ); + const int32_t offsetRed = static_cast( *( palette ) ) - static_cast( r ); + ++palette; + const int32_t offsetGreen = static_cast( *( palette ) ) - static_cast( g ); + ++palette; + const int32_t offsetBlue = static_cast( *( palette ) ) - static_cast( b ); + ++palette; const int32_t distance = offsetRed * offsetRed + offsetGreen * offsetGreen + offsetBlue * offsetBlue; if ( minDistance > distance ) { minDistance = distance; @@ -826,9 +829,12 @@ namespace fheroes2 const uint8_t * value = kb_pal; for ( uint32_t i = 0; i < 256; ++i ) { - const uint32_t red = static_cast( *value++ ) * alpha / 255; - const uint32_t green = static_cast( *value++ ) * alpha / 255; - const uint32_t blue = static_cast( *value++ ) * alpha / 255; + const uint32_t red = static_cast( *value ) * alpha / 255; + ++value; + const uint32_t green = static_cast( *value ) * alpha / 255; + ++value; + const uint32_t blue = static_cast( *value ) * alpha / 255; + ++value; palette[i] = GetPALColorId( static_cast( red ), static_cast( green ), static_cast( blue ) ); } @@ -1114,9 +1120,12 @@ namespace fheroes2 for ( ; imageInX != imageInXEnd; ++imageInX ) { const uint8_t * palette = kb_pal + *imageInX * 3; - sumRed += ( *palette++ ); - sumGreen += ( *palette++ ); - sumBlue += ( *palette++ ); + sumRed += ( *palette ); + ++palette; + sumGreen += ( *palette ); + ++palette; + sumBlue += ( *palette ); + ++palette; } } diff --git a/src/engine/tools.cpp b/src/engine/tools.cpp index 58bcdafcc7a..4242f86b22a 100644 --- a/src/engine/tools.cpp +++ b/src/engine/tools.cpp @@ -510,9 +510,9 @@ std::vector decodeBase64( const std::string & src ) u32 size = 3 * src.size() / 4; if ( src[src.size() - 1] == '=' ) - size--; + --size; if ( src[src.size() - 2] == '=' ) - size--; + --size; res.reserve( size ); diff --git a/src/fheroes2/agg/bin_info.cpp b/src/fheroes2/agg/bin_info.cpp index c1a8b35b309..a7d4c99d94b 100644 --- a/src/fheroes2/agg/bin_info.cpp +++ b/src/fheroes2/agg/bin_info.cpp @@ -207,7 +207,7 @@ namespace Bin_Info uint8_t count = data[243 + idx]; if ( count > 16 ) count = 16; // here we need to reset our object - for ( uint8_t frame = 0; frame < count; frame++ ) { + for ( uint8_t frame = 0; frame < count; ++frame ) { anim.push_back( static_cast( data[277 + idx * 16 + frame] ) ); } animationFrames.push_back( anim ); diff --git a/src/fheroes2/ai/normal/ai_normal_battle.cpp b/src/fheroes2/ai/normal/ai_normal_battle.cpp index 69a3f8035d2..7c2b76ed41c 100644 --- a/src/fheroes2/ai/normal/ai_normal_battle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_battle.cpp @@ -584,7 +584,7 @@ namespace AI assert( currentUnit.Modes( SP_BERSERKER ) ); Actions actions; - Board & board = *arena.GetBoard(); + Board & board = *Arena::GetBoard(); const uint32_t currentUnitUID = currentUnit.GetUID(); const std::vector nearestUnits = board.GetNearestTroops( ¤tUnit, std::vector() ); diff --git a/src/fheroes2/campaign/campaign_data.cpp b/src/fheroes2/campaign/campaign_data.cpp index a32fc7f5fc4..f5ada590659 100644 --- a/src/fheroes2/campaign/campaign_data.cpp +++ b/src/fheroes2/campaign/campaign_data.cpp @@ -34,7 +34,7 @@ namespace Campaign , _scenarios() {} - const std::vector CampaignData::getScenariosBefore( const int scenarioID ) const + std::vector CampaignData::getScenariosBefore( const int scenarioID ) const { std::vector scenarioIDs; @@ -62,7 +62,7 @@ namespace Campaign return _scenarios[scenarioID].getNextMaps(); } - const std::vector CampaignData::getStartingScenarios() const + std::vector CampaignData::getStartingScenarios() const { std::vector startingScenarios; diff --git a/src/fheroes2/campaign/campaign_data.h b/src/fheroes2/campaign/campaign_data.h index 9eadc22732c..ac9d72f6be8 100644 --- a/src/fheroes2/campaign/campaign_data.h +++ b/src/fheroes2/campaign/campaign_data.h @@ -53,9 +53,9 @@ namespace Campaign return _scenarios; } - const std::vector getScenariosBefore( const int scenarioID ) const; + std::vector getScenariosBefore( const int scenarioID ) const; const std::vector & getScenariosAfter( const int scenarioID ) const; - const std::vector getStartingScenarios() const; + std::vector getStartingScenarios() const; bool isAllCampaignMapsPresent() const; bool isLastScenario( const int scenarioID ) const; diff --git a/src/fheroes2/dialog/dialog_selectitems.cpp b/src/fheroes2/dialog/dialog_selectitems.cpp index b2df92e1557..ed99a726aa5 100644 --- a/src/fheroes2/dialog/dialog_selectitems.cpp +++ b/src/fheroes2/dialog/dialog_selectitems.cpp @@ -373,7 +373,7 @@ Monster Dialog::SelectMonster( int id ) listbox.SetListContent( monsters ); if ( id != Monster::UNKNOWN ) - listbox.SetCurrent( static_cast( id ) ); + listbox.SetCurrent( id ); listbox.Redraw(); fheroes2::ButtonGroup btnGroups( fheroes2::Rect( area.x, area.y, area.w, area.h ), Dialog::OK | Dialog::CANCEL ); diff --git a/src/fheroes2/game/game_campaign.cpp b/src/fheroes2/game/game_campaign.cpp index ab9db0f4cb4..dbd84354e72 100644 --- a/src/fheroes2/game/game_campaign.cpp +++ b/src/fheroes2/game/game_campaign.cpp @@ -205,7 +205,7 @@ namespace fheroes2::Blit( fheroes2::AGG::GetICN( icnId, iconIdx ), fheroes2::Display::instance(), offset.x + posX, offset.y + posY ); } - void DrawCampaignScenarioIcons( fheroes2::ButtonGroup & buttonGroup, const Campaign::CampaignData campaignData, const fheroes2::Point & top ) + void DrawCampaignScenarioIcons( fheroes2::ButtonGroup & buttonGroup, const Campaign::CampaignData & campaignData, const fheroes2::Point & top ) { fheroes2::Display & display = fheroes2::Display::instance(); diff --git a/src/fheroes2/game/game_mainmenu.cpp b/src/fheroes2/game/game_mainmenu.cpp index 9be916379cc..ece12f8223e 100644 --- a/src/fheroes2/game/game_mainmenu.cpp +++ b/src/fheroes2/game/game_mainmenu.cpp @@ -42,6 +42,17 @@ #define CREDITS_DEFAULT 13 #define QUIT_DEFAULT 17 +namespace +{ + struct ButtonInfo + { + u32 frame; + fheroes2::Button & button; + bool isOver; + bool wasOver; + }; +} + int Game::MainMenu( bool isFirstGameRun ) { Mixer::Pause(); @@ -105,19 +116,13 @@ int Game::MainMenu( bool isFirstGameRun ) u32 lantern_frame = 0; - struct ButtonInfo - { - u32 frame; - fheroes2::Button & button; - bool isOver; - bool wasOver; - } buttons[] = {{NEWGAME_DEFAULT, buttonNewGame, false, false}, - {LOADGAME_DEFAULT, buttonLoadGame, false, false}, - {HIGHSCORES_DEFAULT, buttonHighScores, false, false}, - {CREDITS_DEFAULT, buttonCredits, false, false}, - {QUIT_DEFAULT, buttonQuit, false, false}}; + ButtonInfo buttons[] = {{NEWGAME_DEFAULT, buttonNewGame, false, false}, + {LOADGAME_DEFAULT, buttonLoadGame, false, false}, + {HIGHSCORES_DEFAULT, buttonHighScores, false, false}, + {CREDITS_DEFAULT, buttonCredits, false, false}, + {QUIT_DEFAULT, buttonQuit, false, false}}; - for ( u32 i = 0; le.MouseMotion() && i < ARRAY_COUNT( buttons ); i++ ) { + for ( u32 i = 0; le.MouseMotion() && i < ARRAY_COUNT( buttons ); ++i ) { cursor.Hide(); const fheroes2::Sprite & sprite = fheroes2::AGG::GetICN( ICN::BTNSHNGL, buttons[i].frame ); fheroes2::Blit( sprite, display, sprite.x(), sprite.y() ); @@ -136,7 +141,7 @@ int Game::MainMenu( bool isFirstGameRun ) bool redrawScreen = false; - for ( u32 i = 0; i < ARRAY_COUNT( buttons ); i++ ) { + for ( u32 i = 0; i < ARRAY_COUNT( buttons ); ++i ) { buttons[i].wasOver = buttons[i].isOver; if ( le.MousePressLeft( buttons[i].button.area() ) ) { @@ -152,7 +157,7 @@ int Game::MainMenu( bool isFirstGameRun ) u32 frame = buttons[i].frame; if ( buttons[i].isOver && !buttons[i].wasOver ) - frame++; + ++frame; if ( !redrawScreen ) { cursor.Hide(); diff --git a/src/fheroes2/gui/cursor.cpp b/src/fheroes2/gui/cursor.cpp index c19d27d3b5f..117a00c0f48 100644 --- a/src/fheroes2/gui/cursor.cpp +++ b/src/fheroes2/gui/cursor.cpp @@ -43,8 +43,7 @@ Cursor & Cursor::Get( void ) return _cursor; } -/* get theme cursor */ -int Cursor::Themes( void ) +int Cursor::Themes() const { return SP_ARROW >= theme ? theme : NONE; } @@ -90,8 +89,7 @@ void Cursor::Redraw( s32 x, s32 y ) } } -/* move cursor */ -void Cursor::Move( s32 x, s32 y ) +void Cursor::Move( int32_t x, int32_t y ) const { fheroes2::cursor().setPosition( x + offset_x, y + offset_y ); } @@ -161,12 +159,12 @@ void Cursor::SetOffset( int name, const fheroes2::Point & defaultOffset ) } } -void Cursor::Show( void ) +void Cursor::Show() const { fheroes2::cursor().show( true ); } -void Cursor::Hide( void ) +void Cursor::Hide() const { fheroes2::cursor().show( false ); } diff --git a/src/fheroes2/gui/cursor.h b/src/fheroes2/gui/cursor.h index f947d1db53a..4edad8e034d 100644 --- a/src/fheroes2/gui/cursor.h +++ b/src/fheroes2/gui/cursor.h @@ -138,17 +138,17 @@ class Cursor static int WithoutDistanceThemes( int ); static void Refresh(); - int Themes( void ); + int Themes() const; bool SetThemes( int, bool force = false ); - void Show( void ); - void Hide( void ); + void Show() const; + void Hide() const; bool isVisible( void ) const; private: Cursor(); ~Cursor(); void SetOffset( int name, const fheroes2::Point & defaultOffset ); - void Move( s32, s32 ); + void Move( int32_t x, int32_t y ) const; int theme; s32 offset_x; diff --git a/src/fheroes2/gui/interface_buttons.cpp b/src/fheroes2/gui/interface_buttons.cpp index 74b209fe56e..3216e396195 100644 --- a/src/fheroes2/gui/interface_buttons.cpp +++ b/src/fheroes2/gui/interface_buttons.cpp @@ -152,9 +152,8 @@ int Interface::ButtonsArea::QueueEventProcessing( void ) le.MousePressLeft( fileRect ) ? buttonFile.drawOnPress() : buttonFile.drawOnRelease(); le.MousePressLeft( systemRect ) ? buttonSystem.drawOnPress() : buttonSystem.drawOnRelease(); - if ( Settings::Get().ShowButtons() && - // move border window - BorderWindow::QueueEventProcessing() ) { + if ( Settings::Get().ShowButtons() && BorderWindow::QueueEventProcessing() ) { + // Move border window. No other action is required. } else if ( buttonNextHero.isEnabled() && le.MouseClickLeft( nextHeroRect ) ) { interface.EventNextHero(); diff --git a/src/fheroes2/maps/maps_tiles.cpp b/src/fheroes2/maps/maps_tiles.cpp index 10adc0fb5aa..bdf98cbe2c5 100644 --- a/src/fheroes2/maps/maps_tiles.cpp +++ b/src/fheroes2/maps/maps_tiles.cpp @@ -1268,18 +1268,6 @@ u32 Maps::Tiles::GetObjectUID() const return uniq; } -// Get Tile metadata field #1 (used for things like monster count or resource amount) -int Maps::Tiles::GetQuantity1( void ) const -{ - return quantity1; -} - -// Get Tile metadata field #2 (used for things like animations or resource type ) -int Maps::Tiles::GetQuantity2( void ) const -{ - return quantity2; -} - int Maps::Tiles::GetPassable( void ) const { return tilePassable; @@ -1726,7 +1714,7 @@ std::string Maps::Tiles::String( void ) const << "maps index : " << GetIndex() << ", " << "point: x(" << GetCenter().x << "), y(" << GetCenter().y << ")" << std::endl << "id : " << uniq << std::endl - << "mp2 object : " << static_cast( GetObject() ) << ", (" << MP2::StringObject( GetObject() ) << ")" << std::endl + << "mp2 object : " << GetObject() << ", (" << MP2::StringObject( GetObject() ) << ")" << std::endl << "tileset : " << static_cast( objectTileset ) << ", (" << ICN::GetString( MP2::GetICNObject( objectTileset ) ) << ")" << std::endl << "object index : " << static_cast( objectIndex ) << ", (animated: " << static_cast( hasSpriteAnimation() ) << ")" << std::endl << "region : " << _region << std::endl @@ -1739,7 +1727,7 @@ std::string Maps::Tiles::String( void ) const os << std::endl << "quantity 1 : " << static_cast( quantity1 ) << std::endl << "quantity 2 : " << static_cast( quantity2 ) << std::endl - << "quantity 3 : " << static_cast( GetQuantity3() ) << std::endl; + << "quantity 3 : " << static_cast( quantity3 ) << std::endl; for ( Addons::const_iterator it = addons_level1.begin(); it != addons_level1.end(); ++it ) os << ( *it ).String( 1 ); diff --git a/src/fheroes2/maps/maps_tiles.h b/src/fheroes2/maps/maps_tiles.h index 8b88105770c..a77f9030450 100644 --- a/src/fheroes2/maps/maps_tiles.h +++ b/src/fheroes2/maps/maps_tiles.h @@ -133,11 +133,21 @@ namespace Maps void SetObjectSpriteIndex( const uint8_t index ); u32 GetObjectUID() const; - int GetQuantity1() const; - int GetQuantity2() const; + + // Get Tile metadata field #1 (used for things like monster count or resource amount) + uint8_t GetQuantity1() const + { + return quantity1; + } + + // Get Tile metadata field #2 (used for things like animations or resource type ) + uint8_t GetQuantity2() const + { + return quantity2; + } // Get third field containing Tile metadata (adventure spell ID) - int GetQuantity3() const + uint8_t GetQuantity3() const { return quantity3; } From c1656cfe421e958ecacc950edde109f6e67448ad Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Wed, 24 Feb 2021 23:48:15 +0800 Subject: [PATCH 38/84] Make names more explicit for macOS builds (#2854) --- .../{osx_pull_request.yml => macos_pull_request.yml} | 2 +- .../workflows/{osx_release.yml => macos_release.yml} | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) rename .github/workflows/{osx_pull_request.yml => macos_pull_request.yml} (98%) rename .github/workflows/{osx_release.yml => macos_release.yml} (78%) diff --git a/.github/workflows/osx_pull_request.yml b/.github/workflows/macos_pull_request.yml similarity index 98% rename from .github/workflows/osx_pull_request.yml rename to .github/workflows/macos_pull_request.yml index 4c7e6b7ab2e..61cd0e78abc 100644 --- a/.github/workflows/osx_pull_request.yml +++ b/.github/workflows/macos_pull_request.yml @@ -1,4 +1,4 @@ -name: OSX +name: macOS on: pull_request: diff --git a/.github/workflows/osx_release.yml b/.github/workflows/macos_release.yml similarity index 78% rename from .github/workflows/osx_release.yml rename to .github/workflows/macos_release.yml index c0bc0b6abd5..b8af9045f29 100644 --- a/.github/workflows/osx_release.yml +++ b/.github/workflows/macos_release.yml @@ -1,4 +1,4 @@ -name: OSX (release) +name: macOS (release) on: push: @@ -23,10 +23,10 @@ jobs: - name: create package run: | cp doc/README.txt . - zip fheroes2_osx_sdl1.zip fheroes2 LICENSE fheroes2.key script/macos/install_sdl_1.sh script/demo/demo_macos.sh changelog.txt README.txt + zip fheroes2_macos10_15_sdl1.zip fheroes2 LICENSE fheroes2.key script/macos/install_sdl_1.sh script/demo/demo_macos.sh changelog.txt README.txt - uses: ncipollo/release-action@v1 with: - artifacts: fheroes2_osx_sdl1.zip + artifacts: fheroes2_macos10_15_sdl1.zip body: Compiled version of ${{ github.sha }} commit token: ${{ secrets.GITHUB_TOKEN }} name: MacOS build with SDL 1 support (latest commit) @@ -51,10 +51,10 @@ jobs: - name: create package run: | cp doc/README.txt . - zip fheroes2_osx_sdl2.zip fheroes2 LICENSE fheroes2.key script/macos/install_sdl_2.sh script/demo/demo_macos.sh changelog.txt README.txt + zip fheroes2_macos10_15_sdl2.zip fheroes2 LICENSE fheroes2.key script/macos/install_sdl_2.sh script/demo/demo_macos.sh changelog.txt README.txt - uses: ncipollo/release-action@v1 with: - artifacts: fheroes2_osx_sdl2.zip + artifacts: fheroes2_macos10_15_sdl2.zip body: Compiled version of ${{ github.sha }} commit token: ${{ secrets.GITHUB_TOKEN }} name: MacOS build with SDL 2 support (latest commit) From 12dd0a152f1790e82f6f29a0531805eba3ab3d62 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Thu, 25 Feb 2021 00:13:50 +0800 Subject: [PATCH 39/84] Fix hero shadow drawing on world map (#2856) close #1995 --- src/fheroes2/heroes/heroes_move.cpp | 80 ++++++++++++++++++----------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/src/fheroes2/heroes/heroes_move.cpp b/src/fheroes2/heroes/heroes_move.cpp index 7ee1dc04844..a14bef5113e 100644 --- a/src/fheroes2/heroes/heroes_move.cpp +++ b/src/fheroes2/heroes/heroes_move.cpp @@ -280,41 +280,59 @@ fheroes2::Sprite SpriteFlag( const Heroes & hero, int index, bool rotate ) fheroes2::Sprite SpriteShad( const Heroes & hero, int index ) { - int icn_shad = hero.isShipMaster() ? ICN::BOATSHAD : ICN::SHADOW32; - int index_sprite = 0; + if ( hero.isShipMaster() ) { + int indexSprite = 0; - switch ( hero.GetDirection() ) { - case Direction::TOP: - index_sprite = 0; - break; - case Direction::TOP_RIGHT: - index_sprite = 9; - break; - case Direction::RIGHT: - index_sprite = 18; - break; - case Direction::BOTTOM_RIGHT: - index_sprite = 27; - break; - case Direction::BOTTOM: - index_sprite = 36; - break; - case Direction::BOTTOM_LEFT: - index_sprite = 45; - break; - case Direction::LEFT: - index_sprite = 54; - break; - case Direction::TOP_LEFT: - index_sprite = 63; - break; + switch ( hero.GetDirection() ) { + case Direction::TOP: + indexSprite = 0; + break; + case Direction::TOP_RIGHT: + indexSprite = 9; + break; + case Direction::RIGHT: + indexSprite = 18; + break; + case Direction::BOTTOM_RIGHT: + indexSprite = 27; + break; + case Direction::BOTTOM: + indexSprite = 36; + break; + case Direction::BOTTOM_LEFT: + indexSprite = 45; + break; + case Direction::LEFT: + indexSprite = 54; + break; + case Direction::TOP_LEFT: + indexSprite = 63; + break; + default: + DEBUG_LOG( DBG_GAME, DBG_WARN, "unknown direction" ); + break; + } - default: - DEBUG_LOG( DBG_GAME, DBG_WARN, "unknown direction" ); - break; + return fheroes2::AGG::GetICN( ICN::BOATSHAD, indexSprite + ( index % 9 ) ); } + else { + int indexSprite = index; - return fheroes2::AGG::GetICN( icn_shad, index_sprite + ( index % 9 ) ); + if ( indexSprite == 51 ) + indexSprite = 56; + else if ( indexSprite == 50 ) + indexSprite = 57; + else if ( indexSprite == 49 ) + indexSprite = 58; + else if ( indexSprite == 47 ) + indexSprite = 55; + else if ( indexSprite == 46 ) + indexSprite = 55; + + const int indexOffset = ( indexSprite < 9 || indexSprite >= 36 ) ? 0 : 50; + + return fheroes2::AGG::GetICN( ICN::SHADOW32, indexSprite + indexOffset ); + } } fheroes2::Sprite SpriteFroth( const Heroes & hero, int index ) From 5ef5a432118f41de821780e0fbfcc251723f9d30 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Thu, 25 Feb 2021 06:51:23 -0500 Subject: [PATCH 40/84] AI: do not chase after faster units (#2861) close #2727 Very straightforward fix. Situation happens only when AI's melee units are on the offense action. Increased distance penalty rather than ignoring faster units to deal with archers first. Flyers are always "faster" than walkers. --- src/fheroes2/ai/normal/ai_normal_battle.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/fheroes2/ai/normal/ai_normal_battle.cpp b/src/fheroes2/ai/normal/ai_normal_battle.cpp index 7c2b76ed41c..d0865f79e94 100644 --- a/src/fheroes2/ai/normal/ai_normal_battle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_battle.cpp @@ -458,8 +458,10 @@ namespace AI if ( target.unit == nullptr ) { // move node pair consists of move hex index and distance const std::pair move = arena.CalculateMoveToUnit( *enemy ); + // Do not chase after faster units that might kite away and avoid engagement + const uint32_t distance = ( !enemy->isArchers() && isUnitFaster( *enemy, currentUnit ) ) ? move.second + ARENAW + ARENAH : move.second; - const double unitPriority = enemy->GetScoreQuality( currentUnit ) - move.second * attackDistanceModifier; + const double unitPriority = enemy->GetScoreQuality( currentUnit ) - distance * attackDistanceModifier; if ( unitPriority > maxPriority ) { maxPriority = unitPriority; target.cell = move.first; From dadf692961783e04bf46f99315b8ff081180f3f3 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Thu, 25 Feb 2021 06:53:20 -0500 Subject: [PATCH 41/84] Check if space is taken when selecting a moat cell (#2862) close #2754 --- src/fheroes2/ai/normal/ai_normal_battle.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/fheroes2/ai/normal/ai_normal_battle.cpp b/src/fheroes2/ai/normal/ai_normal_battle.cpp index d0865f79e94..686021805ee 100644 --- a/src/fheroes2/ai/normal/ai_normal_battle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_battle.cpp @@ -474,6 +474,10 @@ namespace AI uint32_t shortestDist = MAXU16; for ( const int wallIndex : underWallsIndicies ) { + if ( !arena.hexIsPassable( wallIndex ) ) { + continue; + } + const uint32_t dist = arena.CalculateMoveDistance( wallIndex ); if ( dist < shortestDist ) { shortestDist = dist; From d730aa57256c71b3a62c92e812468174ed80165d Mon Sep 17 00:00:00 2001 From: vasilenkoalexey Date: Thu, 25 Feb 2021 15:13:40 +0300 Subject: [PATCH 42/84] Fix summon boat logic (#2859) close #2435 close #2858 close #2175 --- src/fheroes2/heroes/heroes_spell.cpp | 65 ++++++++++------------------ 1 file changed, 24 insertions(+), 41 deletions(-) diff --git a/src/fheroes2/heroes/heroes_spell.cpp b/src/fheroes2/heroes/heroes_spell.cpp index 1e2eff97445..030f920e965 100644 --- a/src/fheroes2/heroes/heroes_spell.cpp +++ b/src/fheroes2/heroes/heroes_spell.cpp @@ -47,22 +47,22 @@ namespace const uint32_t townGatePenalty = 225; } -void DialogSpellFailed( const Spell & ); -void DialogNotAvailable( void ); - -bool ActionSpellViewMines( const Heroes & ); -bool ActionSpellViewResources( const Heroes & ); -bool ActionSpellViewArtifacts( Heroes & ); -bool ActionSpellViewTowns( const Heroes & ); -bool ActionSpellViewHeroes( const Heroes & ); -bool ActionSpellViewAll( const Heroes & ); -bool ActionSpellIdentifyHero( const Heroes & ); -bool ActionSpellSummonBoat( const Heroes & ); -bool ActionSpellDimensionDoor( Heroes & ); -bool ActionSpellTownGate( Heroes & ); -bool ActionSpellTownPortal( Heroes & ); -bool ActionSpellVisions( Heroes & ); -bool ActionSpellSetGuardian( Heroes &, const Spell & ); +void DialogSpellFailed( const Spell & spell ); +void DialogNotAvailable(); + +bool ActionSpellViewMines( const Heroes & hero ); +bool ActionSpellViewResources( const Heroes & hero ); +bool ActionSpellViewArtifacts( Heroes & hero ); +bool ActionSpellViewTowns( const Heroes & hero ); +bool ActionSpellViewHeroes( const Heroes & hero ); +bool ActionSpellViewAll( const Heroes & hero ); +bool ActionSpellIdentifyHero( const Heroes & hero ); +bool ActionSpellSummonBoat( const Heroes & hero ); +bool ActionSpellDimensionDoor( Heroes & hero ); +bool ActionSpellTownGate( Heroes & hero ); +bool ActionSpellTownPortal( Heroes & hero ); +bool ActionSpellVisions( Heroes & hero ); +bool ActionSpellSetGuardian( Heroes & hero, const Spell & spell ); class CastleIndexListBox : public Interface::ListBox { @@ -364,11 +364,11 @@ bool ActionSpellSummonBoat( const Heroes & hero ) return false; } - const s32 center = hero.GetIndex(); + const int32_t center = hero.GetIndex(); const Point & centerPoint = Maps::GetPoint( center ); // find water - s32 dst_water = -1; + int32_t dst_water = -1; MapsIndexes freeTiles = Maps::ScanAroundObject( center, MP2::OBJ_ZERO ); std::sort( freeTiles.begin(), freeTiles.end(), [¢erPoint]( const int32_t left, const int32_t right ) { const Point & leftPoint = Maps::GetPoint( left ); @@ -380,6 +380,7 @@ bool ActionSpellSummonBoat( const Heroes & hero ) return ( leftDiffX * leftDiffX + leftDiffY * leftDiffY ) < ( rightDiffX * rightDiffX + rightDiffY * rightDiffY ); } ); + for ( MapsIndexes::const_iterator it = freeTiles.begin(); it != freeTiles.end(); ++it ) { if ( world.GetTiles( *it ).isWater() ) { dst_water = *it; @@ -392,32 +393,14 @@ bool ActionSpellSummonBoat( const Heroes & hero ) return false; } - u32 chance = 0; - - switch ( hero.GetLevelSkill( Skill::Secondary::WISDOM ) ) { - case Skill::Level::BASIC: - chance = 50; - break; - case Skill::Level::ADVANCED: - chance = 75; - break; - case Skill::Level::EXPERT: - chance = 100; - break; - default: - chance = 30; - break; - } - const MapsIndexes & boats = Maps::GetObjectPositions( center, MP2::OBJ_BOAT, false ); - for ( size_t i = 0; i < boats.size(); ++i ) { - const s32 boat = boats[i]; - if ( Maps::isValidAbsIndex( boat ) ) { - if ( Rand::Get( 1, 100 ) <= chance ) { - Game::ObjectFadeAnimation::StartFadeTask( MP2::OBJ_BOAT, boat, dst_water, true, true ); + for ( auto it = boats.cbegin(); it != boats.cend(); ++it ) { + if ( Maps::isValidAbsIndex( *it ) ) { + const uint32_t distance = Maps::GetApproximateDistance( *it, hero.GetIndex() ); + if ( distance > 1 ) { + Game::ObjectFadeAnimation::StartFadeTask( MP2::OBJ_BOAT, *it, dst_water, true, true ); return true; } - break; } } From 341b4349978eb56ea5b122246624c9a3a06e3baa Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Thu, 25 Feb 2021 22:53:30 +0800 Subject: [PATCH 43/84] Allow to modify a hero during level up (#2866) close #2004 --- src/fheroes2/dialog/dialog_levelup.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/fheroes2/dialog/dialog_levelup.cpp b/src/fheroes2/dialog/dialog_levelup.cpp index ce7995b6b45..e41117574ba 100644 --- a/src/fheroes2/dialog/dialog_levelup.cpp +++ b/src/fheroes2/dialog/dialog_levelup.cpp @@ -196,7 +196,13 @@ int DialogSelectSecondary( const std::string & name, const std::string & primary else if ( le.MouseClickLeft( button_learn2.area() ) || Game::HotKeyPressEvent( Game::EVENT_DEFAULT_RIGHT ) ) return sec2.Skill(); else if ( le.MouseClickLeft( button_hero.area() ) || Game::HotKeyPressEvent( Game::EVENT_DEFAULT_READY ) ) { - hero.OpenDialog( true /* read only */, false ); + const bool noDismiss = hero.Modes( Heroes::NOTDISMISS ); + hero.SetModes( Heroes::NOTDISMISS ); + hero.OpenDialog( false, true ); + + if ( !noDismiss ) { + hero.ResetModes( Heroes::NOTDISMISS ); + } cursor.Show(); display.render(); } From ee397683e90994fcf6a51015544531570e65208f Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Fri, 26 Feb 2021 09:35:26 -0500 Subject: [PATCH 44/84] AI wide units pathfding and moat logic (#2868) close #2753 --- src/fheroes2/battle/battle_board.cpp | 25 ++-- src/fheroes2/battle/battle_board.h | 1 + src/fheroes2/battle/battle_pathfinding.cpp | 135 ++++++++------------- src/fheroes2/battle/battle_pathfinding.h | 4 +- 4 files changed, 68 insertions(+), 97 deletions(-) diff --git a/src/fheroes2/battle/battle_board.cpp b/src/fheroes2/battle/battle_board.cpp index 22b41be0172..458c27f97de 100644 --- a/src/fheroes2/battle/battle_board.cpp +++ b/src/fheroes2/battle/battle_board.cpp @@ -71,20 +71,6 @@ namespace } } -namespace Battle -{ - bool IsLeftDirection( const int32_t startCellId, const int32_t endCellId, const bool prevLeftDirection ) - { - const int startX = startCellId % ARENAW; - const int endX = endCellId % ARENAW; - - if ( prevLeftDirection ) - return endX <= startX; - else - return endX < startX; - } -} - Battle::Board::Board() { reserve( ARENASIZE ); @@ -588,6 +574,17 @@ bool Battle::Board::isReflectDirection( int d ) return false; } +bool Battle::Board::IsLeftDirection( const int32_t startCellId, const int32_t endCellId, const bool prevLeftDirection ) +{ + const int startX = startCellId % ARENAW; + const int endX = endCellId % ARENAW; + + if ( prevLeftDirection ) + return endX <= startX; + else + return endX < startX; +} + bool Battle::Board::isNegativeDistance( s32 index1, s32 index2 ) { return ( index1 % ARENAW ) - ( index2 % ARENAW ) < 0; diff --git a/src/fheroes2/battle/battle_board.h b/src/fheroes2/battle/battle_board.h index 1ea84877ae7..2f06bea5793 100644 --- a/src/fheroes2/battle/battle_board.h +++ b/src/fheroes2/battle/battle_board.h @@ -79,6 +79,7 @@ namespace Battle static bool isImpassableIndex( s32 ); static bool isOutOfWallsIndex( s32 ); static bool isReflectDirection( int ); + static bool IsLeftDirection( const int32_t startCellId, const int32_t endCellId, const bool prevLeftDirection ); static bool isNegativeDistance( s32 index1, s32 index2 ); static int GetReflectDirection( int ); static int GetDirection( s32, s32 ); diff --git a/src/fheroes2/battle/battle_pathfinding.cpp b/src/fheroes2/battle/battle_pathfinding.cpp index 57e75d206b5..512c5b0652e 100644 --- a/src/fheroes2/battle/battle_pathfinding.cpp +++ b/src/fheroes2/battle/battle_pathfinding.cpp @@ -19,78 +19,21 @@ ***************************************************************************/ #include "battle_pathfinding.h" +#include "battle_bridge.h" #include "battle_troop.h" +#include "castle.h" #include "logging.h" #include namespace Battle { - // If move is not valid this function returns -1 otherwise returns new index after the move. - int GetValidMoveIndex( int fromCell, int directionMask, bool isWide ) - { - int newIndex = -1; - int wideUnitOffset = 0; - const int x = fromCell % ARENAW; - const int y = fromCell / ARENAW; - const bool isOddRow = ( y % 2 ) == 1; - - switch ( directionMask ) { - case Battle::TOP_LEFT: - if ( y > 0 && ( x > 0 || !isOddRow ) ) { - newIndex = fromCell - ARENAW - ( isOddRow ? 1 : 0 ); - wideUnitOffset = 1; - } - break; - case Battle::TOP_RIGHT: - if ( y > 0 && ( x < ARENAW - 1 || isOddRow ) ) { - newIndex = fromCell - ARENAW + ( isOddRow ? 0 : 1 ); - wideUnitOffset = -1; - } - break; - case Battle::RIGHT: - if ( x < ARENAW - 1 ) { - newIndex = fromCell + 1; - wideUnitOffset = -1; - } - break; - case Battle::BOTTOM_RIGHT: - if ( y < ARENAH - 1 && ( x < ARENAW - 1 || isOddRow ) ) { - newIndex = fromCell + ARENAW + ( isOddRow ? 0 : 1 ); - wideUnitOffset = -1; - } - break; - case Battle::BOTTOM_LEFT: - if ( y < ARENAH - 1 && ( x > 0 || !isOddRow ) ) { - newIndex = fromCell + ARENAW - ( isOddRow ? 1 : 0 ); - wideUnitOffset = 1; - } - break; - case Battle::LEFT: - if ( x > 0 ) { - newIndex = fromCell - 1; - wideUnitOffset = 1; - } - break; - default: - return -1; - } - - // invalidate move - if ( newIndex == -1 || !Board::GetCell( newIndex )->isPassable1( false ) ) - return -1; - - if ( isWide && ( x + wideUnitOffset < 0 || x + wideUnitOffset > ARENAW - 1 || !Board::GetCell( newIndex + wideUnitOffset )->isPassable1( false ) ) ) - return -1; - - return newIndex; - } - void ArenaNode::resetNode() { _from = -1; _cost = MAX_MOVE_COST; _objectID = 0; _isOpen = true; + _isLeftDirection = false; } ArenaPathfinder::ArenaPathfinder() @@ -133,31 +76,40 @@ namespace Battle { reset(); + const bool unitIsWide = unit.isWide(); + const Cell * unitHead = unit.GetPosition().GetHead(); - if ( !unitHead ) { + const Cell * unitTail = unit.GetPosition().GetTail(); + if ( !unitHead || ( unitIsWide && !unitTail ) ) { DEBUG_LOG( DBG_BATTLE, DBG_WARN, "Pathfinder: Invalid unit is passed in! " << unit.GetName() ); return; } - const Cell * unitTail = unit.GetPosition().GetTail(); - const bool unitIsWide = unit.isWide(); + const Bridge * bridge = Arena::GetBridge(); + const Castle * castle = Arena::GetCastle(); - const int headIdx = unitHead->GetIndex(); + const bool isPassableBridge = bridge == nullptr || bridge->isPassable( unit.GetColor() ); + const bool isMoatBuilt = castle && castle->isBuild( BUILD_MOAT ); + + // Initialize the starting cells + const int32_t headIdx = unitHead->GetIndex(); _cache[headIdx]._cost = 0; _cache[headIdx]._isOpen = false; + _cache[headIdx]._isLeftDirection = unit.isReflect(); - int tailIdx = -1; - if ( unitTail ) { - tailIdx = unitTail->GetIndex(); + if ( unitIsWide ) { + const int32_t tailIdx = unitTail->GetIndex(); + _cache[tailIdx]._from = headIdx; _cache[tailIdx]._cost = 0; - _cache[tailIdx]._isOpen = false; + _cache[tailIdx]._isOpen = true; + _cache[headIdx]._isLeftDirection = !unit.isReflect(); } if ( unit.isFlying() ) { const Board & board = *Arena::GetBoard(); for ( Board::const_iterator it = board.begin(); it != board.end(); ++it ) { - const int idx = it->GetIndex(); + const int32_t idx = it->GetIndex(); ArenaNode & node = _cache[idx]; if ( it->isPassable1( true ) ) { @@ -172,11 +124,11 @@ namespace Battle for ( Board::const_iterator it = board.begin(); it != board.end(); ++it ) { const Unit * boardUnit = it->GetUnit(); if ( boardUnit && boardUnit->GetUID() != unit.GetUID() ) { - const int unitIdx = it->GetIndex(); + const int32_t unitIdx = it->GetIndex(); ArenaNode & unitNode = _cache[unitIdx]; const Indexes & around = Battle::Board::GetAroundIndexes( unitIdx ); - for ( const int cell : around ) { + for ( const int32_t cell : around ) { const uint32_t flyingDist = static_cast( Battle::Board::GetDistance( headIdx, cell ) ); if ( hexIsPassable( cell ) && ( flyingDist < unitNode._cost ) ) { unitNode._isOpen = false; @@ -188,31 +140,50 @@ namespace Battle } } else { - std::vector nodesToExplore; + std::vector nodesToExplore; nodesToExplore.push_back( headIdx ); - - if ( tailIdx != -1 ) - nodesToExplore.push_back( tailIdx ); + if ( unitIsWide ) + nodesToExplore.push_back( unitTail->GetIndex() ); for ( size_t lastProcessedNode = 0; lastProcessedNode < nodesToExplore.size(); ++lastProcessedNode ) { - const int fromNode = nodesToExplore[lastProcessedNode]; + const int32_t fromNode = nodesToExplore[lastProcessedNode]; + ArenaNode & previousNode = _cache[fromNode]; + + Indexes aroundCellIds; + if ( !unitIsWide ) + aroundCellIds = Board::GetAroundIndexes( fromNode ); + else if ( previousNode._from < 0 ) + aroundCellIds = Board::GetMoveWideIndexes( fromNode, unit.isReflect() ); + else + aroundCellIds = Board::GetMoveWideIndexes( fromNode, ( RIGHT_SIDE & Board::GetDirection( fromNode, previousNode._from ) ) ); - for ( int direction = TOP_LEFT; direction < CENTER; direction = direction << 1 ) { - const int newNode = GetValidMoveIndex( fromNode, direction, unitIsWide ); + for ( const int32_t newNode : aroundCellIds ) { + const Cell * headCell = Board::GetCell( newNode ); - if ( newNode != -1 ) { + const bool isLeftDirection = unitIsWide && Board::IsLeftDirection( fromNode, newNode, previousNode._isLeftDirection ); + const int32_t tailCellId = isLeftDirection ? newNode + 1 : newNode - 1; + + if ( headCell->isPassable1( false ) && ( !unitIsWide || Board::GetCell( tailCellId )->isPassable1( false ) ) + && ( isPassableBridge || !Board::isBridgeIndex( newNode ) ) ) { const uint32_t cost = _cache[fromNode]._cost; ArenaNode & node = _cache[newNode]; - if ( Board::GetCell( newNode )->GetUnit() && cost < node._cost ) { + uint32_t additionalCost = ( isMoatBuilt && Board::isMoatIndex( newNode ) ) ? 2 : 1; + // Turn back. No movement at all. + if ( isLeftDirection != previousNode._isLeftDirection ) + additionalCost = 0; + + if ( headCell->GetUnit() && cost < node._cost ) { node._isOpen = false; node._from = fromNode; node._cost = cost; + node._isLeftDirection = isLeftDirection; } - else if ( cost + 1 < node._cost ) { + else if ( cost + additionalCost < node._cost ) { node._isOpen = true; node._from = fromNode; - node._cost = cost + 1; + node._cost = cost + additionalCost; + node._isLeftDirection = isLeftDirection; nodesToExplore.push_back( newNode ); } } diff --git a/src/fheroes2/battle/battle_pathfinding.h b/src/fheroes2/battle/battle_pathfinding.h index 64dcd589407..31b1c659ca3 100644 --- a/src/fheroes2/battle/battle_pathfinding.h +++ b/src/fheroes2/battle/battle_pathfinding.h @@ -38,14 +38,16 @@ namespace Battle struct ArenaNode : public PathfindingNode { bool _isOpen = true; + bool _isLeftDirection = false; // ArenaNode uses different default values ArenaNode() : PathfindingNode( -1, MAX_MOVE_COST, 0 ) {} - ArenaNode( int node, uint16_t cost, bool isOpen ) + ArenaNode( int node, uint16_t cost, bool isOpen, bool isLeftDirection ) : PathfindingNode( node, cost, 0 ) , _isOpen( isOpen ) + , _isLeftDirection( isLeftDirection ) {} // Override the base version of the call to use proper values virtual void resetNode() override; From 1b62c18adcd55b34236446758bb6ab29e8dafbe5 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Fri, 26 Feb 2021 23:25:01 +0800 Subject: [PATCH 45/84] Fix ACCEPT button font for Good Interface (#2869) relates to #2461 --- src/fheroes2/agg/agg.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/fheroes2/agg/agg.cpp b/src/fheroes2/agg/agg.cpp index 83cbfb44d95..50c50778d23 100644 --- a/src/fheroes2/agg/agg.cpp +++ b/src/fheroes2/agg/agg.cpp @@ -1331,6 +1331,13 @@ namespace fheroes2 out.setPosition( source.y(), source.x() ); } return true; + case ICN::SURRENDR: + LoadOriginalICN( id ); + if ( !_icnVsSprite[id].empty() ) { + // Fix incorrect font color. + ReplaceColorId( _icnVsSprite[id][0], 28, 56 ); + } + return true; default: break; } From c73e4d0ed79601df4f6a3090a710ee43d7231bbd Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Sat, 27 Feb 2021 08:30:23 +0800 Subject: [PATCH 46/84] Add missing shadow for arrow cursor on SDL2 (#2872) close #2810 --- src/engine/screen.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/engine/screen.cpp b/src/engine/screen.cpp index a0e0bfa9082..7d42849546a 100644 --- a/src/engine/screen.cpp +++ b/src/engine/screen.cpp @@ -178,6 +178,11 @@ namespace const uint8_t * value = currentPalette + *in * 3; *out = SDL_MapRGBA( surface->format, *( value ), *( value + 1 ), *( value + 2 ), 255 ); } + else if ( *transform > 1 ) { + // SDL2 uses RGBA image on OS level separately from frame rendering. + // Here we are trying to simulate cursor's shadow as close as possible to the original game. + *out = SDL_MapRGBA( surface->format, 0, 0, 0, 64 ); + } } } else { From 76d84969130c0f6910c1c8523091e3b26036c646 Mon Sep 17 00:00:00 2001 From: wch510719826 Date: Sat, 27 Feb 2021 18:24:57 +0800 Subject: [PATCH 47/84] Add Visual Studio 2019 support (#2875) --- .github/workflows/vs2019_pull_request.yml | 27 ++ README.md | 2 +- VisualStudio/Debug.props | 1 + appveyor.yml | 3 + fheroes2-vs2015.vcxproj | 186 ++++++++ fheroes2-vs2019.vcxproj | 198 ++++++++ fheroes2.vcxproj | 542 ---------------------- 7 files changed, 416 insertions(+), 543 deletions(-) create mode 100644 .github/workflows/vs2019_pull_request.yml create mode 100644 fheroes2-vs2015.vcxproj create mode 100644 fheroes2-vs2019.vcxproj delete mode 100644 fheroes2.vcxproj diff --git a/.github/workflows/vs2019_pull_request.yml b/.github/workflows/vs2019_pull_request.yml new file mode 100644 index 00000000000..74c919cf038 --- /dev/null +++ b/.github/workflows/vs2019_pull_request.yml @@ -0,0 +1,27 @@ +name: VS2019 + +on: + pull_request: + branches: [ master ] + +jobs: + build: + strategy: + fail-fast: false + matrix: + config-type: [Release-SDL1, Release-SDL2] + platform-type: [x86, x64] + runs-on: windows-2019 + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 50 + - name: install packages + run: | + cd script/windows + ./install_packages.bat + cd ../.. + - name: Add msbuild to PATH + uses: microsoft/setup-msbuild@v1.0.2 + - name: compile + run: MSBuild.exe fheroes2-vs2019.vcxproj /property:Configuration=${{ matrix.config-type }} /property:Platform=${{ matrix.platform-type }} diff --git a/README.md b/README.md index 7ebcc0805a1..6b6b2f86891 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Windows - **Optional step**: Install **7-zip** archiver at its default location as `C:\Program Files\7-Zip\7z.exe` or otherwise, you will need to manually extract each downloaded package (follow the instructions shown by batch scripts mentioned below). - open `script/windows` directory and run **install_packages.bat** file. It installs necessary packages for Visual Studio build. - open `script/demo` directory and run **demo_windows.bat** file. It downloads a demo version of the game which is needed for minimum development. -- open **fheroes.vcxproj** by Visual Studio and compile the project. +- open **fheroes2-vs2015.vcxproj** or **fheroes2-vs2019.vcxproj** file depending on your Visual Studio and compile the project. MacOS and Linux ------------------- diff --git a/VisualStudio/Debug.props b/VisualStudio/Debug.props index a0d70e2be32..0d46a8afa31 100644 --- a/VisualStudio/Debug.props +++ b/VisualStudio/Debug.props @@ -5,6 +5,7 @@ + true Disabled WITH_DEBUG;_DEBUG;%(PreprocessorDefinitions) diff --git a/appveyor.yml b/appveyor.yml index dee2d6c2692..415e815f816 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -45,6 +45,9 @@ install: - if not exist C:\projects\packages call install_packages.bat - cd C:\projects\fheroes2 +build: + project: fheroes2-vs2015.vcxproj + after_build: - cmd: cd C:\projects\fheroes2\build\%platform%\%configuration% - cmd: if "%platform%" == "x86" xcopy /Y /s /Q "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\redist\x86\Microsoft.VC140.CRT\msvcp140.dll" "." diff --git a/fheroes2-vs2015.vcxproj b/fheroes2-vs2015.vcxproj new file mode 100644 index 00000000000..ea31b780788 --- /dev/null +++ b/fheroes2-vs2015.vcxproj @@ -0,0 +1,186 @@ + + + + + Debug-SDL1 + Win32 + + + Debug-SDL2 + Win32 + + + Debug-SDL2 + x64 + + + Release-SDL1 + Win32 + + + Debug-SDL1 + x64 + + + Release-SDL1 + x64 + + + Release-SDL2 + Win32 + + + Release-SDL2 + x64 + + + + {DD8F214C-C405-4951-8F98-66B969BA8E08} + Win32Proj + fheroes2 + + + + Application + true + v140 + MultiByte + + + Application + true + v140 + MultiByte + + + Application + false + v140 + true + MultiByte + + + Application + false + v140 + true + MultiByte + + + Application + true + v140 + MultiByte + + + Application + true + v140 + MultiByte + + + Application + false + v140 + true + MultiByte + + + Application + false + v140 + true + MultiByte + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MultiThreadedDebug + + + + + MultiThreadedDebug + + + + + + + MultiThreaded + + + + + MultiThreaded + + + + + + + + + + + + + + + + + diff --git a/fheroes2-vs2019.vcxproj b/fheroes2-vs2019.vcxproj new file mode 100644 index 00000000000..28696e18f39 --- /dev/null +++ b/fheroes2-vs2019.vcxproj @@ -0,0 +1,198 @@ + + + + + Debug-SDL1 + Win32 + + + Debug-SDL2 + Win32 + + + Debug-SDL2 + x64 + + + Release-SDL1 + Win32 + + + Debug-SDL1 + x64 + + + Release-SDL1 + x64 + + + Release-SDL2 + Win32 + + + Release-SDL2 + x64 + + + + {DD8F214C-C405-4951-8F98-66B969BA8E08} + Win32Proj + fheroes2 + 10.0 + + + + Application + true + v142 + MultiByte + + + Application + true + v142 + MultiByte + + + Application + false + v142 + true + MultiByte + + + Application + false + v142 + true + MultiByte + + + Application + true + v142 + MultiByte + + + Application + true + v142 + MultiByte + + + Application + false + v142 + true + MultiByte + + + Application + false + v142 + true + MultiByte + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fheroes2 + + + fheroes2 + + + fheroes2 + + + fheroes2 + + + + + + MultiThreadedDebug + + + + + MultiThreadedDebug + + + + + + + MultiThreaded + + + + + MultiThreaded + + + + + + + + + + + + + + + + + diff --git a/fheroes2.vcxproj b/fheroes2.vcxproj deleted file mode 100644 index 9ddf22cb3ea..00000000000 --- a/fheroes2.vcxproj +++ /dev/null @@ -1,542 +0,0 @@ - - - - - Debug-SDL1 - Win32 - - - Debug-SDL2 - Win32 - - - Debug-SDL2 - x64 - - - Release-SDL1 - Win32 - - - Debug-SDL1 - x64 - - - Release-SDL1 - x64 - - - Release-SDL2 - Win32 - - - Release-SDL2 - x64 - - - - {DD8F214C-C405-4951-8F98-66B969BA8E08} - Win32Proj - fheroes2 - - - - Application - true - v140 - MultiByte - - - Application - true - v140 - MultiByte - - - Application - false - v140 - true - MultiByte - - - Application - false - v140 - true - MultiByte - - - Application - true - v140 - MultiByte - - - Application - true - v140 - MultiByte - - - Application - false - v140 - true - MultiByte - - - Application - false - v140 - true - MultiByte - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MultiThreadedDebug - - - - - MultiThreadedDebug - - - - - - - MultiThreaded - - - - - MultiThreaded - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From c6f9321f43ec814ba37ba403258c2231ff833c71 Mon Sep 17 00:00:00 2001 From: Pavel <53114202+shprotru@users.noreply.github.com> Date: Sat, 27 Feb 2021 14:27:14 +0300 Subject: [PATCH 48/84] Combat screen, logs list, indicate "Ballista" instead of "Turret" when referring to the Ballista (#2849) close #2784 --- src/fheroes2/battle/battle_action.cpp | 2 +- src/fheroes2/battle/battle_interface.cpp | 5 +++-- src/fheroes2/battle/battle_interface.h | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/fheroes2/battle/battle_action.cpp b/src/fheroes2/battle/battle_action.cpp index d0ca1dbf324..319d199915d 100644 --- a/src/fheroes2/battle/battle_action.cpp +++ b/src/fheroes2/battle/battle_action.cpp @@ -819,7 +819,7 @@ void Battle::Arena::ApplyActionTower( Command & cmd ) interface->RedrawActionTowerPart1( *tower, *b2 ); target.killed = b2->ApplyDamage( *tower, target.damage ); if ( interface ) - interface->RedrawActionTowerPart2( target ); + interface->RedrawActionTowerPart2( *tower, target ); if ( b2->Modes( SP_BLIND ) ) b2->ResetBlind(); diff --git a/src/fheroes2/battle/battle_interface.cpp b/src/fheroes2/battle/battle_interface.cpp index cc6218a83ae..c170db43e2c 100644 --- a/src/fheroes2/battle/battle_interface.cpp +++ b/src/fheroes2/battle/battle_interface.cpp @@ -3512,7 +3512,7 @@ void Battle::Interface::RedrawActionTowerPart1( const Tower & tower, const Unit RedrawMissileAnimation( missileStart, targetPos, angle, Monster::ORC ); } -void Battle::Interface::RedrawActionTowerPart2( const TargetInfo & target ) +void Battle::Interface::RedrawActionTowerPart2( const Tower & tower, const TargetInfo & target ) { TargetsInfo targets; targets.push_back( target ); @@ -3522,7 +3522,8 @@ void Battle::Interface::RedrawActionTowerPart2( const TargetInfo & target ) RedrawActionWincesKills( targets ); // draw status for first defender - std::string msg = _( "Tower does %{damage} damage." ); + std::string msg = _( "%{tower} does %{damage} damage." ); + StringReplace( msg, "%{tower}", tower.GetName() ); StringReplace( msg, "%{damage}", target.damage ); if ( target.killed ) { msg.append( " " ); diff --git a/src/fheroes2/battle/battle_interface.h b/src/fheroes2/battle/battle_interface.h index a258b57b9d3..d78a1247e1f 100644 --- a/src/fheroes2/battle/battle_interface.h +++ b/src/fheroes2/battle/battle_interface.h @@ -216,7 +216,7 @@ namespace Battle void RedrawActionMorale( Unit &, bool ); void RedrawActionLuck( const Unit & ); void RedrawActionTowerPart1( const Tower &, const Unit & ); - void RedrawActionTowerPart2( const TargetInfo & ); + void RedrawActionTowerPart2( const Tower &, const TargetInfo & ); void RedrawActionCatapult( int ); void RedrawActionTeleportSpell( Unit &, s32 ); void RedrawActionEarthQuakeSpell( const std::vector & ); From 83c6e5cb3a33f20aa6e6c935f53bf3071b921a47 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Sat, 27 Feb 2021 06:29:50 -0500 Subject: [PATCH 49/84] Pathfinder should know that moat consumes all remaining movement (#2870) close #2737 close #2871 --- src/fheroes2/battle/battle_board.cpp | 3 ++- src/fheroes2/battle/battle_pathfinding.cpp | 17 ++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/fheroes2/battle/battle_board.cpp b/src/fheroes2/battle/battle_board.cpp index 458c27f97de..c9c9d598923 100644 --- a/src/fheroes2/battle/battle_board.cpp +++ b/src/fheroes2/battle/battle_board.cpp @@ -237,6 +237,7 @@ Battle::Indexes Battle::Board::GetAStarPath( const Unit & unit, const Position & const Castle * castle = Arena::GetCastle(); const bool isPassableBridge = bridge == nullptr || bridge->isPassable( unit.GetColor() ); const bool isMoatBuilt = castle && castle->isBuild( BUILD_MOAT ); + const int32_t moatPenalty = unit.GetSpeed() * 100; std::map cellMap; cellMap[currentCellId].parentCellId = -1; @@ -272,7 +273,7 @@ Battle::Indexes Battle::Board::GetAStarPath( const Unit & unit, const Position & int32_t cost = 100 * ( Board::GetDistance( cellId, targetHeadCellId ) + Board::GetDistance( tailCellId, targetTailCellId ) ); if ( isMoatBuilt && Board::isMoatIndex( cellId ) ) - cost += 100; + cost += std::max( moatPenalty - currentCellNode.cost, 100 ); // Turn back. No movement at all. if ( isLeftDirection != currentCellNode.leftDirection ) diff --git a/src/fheroes2/battle/battle_pathfinding.cpp b/src/fheroes2/battle/battle_pathfinding.cpp index 512c5b0652e..31a96ed4dc2 100644 --- a/src/fheroes2/battle/battle_pathfinding.cpp +++ b/src/fheroes2/battle/battle_pathfinding.cpp @@ -90,6 +90,7 @@ namespace Battle const bool isPassableBridge = bridge == nullptr || bridge->isPassable( unit.GetColor() ); const bool isMoatBuilt = castle && castle->isBuild( BUILD_MOAT ); + const uint32_t moatPenalty = unit.GetSpeed(); // Initialize the starting cells const int32_t headIdx = unitHead->GetIndex(); @@ -161,17 +162,19 @@ namespace Battle const Cell * headCell = Board::GetCell( newNode ); const bool isLeftDirection = unitIsWide && Board::IsLeftDirection( fromNode, newNode, previousNode._isLeftDirection ); - const int32_t tailCellId = isLeftDirection ? newNode + 1 : newNode - 1; + const Cell * tailCell = unitIsWide ? Board::GetCell( isLeftDirection ? newNode + 1 : newNode - 1 ) : nullptr; - if ( headCell->isPassable1( false ) && ( !unitIsWide || Board::GetCell( tailCellId )->isPassable1( false ) ) - && ( isPassableBridge || !Board::isBridgeIndex( newNode ) ) ) { + if ( headCell->isPassable1( false ) && ( !tailCell || tailCell->isPassable1( false ) ) && ( isPassableBridge || !Board::isBridgeIndex( newNode ) ) ) { const uint32_t cost = _cache[fromNode]._cost; ArenaNode & node = _cache[newNode]; - uint32_t additionalCost = ( isMoatBuilt && Board::isMoatIndex( newNode ) ) ? 2 : 1; - // Turn back. No movement at all. - if ( isLeftDirection != previousNode._isLeftDirection ) - additionalCost = 0; + // Check if we're turning back. No movement at all. + uint32_t additionalCost = ( isLeftDirection != previousNode._isLeftDirection ) ? 0 : 1; + + // Moat penalty consumes all remaining movement. Be careful when dealing with unsigned values + if ( isMoatBuilt && Board::isMoatIndex( newNode ) ) { + additionalCost += ( moatPenalty > previousNode._cost ) ? moatPenalty - previousNode._cost : 1u; + } if ( headCell->GetUnit() && cost < node._cost ) { node._isOpen = false; From 50296f5d149c0a00ae6a19e5dc831e4a9873c0ee Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Sat, 27 Feb 2021 21:18:57 +0800 Subject: [PATCH 50/84] Fix compilation output for Visual Studio (#2876) --- VisualStudio/common.props | 1 + fheroes2-vs2019.vcxproj | 13 +------------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/VisualStudio/common.props b/VisualStudio/common.props index 74d23ab4809..a0681632b37 100644 --- a/VisualStudio/common.props +++ b/VisualStudio/common.props @@ -5,6 +5,7 @@ $(MSBuildThisFileDirectory)..\build\$(PlatformTarget)\$(Configuration)\ $(MSBuildThisFileDirectory)..\build\$(PlatformTarget)\$(Configuration)\$(ProjectName)\ + fheroes2 diff --git a/fheroes2-vs2019.vcxproj b/fheroes2-vs2019.vcxproj index 28696e18f39..790b227b9a1 100644 --- a/fheroes2-vs2019.vcxproj +++ b/fheroes2-vs2019.vcxproj @@ -145,18 +145,7 @@ - - fheroes2 - - - fheroes2 - - - fheroes2 - - - fheroes2 - + From d01be00a910532f50c358d11c8d7ee1b38f18447 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Sat, 27 Feb 2021 08:47:59 -0500 Subject: [PATCH 51/84] Optimize army placement only during AI turn (#2874) close #2863 --- src/fheroes2/ai/ai.h | 6 +- src/fheroes2/ai/ai_base.cpp | 4 +- src/fheroes2/ai/ai_common.cpp | 81 ++++++++++++++++++++- src/fheroes2/ai/normal/ai_normal.h | 2 +- src/fheroes2/ai/normal/ai_normal_battle.cpp | 75 +------------------ src/fheroes2/ai/normal/ai_normal_castle.cpp | 1 + src/fheroes2/battle/battle_main.cpp | 8 +- 7 files changed, 94 insertions(+), 83 deletions(-) diff --git a/src/fheroes2/ai/ai.h b/src/fheroes2/ai/ai.h index 50505480b53..32f2f6a7d4e 100644 --- a/src/fheroes2/ai/ai.h +++ b/src/fheroes2/ai/ai.h @@ -32,6 +32,7 @@ class Castle; class HeroBase; class Heroes; class Kingdom; +class Army; struct VecHeroes; namespace Maps { @@ -75,8 +76,8 @@ namespace AI virtual void HeroesAdd( const Heroes & hero ); virtual void HeroesRemove( const Heroes & hero ); - virtual void HeroesPreBattle( HeroBase & hero ); - virtual void HeroesAfterBattle( HeroBase & hero ); + virtual void HeroesPreBattle( HeroBase & hero, bool isAttacking ); + virtual void HeroesAfterBattle( HeroBase & hero, bool wasAttacking ); virtual void HeroesPostLoad( Heroes & hero ); virtual bool HeroesCanMove( const Heroes & hero ); virtual bool HeroesGetTask( Heroes & hero ); @@ -124,6 +125,7 @@ namespace AI bool BuildIfEnoughResources( Castle & castle, int building, uint32_t minimumMultiplicator ); uint32_t GetResourceMultiplier( const Castle & castle, uint32_t min, uint32_t max ); void ReinforceHeroInCastle( Heroes & hero, Castle & castle, const Funds & budget ); + void OptimizeTroopsOrder( Army & hero ); StreamBase & operator<<( StreamBase &, const AI::Base & ); StreamBase & operator>>( StreamBase &, AI::Base & ); diff --git a/src/fheroes2/ai/ai_base.cpp b/src/fheroes2/ai/ai_base.cpp index b8d0da163c7..03acdad545c 100644 --- a/src/fheroes2/ai/ai_base.cpp +++ b/src/fheroes2/ai/ai_base.cpp @@ -81,9 +81,9 @@ namespace AI void Base::HeroesRemove( const Heroes & ) {} - void Base::HeroesPreBattle( HeroBase & ) {} + void Base::HeroesPreBattle( HeroBase &, bool ) {} - void Base::HeroesAfterBattle( HeroBase & ) {} + void Base::HeroesAfterBattle( HeroBase &, bool ) {} void Base::HeroesActionNewPosition( Heroes & ) {} diff --git a/src/fheroes2/ai/ai_common.cpp b/src/fheroes2/ai/ai_common.cpp index d65fb19b46e..6715ad885bb 100644 --- a/src/fheroes2/ai/ai_common.cpp +++ b/src/fheroes2/ai/ai_common.cpp @@ -19,6 +19,7 @@ ***************************************************************************/ #include "ai.h" +#include "army.h" #include "castle.h" #include "kingdom.h" #include "normal/ai_normal.h" @@ -62,8 +63,84 @@ namespace AI hero.BuySpellBook( &castle ); } - hero.GetArmy().UpgradeTroops( castle ); + Army & heroArmy = hero.GetArmy(); + heroArmy.UpgradeTroops( castle ); castle.recruitBestAvailable( budget ); - hero.GetArmy().JoinStrongestFromArmy( castle.GetArmy() ); + heroArmy.JoinStrongestFromArmy( castle.GetArmy() ); + OptimizeTroopsOrder( heroArmy ); + } + + void OptimizeTroopsOrder( Army & army ) + { + // Optimize troops placement before the battle + std::vector archers; + std::vector others; + + // Validate and pick the troops + for ( size_t slot = 0; slot < ARMYMAXTROOPS; ++slot ) { + Troop * troop = army.GetTroop( slot ); + if ( troop && troop->isValid() ) { + if ( troop->isArchers() ) { + archers.push_back( *troop ); + } + else { + others.push_back( *troop ); + } + } + } + + // Sort troops by tactical priority. For melee: + // 1. Faster units first + // 2. Flyers first + // 3. Finally if unit type and speed is same, compare by strength + std::sort( others.begin(), others.end(), []( const Troop & left, const Troop & right ) { + if ( left.GetSpeed() == right.GetSpeed() ) { + if ( left.isFlying() == right.isFlying() ) { + return left.GetStrength() < right.GetStrength(); + } + return right.isFlying(); + } + return left.GetSpeed() < right.GetSpeed(); + } ); + + // Archers sorted purely by strength. + std::sort( archers.begin(), archers.end(), []( const Troop & left, const Troop & right ) { return left.GetStrength() < right.GetStrength(); } ); + + std::vector slotOrder = {2, 1, 3, 0, 4}; + switch ( archers.size() ) { + case 1: + slotOrder = {0, 2, 1, 3, 4}; + break; + case 2: + slotOrder = {0, 4, 2, 1, 3}; + break; + case 3: + slotOrder = {0, 4, 2, 1, 3}; + break; + case 4: + slotOrder = {0, 4, 2, 3, 1}; + break; + case 5: + slotOrder = {0, 4, 1, 2, 3}; + break; + default: + break; + } + + // Re-arrange troops in army + army.Clean(); + for ( const size_t slot : slotOrder ) { + if ( !archers.empty() ) { + army.GetTroop( slot )->Set( archers.back() ); + archers.pop_back(); + } + else if ( !others.empty() ) { + army.GetTroop( slot )->Set( others.back() ); + others.pop_back(); + } + else { + break; + } + } } } diff --git a/src/fheroes2/ai/normal/ai_normal.h b/src/fheroes2/ai/normal/ai_normal.h index a19d6083315..acf3330a3ff 100644 --- a/src/fheroes2/ai/normal/ai_normal.h +++ b/src/fheroes2/ai/normal/ai_normal.h @@ -84,7 +84,7 @@ namespace AI virtual void revealFog( const Maps::Tiles & tile ) override; - virtual void HeroesPreBattle( HeroBase & hero ) override; + virtual void HeroesPreBattle( HeroBase & hero, bool isAttacking ) override; virtual void HeroesActionComplete( Heroes & hero ) override; double getObjectValue( const Heroes & hero, int index, int objectID, double valueToIgnore ) const; diff --git a/src/fheroes2/ai/normal/ai_normal_battle.cpp b/src/fheroes2/ai/normal/ai_normal_battle.cpp index 686021805ee..1078bc28d09 100644 --- a/src/fheroes2/ai/normal/ai_normal_battle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_battle.cpp @@ -43,79 +43,10 @@ namespace AI const double STRENGTH_DISTANCE_FACTOR = 5.0; const std::vector underWallsIndicies = {7, 28, 49, 72, 95}; - void Normal::HeroesPreBattle( HeroBase & hero ) + void Normal::HeroesPreBattle( HeroBase & hero, bool isAttacking ) { - // Optimize troops placement before the battle - Army & army = hero.GetArmy(); - - std::vector archers; - std::vector others; - - // Validate and pick the troops - for ( size_t slot = 0; slot < ARMYMAXTROOPS; ++slot ) { - Troop * troop = army.GetTroop( slot ); - if ( troop && troop->isValid() ) { - if ( troop->isArchers() ) { - archers.push_back( *troop ); - } - else { - others.push_back( *troop ); - } - } - } - - // Sort troops by tactical priority. For melee: - // 1. Faster units first - // 2. Flyers first - // 3. Finally if unit type and speed is same, compare by strength - std::sort( others.begin(), others.end(), []( const Troop & left, const Troop & right ) { - if ( left.GetSpeed() == right.GetSpeed() ) { - if ( left.isFlying() == right.isFlying() ) { - return left.GetStrength() < right.GetStrength(); - } - return right.isFlying(); - } - return left.GetSpeed() < right.GetSpeed(); - } ); - - // Archers sorted purely by strength. - std::sort( archers.begin(), archers.end(), []( const Troop & left, const Troop & right ) { return left.GetStrength() < right.GetStrength(); } ); - - std::vector slotOrder = {2, 1, 3, 0, 4}; - switch ( archers.size() ) { - case 1: - slotOrder = {0, 2, 1, 3, 4}; - break; - case 2: - slotOrder = {0, 4, 2, 1, 3}; - break; - case 3: - slotOrder = {0, 4, 2, 1, 3}; - break; - case 4: - slotOrder = {0, 4, 2, 3, 1}; - break; - case 5: - slotOrder = {0, 4, 1, 2, 3}; - break; - default: - break; - } - - // Re-arrange troops in army - army.Clean(); - for ( const size_t slot : slotOrder ) { - if ( !archers.empty() ) { - army.GetTroop( slot )->Set( archers.back() ); - archers.pop_back(); - } - else if ( !others.empty() ) { - army.GetTroop( slot )->Set( others.back() ); - others.pop_back(); - } - else { - break; - } + if ( isAttacking ) { + OptimizeTroopsOrder( hero.GetArmy() ); } } diff --git a/src/fheroes2/ai/normal/ai_normal_castle.cpp b/src/fheroes2/ai/normal/ai_normal_castle.cpp index b89b7f0f888..f12763aa5b8 100644 --- a/src/fheroes2/ai/normal/ai_normal_castle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_castle.cpp @@ -152,6 +152,7 @@ namespace AI Build( castle, GetDefensiveStructures( castle.GetRace() ) ); castle.recruitBestAvailable( castle.GetKingdom().GetFunds() ); + OptimizeTroopsOrder( castle.GetArmy() ); } else { CastleDevelopment( castle ); diff --git a/src/fheroes2/battle/battle_main.cpp b/src/fheroes2/battle/battle_main.cpp index 20e879834be..3c51365be0f 100644 --- a/src/fheroes2/battle/battle_main.cpp +++ b/src/fheroes2/battle/battle_main.cpp @@ -68,7 +68,7 @@ Battle::Result Battle::Loader( Army & army1, Army & army2, s32 mapsindex ) if ( army1.GetCommander()->isCaptain() ) army1.GetCommander()->ActionPreBattle(); else if ( army1.isControlAI() ) - AI::Get().HeroesPreBattle( *army1.GetCommander() ); + AI::Get().HeroesPreBattle( *army1.GetCommander(), true ); else army1.GetCommander()->ActionPreBattle(); } @@ -78,7 +78,7 @@ Battle::Result Battle::Loader( Army & army1, Army & army2, s32 mapsindex ) if ( army2.GetCommander()->isCaptain() ) army2.GetCommander()->ActionPreBattle(); else if ( army2.isControlAI() ) - AI::Get().HeroesPreBattle( *army2.GetCommander() ); + AI::Get().HeroesPreBattle( *army2.GetCommander(), false ); else army2.GetCommander()->ActionPreBattle(); } @@ -142,7 +142,7 @@ Battle::Result Battle::Loader( Army & army1, Army & army2, s32 mapsindex ) // after battle army1 if ( army1.GetCommander() ) { if ( army1.isControlAI() ) - AI::Get().HeroesAfterBattle( *army1.GetCommander() ); + AI::Get().HeroesAfterBattle( *army1.GetCommander(), true ); else army1.GetCommander()->ActionAfterBattle(); } @@ -150,7 +150,7 @@ Battle::Result Battle::Loader( Army & army1, Army & army2, s32 mapsindex ) // after battle army2 if ( army2.GetCommander() ) { if ( army2.isControlAI() ) - AI::Get().HeroesAfterBattle( *army2.GetCommander() ); + AI::Get().HeroesAfterBattle( *army2.GetCommander(), false ); else army2.GetCommander()->ActionAfterBattle(); } From 240868ea538a9974293c12031e85e4bd6dddbcd3 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Sat, 27 Feb 2021 23:13:31 +0800 Subject: [PATCH 52/84] Stop color cycling in "View World" mode (#2878) --- src/fheroes2/kingdom/view_world.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/fheroes2/kingdom/view_world.cpp b/src/fheroes2/kingdom/view_world.cpp index 36e13c63220..71e92dc0bc3 100644 --- a/src/fheroes2/kingdom/view_world.cpp +++ b/src/fheroes2/kingdom/view_world.cpp @@ -130,7 +130,6 @@ namespace #else cachedImages.resize( 3 ); #endif - LocalEvent::Get().PauseCycling(); for ( size_t i = 0; i < cachedImages.size(); ++i ) { cachedImages[i].resize( world.w() * tileSizePerZoomLevel[i], world.h() * tileSizePerZoomLevel[i] ); @@ -175,8 +174,6 @@ namespace } } } - - LocalEvent::Get().ResumeCycling(); } }; @@ -415,6 +412,9 @@ void ViewWorld::ViewWorldWindow( const int color, const ViewWorldMode mode, Inte fheroes2::ImageRestorer restorer( display ); + LocalEvent & le = LocalEvent::Get(); + le.PauseCycling(); + // Creates fixed radar on top-right, even if hidden interface Interface::Radar radar = Interface::Radar::MakeRadarViewWorld( interface.GetRadar() ); @@ -467,7 +467,6 @@ void ViewWorld::ViewWorldWindow( const int color, const ViewWorldMode mode, Inte fheroes2::Point initRoiCenter; // message loop - LocalEvent & le = LocalEvent::Get(); while ( le.HandleEvents() ) { le.MousePressLeft( buttonExit.area() ) ? buttonExit.drawOnPress() : buttonExit.drawOnRelease(); le.MousePressLeft( buttonZoom.area() ) ? buttonZoom.drawOnPress() : buttonZoom.drawOnRelease(); @@ -488,7 +487,7 @@ void ViewWorld::ViewWorldWindow( const int color, const ViewWorldMode mode, Inte } else if ( le.MousePressLeft( visibleScreenInPixels ) ) { if ( isDrag ) { - Point newMousePos = le.GetMouseCursor(); + const Point newMousePos = le.GetMouseCursor(); const fheroes2::Point newRoiCenter( initRoiCenter.x - ( newMousePos.x - initMousePos.x ) * TILEWIDTH / tileSizePerZoomLevel[static_cast( currentROI._zoomLevel )], initRoiCenter.y - ( newMousePos.y - initMousePos.y ) * TILEWIDTH / tileSizePerZoomLevel[static_cast( currentROI._zoomLevel )] ); @@ -524,4 +523,6 @@ void ViewWorld::ViewWorldWindow( const int color, const ViewWorldMode mode, Inte } cursor.SetThemes( oldcursor ); + + le.ResumeCycling(); } From 927bcec5d17270a3be6e82c04529031a22f0ff08 Mon Sep 17 00:00:00 2001 From: Egor Sergeenko Date: Sun, 28 Feb 2021 03:48:03 +0100 Subject: [PATCH 53/84] Fix object's visited info and grammar (#2867) close #293 --- src/fheroes2/dialog/dialog_quickinfo.cpp | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/fheroes2/dialog/dialog_quickinfo.cpp b/src/fheroes2/dialog/dialog_quickinfo.cpp index ab83f397135..50bb8899d9f 100644 --- a/src/fheroes2/dialog/dialog_quickinfo.cpp +++ b/src/fheroes2/dialog/dialog_quickinfo.cpp @@ -263,22 +263,13 @@ std::string ShowLocalVisitObjectInfo( const Maps::Tiles & tile, const Heroes * h return str; } -std::string ShowGlobalVisitInfo( const Maps::Tiles & tile, const Kingdom & kingdom ) -{ - std::string str = MP2::StringObject( tile.GetObject() ); - str.append( "\n \n" ); - str.append( kingdom.isVisited( tile ) ? _( "(already visited)" ) : _( "(not visited)" ) ); - - return str; -} - std::string ShowGlobalVisitInfo( const Maps::Tiles & tile, const Kingdom & kingdom, bool showVisitedOption ) { std::string str = MP2::StringObject( tile.GetObject() ); - if ( showVisitedOption && kingdom.isVisited( tile ) ) { + if ( showVisitedOption ) { str.append( "\n \n" ); - str.append( _( "(already visited)" ) ); + str.append( kingdom.isVisited( tile ) ? _( "(already visited)" ) : _( "(not visited)" ) ); } return str; @@ -453,7 +444,7 @@ void Dialog::QuickInfo( const Maps::Tiles & tile ) const bool extendedScoutingOption = settings.ExtWorldScouteExtended(); if ( tile.isFog( settings.CurrentColor() ) ) - name_object = _( "Unchartered Territory" ); + name_object = _( "Uncharted Territory" ); else // check guardians mine if ( MP2::OBJ_ABANDONEDMINE == objectType || tile.CaptureObjectIsProtection() ) { @@ -547,7 +538,7 @@ void Dialog::QuickInfo( const Maps::Tiles & tile ) break; case MP2::OBJ_ARTESIANSPRING: - name_object = ShowGlobalVisitInfo( tile, kingdom ); + name_object = ShowGlobalVisitInfo( tile, kingdom, true ); break; case MP2::OBJ_MAGICWELL: @@ -576,7 +567,7 @@ void Dialog::QuickInfo( const Maps::Tiles & tile ) break; case MP2::OBJ_OBELISK: - name_object = ShowGlobalVisitInfo( tile, kingdom ); + name_object = ShowGlobalVisitInfo( tile, kingdom, true ); break; case MP2::OBJ_BARRIER: From f4445d53d49e66c48999f82197980ca7d3de6cd9 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Sun, 28 Feb 2021 20:17:32 +0800 Subject: [PATCH 54/84] Add experimental image loading and speed up rendering --- src/engine/image.cpp | 16 +++++------ src/engine/image_tool.cpp | 35 ++++++++++++++++++++++++ src/engine/image_tool.h | 2 ++ src/fheroes2/agg/agg.cpp | 40 ++++++++-------------------- src/fheroes2/agg/icn.h | 3 ++- src/fheroes2/agg/til.h | 2 ++ src/fheroes2/gui/interface_radar.cpp | 3 +-- 7 files changed, 59 insertions(+), 42 deletions(-) diff --git a/src/engine/image.cpp b/src/engine/image.cpp index 980c0935752..141d21133e5 100644 --- a/src/engine/image.cpp +++ b/src/engine/image.cpp @@ -919,12 +919,10 @@ namespace fheroes2 const uint8_t * imageOutXEnd = imageOutX + width; for ( ; imageOutX != imageOutXEnd; --imageInX, --transformInX, ++imageOutX ) { - if ( *transformInX == 1 ) { // skip pixel - continue; - } - if ( *transformInX > 0 ) { // apply a transformation - *imageOutX = *( transformTable + ( *transformInX ) * 256 + *imageOutX ); + if ( *transformInX != 1 ) { // skip pixel + *imageOutX = *( transformTable + ( *transformInX ) * 256 + *imageOutX ); + } } else { // copy a pixel *imageOutX = *imageInX; @@ -975,12 +973,10 @@ namespace fheroes2 const uint8_t * imageInXEnd = imageInX + width; for ( ; imageInX != imageInXEnd; ++imageInX, ++transformInX, ++imageOutX ) { - if ( *transformInX == 1 ) { // skip pixel - continue; - } - if ( *transformInX > 0 ) { // apply a transformation - *imageOutX = *( transformTable + ( *transformInX ) * 256 + *imageOutX ); + if ( *transformInX != 1 ) { // skip pixel + *imageOutX = *( transformTable + ( *transformInX ) * 256 + *imageOutX ); + } } else { // copy a pixel *imageOutX = *imageInX; diff --git a/src/engine/image_tool.cpp b/src/engine/image_tool.cpp index fd1a77c8d79..335586b03ac 100644 --- a/src/engine/image_tool.cpp +++ b/src/engine/image_tool.cpp @@ -110,6 +110,41 @@ namespace fheroes2 return SaveImage( temp, path ); } + bool Load( const std::string & path, Image & image ) + { + SDL_Surface * surface = SDL_LoadBMP( path.c_str() ); + if ( surface == nullptr ) { + return false; + } + + if ( surface->format->BytesPerPixel != 3 ) { + SDL_FreeSurface( surface ); + return false; + } + + image.resize( surface->w, surface->h ); + memset( image.transform(), 0, surface->w * surface->h ); + + const uint8_t * inY = reinterpret_cast( surface->pixels ); + uint8_t * outY = image.image(); + + const uint8_t * inYEnd = inY + surface->h * surface->pitch; + + for ( ; inY != inYEnd; inY += surface->pitch, outY += surface->w ) { + const uint8_t * inX = inY; + uint8_t * outX = outY; + const uint8_t * inXEnd = inX + surface->w * 3; + + for ( ; inX != inXEnd; inX += 3, ++outX ) { + *outX = GetColorId( *( inX + 2 ), *( inX + 1 ), *( inX ) ); + } + } + + SDL_FreeSurface( surface ); + + return true; + } + Sprite decodeICNSprite( const uint8_t * data, uint32_t sizeData, const int32_t width, const int32_t height, const int16_t offsetX, const int16_t offsetY ) { Sprite sprite( width, height, offsetX, offsetY ); diff --git a/src/engine/image_tool.h b/src/engine/image_tool.h index b826bf3e3e1..4df61de2da5 100644 --- a/src/engine/image_tool.h +++ b/src/engine/image_tool.h @@ -27,5 +27,7 @@ namespace fheroes2 // Save an image into file. 'background' represents palette index from the original palette bool Save( const Image & image, const std::string & path, uint8_t background = 23 ); + bool Load( const std::string & path, Image & image ); + Sprite decodeICNSprite( const uint8_t * data, uint32_t sizeData, const int32_t width, const int32_t height, const int16_t offsetX, const int16_t offsetY ); } diff --git a/src/fheroes2/agg/agg.cpp b/src/fheroes2/agg/agg.cpp index 50c50778d23..8a01f7e8eda 100644 --- a/src/fheroes2/agg/agg.cpp +++ b/src/fheroes2/agg/agg.cpp @@ -820,11 +820,9 @@ namespace fheroes2 { namespace AGG { - std::vector > _icnVsSprite; - const fheroes2::Sprite errorICNImage; - - std::vector > > _tilVsImage; - const fheroes2::Image errorTILImage; + std::vector > _icnVsSprite( ICN::LASTICN ); + std::vector > > _tilVsImage( TIL::LASTTIL ); + const fheroes2::Sprite errorImage; const uint32_t headerSize = 6; @@ -832,28 +830,12 @@ namespace fheroes2 bool IsValidICNId( int id ) { - if ( id < 0 ) { - return false; - } - - if ( _icnVsSprite.empty() ) { - _icnVsSprite.resize( ICN::LASTICN ); - } - - return static_cast( id ) < _icnVsSprite.size(); + return id >= 0 && static_cast( id ) < _icnVsSprite.size(); } bool IsValidTILId( int id ) { - if ( id < 0 ) { - return false; - } - - if ( _tilVsImage.empty() ) { - _tilVsImage.resize( TIL::LASTTIL ); - } - - return static_cast( id ) < _tilVsImage.size(); + return id >= 0 && static_cast( id ) < _tilVsImage.size(); } void LoadOriginalICN( int id ) @@ -1440,11 +1422,11 @@ namespace fheroes2 const Sprite & GetICN( int icnId, uint32_t index ) { if ( !IsValidICNId( icnId ) ) { - return errorICNImage; + return errorImage; } if ( index >= GetMaximumICNIndex( icnId ) ) { - return errorICNImage; + return errorImage; } if ( IsScalableICN( icnId ) ) { @@ -1466,16 +1448,16 @@ namespace fheroes2 const Image & GetTIL( int tilId, uint32_t index, uint32_t shapeId ) { if ( shapeId > 3 ) { - return errorTILImage; + return errorImage; } if ( !IsValidTILId( tilId ) ) { - return errorTILImage; + return errorImage; } const size_t maxTILIndex = GetMaximumTILIndex( tilId ); if ( index >= maxTILIndex ) { - return errorTILImage; + return errorImage; } return _tilVsImage[tilId][shapeId][index]; @@ -1484,7 +1466,7 @@ namespace fheroes2 const Sprite & GetLetter( uint32_t character, uint32_t fontType ) { if ( character < 0x21 ) { - return errorICNImage; + return errorImage; } // TODO: correct naming and standartise the code diff --git a/src/fheroes2/agg/icn.h b/src/fheroes2/agg/icn.h index c5afea3f374..c21a4791d49 100644 --- a/src/fheroes2/agg/icn.h +++ b/src/fheroes2/agg/icn.h @@ -919,7 +919,8 @@ namespace ICN MONSTER_SWITCH_LEFT_ARROW, MONSTER_SWITCH_RIGHT_ARROW, - LASTICN, // just a marker, indicating end of the enumeration + // IMPORTANT! Put any new entry just above this one. + LASTICN }; const char * GetString( int ); diff --git a/src/fheroes2/agg/til.h b/src/fheroes2/agg/til.h index c96280f47aa..1635151932f 100644 --- a/src/fheroes2/agg/til.h +++ b/src/fheroes2/agg/til.h @@ -31,6 +31,8 @@ namespace TIL CLOF32, GROUND32, STON, + + // IMPORTANT! Put any new entry just above this one. LASTTIL }; diff --git a/src/fheroes2/gui/interface_radar.cpp b/src/fheroes2/gui/interface_radar.cpp index 35ae3782a40..f96cb86b8d2 100644 --- a/src/fheroes2/gui/interface_radar.cpp +++ b/src/fheroes2/gui/interface_radar.cpp @@ -353,8 +353,7 @@ void Interface::Radar::RedrawObjects( int color, ViewWorldMode flags ) const fheroes2::Fill( display, dstx, dsty, sw, sw, fillColor ); } else { - if ( dstx < display.width() && dsty < display.height() ) - fheroes2::SetPixel( display, dstx, dsty, fillColor ); + fheroes2::SetPixel( display, dstx, dsty, fillColor ); } } } From cef14e2d36676d48ede74087c20d28ae086006a7 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Sun, 28 Feb 2021 22:44:33 +0800 Subject: [PATCH 55/84] Disable retreat for auto battle mode (#2881) relates to #2554 --- src/fheroes2/ai/normal/ai_normal_battle.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/fheroes2/ai/normal/ai_normal_battle.cpp b/src/fheroes2/ai/normal/ai_normal_battle.cpp index 1078bc28d09..55dcb2206e8 100644 --- a/src/fheroes2/ai/normal/ai_normal_battle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_battle.cpp @@ -146,7 +146,8 @@ namespace AI // Step 2. Check retreat/surrender condition const Heroes * actualHero = dynamic_cast( commander ); - if ( actualHero && arena.CanRetreatOpponent( _myColor ) && isHeroWorthSaving( actualHero ) && checkRetreatCondition( _myArmyStrength, _enemyArmyStrength ) ) { + if ( actualHero && !actualHero->isControlHuman() && arena.CanRetreatOpponent( _myColor ) && isHeroWorthSaving( actualHero ) + && checkRetreatCondition( _myArmyStrength, _enemyArmyStrength ) ) { // Cast maximum damage spell actions = forceSpellcastBeforeRetreat( arena, commander ); From 881d3bd76f14cbf2fd4eccad5aec988a00bb89e5 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Sun, 28 Feb 2021 23:37:41 +0800 Subject: [PATCH 56/84] Remove few buggy experimental options (#2882) relates to #1272 - "heroes: surrendering gives some experience" - "heroes: recalculate movement points after creatures movement" --- src/fheroes2/ai/ai_hero_action.cpp | 14 +------------- src/fheroes2/castle/castle_dialog.cpp | 3 --- src/fheroes2/dialog/dialog_settings.cpp | 2 -- src/fheroes2/heroes/heroes_action.cpp | 23 +---------------------- src/fheroes2/heroes/heroes_meeting.cpp | 5 ----- src/fheroes2/system/settings.cpp | 18 ------------------ src/fheroes2/system/settings.h | 5 ----- 7 files changed, 2 insertions(+), 68 deletions(-) diff --git a/src/fheroes2/ai/ai_hero_action.cpp b/src/fheroes2/ai/ai_hero_action.cpp index e32fd67e9c8..cf6e6149672 100644 --- a/src/fheroes2/ai/ai_hero_action.cpp +++ b/src/fheroes2/ai/ai_hero_action.cpp @@ -170,19 +170,7 @@ namespace AI void AIBattleLose( Heroes & hero, const Battle::Result & res, bool attacker, int color = Color::NONE, const Point * centerOn = nullptr ) { - u32 reason = attacker ? res.AttackerResult() : res.DefenderResult(); - - if ( Settings::Get().ExtHeroSurrenderingGiveExp() && Battle::RESULT_SURRENDER == reason ) { - const uint32_t exp = attacker ? res.GetExperienceAttacker() : res.GetExperienceDefender(); - - if ( hero.isControlHuman() ) { - std::string msg = _( "Hero %{name} also got a %{count} experience." ); - StringReplace( msg, "%{name}", hero.GetName() ); - StringReplace( msg, "%{count}", exp ); - Dialog::Message( "", msg, Font::BIG, Dialog::OK ); - } - hero.IncreaseExperience( exp ); - } + const uint32_t reason = attacker ? res.AttackerResult() : res.DefenderResult(); if ( centerOn != nullptr ) { Interface::Basic::Get().GetGameArea().SetCenter( *centerOn ); diff --git a/src/fheroes2/castle/castle_dialog.cpp b/src/fheroes2/castle/castle_dialog.cpp index ef1caf78229..abdd1470088 100644 --- a/src/fheroes2/castle/castle_dialog.cpp +++ b/src/fheroes2/castle/castle_dialog.cpp @@ -742,9 +742,6 @@ int Castle::OpenDialog( bool readonly ) BuyBuilding( build ); } - if ( heroes.Guest() && conf.ExtHeroRecalculateMovement() ) - heroes.Guest()->RecalculateMovePoints(); - if ( conf.ExtGameDynamicInterface() ) conf.SetEvilInterface( interface ); diff --git a/src/fheroes2/dialog/dialog_settings.cpp b/src/fheroes2/dialog/dialog_settings.cpp index be9ef266e5c..9f4a0662f7a 100644 --- a/src/fheroes2/dialog/dialog_settings.cpp +++ b/src/fheroes2/dialog/dialog_settings.cpp @@ -197,8 +197,6 @@ void Dialog::ExtSettings( bool readonly ) states.push_back( Settings::HEROES_BUY_BOOK_FROM_SHRINES ); states.push_back( Settings::HEROES_COST_DEPENDED_FROM_LEVEL ); states.push_back( Settings::HEROES_REMEMBER_POINTS_RETREAT ); - states.push_back( Settings::HEROES_SURRENDERING_GIVE_EXP ); - states.push_back( Settings::HEROES_RECALCULATE_MOVEMENT ); states.push_back( Settings::HEROES_TRANSCRIBING_SCROLLS ); states.push_back( Settings::HEROES_ARENA_ANY_SKILLS ); diff --git a/src/fheroes2/heroes/heroes_action.cpp b/src/fheroes2/heroes/heroes_action.cpp index 5cb82d687fa..0c22baa8154 100644 --- a/src/fheroes2/heroes/heroes_action.cpp +++ b/src/fheroes2/heroes/heroes_action.cpp @@ -284,19 +284,7 @@ uint32_t DialogLuck( const std::string & hdr, const std::string & msg, bool good void BattleLose( Heroes & hero, const Battle::Result & res, bool attacker, int color = Color::NONE ) { - u32 reason = attacker ? res.AttackerResult() : res.DefenderResult(); - - if ( Settings::Get().ExtHeroSurrenderingGiveExp() && Battle::RESULT_SURRENDER == reason ) { - const uint32_t exp = attacker ? res.GetExperienceAttacker() : res.GetExperienceDefender(); - - if ( hero.isControlHuman() ) { - std::string msg = _( "Hero %{name} also got a %{count} experience." ); - StringReplace( msg, "%{name}", hero.GetName() ); - StringReplace( msg, "%{count}", exp ); - Dialog::Message( "", msg, Font::BIG, Dialog::OK ); - } - hero.IncreaseExperience( exp ); - } + const uint32_t reason = attacker ? res.AttackerResult() : res.DefenderResult(); AGG::PlaySound( M82::KILLFADE ); hero.FadeOut(); @@ -340,9 +328,6 @@ void RecruitMonsterFromTile( Heroes & hero, Maps::Tiles & tile, const std::strin hero.GetArmy().JoinTroop( troop(), recruit ); hero.MovePointsScaleFixed(); - if ( Settings::Get().ExtHeroRecalculateMovement() ) - hero.RecalculateMovePoints(); - Interface::Basic::Get().GetStatusWindow().SetRedraw(); } } @@ -2171,9 +2156,6 @@ void ActionToDwellingJoinMonster( Heroes & hero, u32 obj, s32 dst_index ) hero.GetArmy().JoinTroop( troop ); hero.MovePointsScaleFixed(); - if ( Settings::Get().ExtHeroRecalculateMovement() ) - hero.RecalculateMovePoints(); - Interface::Basic::Get().GetStatusWindow().SetRedraw(); } } @@ -2554,9 +2536,6 @@ void ActionToUpgradeArmyObject( Heroes & hero, u32 obj ) offsetX += border.width() + 4; } Dialog::SpriteInfo( MP2::StringObject( obj ), msg1, surface ); - - if ( Settings::Get().ExtHeroRecalculateMovement() ) - hero.RecalculateMovePoints(); } else { Dialog::Message( MP2::StringObject( obj ), msg2, Font::BIG, Dialog::OK ); diff --git a/src/fheroes2/heroes/heroes_meeting.cpp b/src/fheroes2/heroes/heroes_meeting.cpp index 987ddff5ddf..16f15f32f33 100644 --- a/src/fheroes2/heroes/heroes_meeting.cpp +++ b/src/fheroes2/heroes/heroes_meeting.cpp @@ -447,11 +447,6 @@ void Heroes::MeetingDialog( Heroes & heroes2 ) } } - if ( Settings::Get().ExtHeroRecalculateMovement() ) { - RecalculateMovePoints(); - heroes2.RecalculateMovePoints(); - } - backPrimary.reset(); armyCountBackgroundRestorer.reset(); restorer.restore(); diff --git a/src/fheroes2/system/settings.cpp b/src/fheroes2/system/settings.cpp index 5d90a73dd63..f5fd35b7cf6 100644 --- a/src/fheroes2/system/settings.cpp +++ b/src/fheroes2/system/settings.cpp @@ -257,14 +257,6 @@ const settings_t settingsFHeroes2[] = { Settings::HEROES_REMEMBER_POINTS_RETREAT, _( "heroes: remember move points for retreat/surrender result" ), }, - { - Settings::HEROES_SURRENDERING_GIVE_EXP, - _( "heroes: surrendering gives some experience" ), - }, - { - Settings::HEROES_RECALCULATE_MOVEMENT, - _( "heroes: recalculate movement points after creatures movement" ), - }, { Settings::HEROES_TRANSCRIBING_SCROLLS, _( "heroes: allow transcribing scrolls (needs: Eye Eagle skill)" ), @@ -1601,16 +1593,6 @@ bool Settings::ExtHeroRememberPointsForRetreating( void ) const return ExtModes( HEROES_REMEMBER_POINTS_RETREAT ); } -bool Settings::ExtHeroSurrenderingGiveExp( void ) const -{ - return ExtModes( HEROES_SURRENDERING_GIVE_EXP ); -} - -bool Settings::ExtHeroRecalculateMovement( void ) const -{ - return ExtModes( HEROES_RECALCULATE_MOVEMENT ); -} - bool Settings::ExtUnionsAllowCastleVisiting( void ) const { return ExtModes( UNIONS_ALLOW_CASTLE_VISITING ); diff --git a/src/fheroes2/system/settings.h b/src/fheroes2/system/settings.h index 9837967acbc..8eca7471dac 100644 --- a/src/fheroes2/system/settings.h +++ b/src/fheroes2/system/settings.h @@ -98,8 +98,6 @@ class Settings CASTLE_ALLOW_GUARDIANS = 0x20080000, HEROES_COST_DEPENDED_FROM_LEVEL = 0x20800000, HEROES_REMEMBER_POINTS_RETREAT = 0x21000000, - HEROES_SURRENDERING_GIVE_EXP = 0x22000000, - HEROES_RECALCULATE_MOVEMENT = 0x24000000, CASTLE_MAGEGUILD_POINTS_TURN = 0x30000001, WORLD_STARTHERO_LOSSCOND4HUMANS = 0x30000008, @@ -195,8 +193,6 @@ class Settings bool ExtHeroBuySpellBookFromShrine( void ) const; bool ExtHeroRecruitCostDependedFromLevel( void ) const; bool ExtHeroRememberPointsForRetreating( void ) const; - bool ExtHeroSurrenderingGiveExp( void ) const; - bool ExtHeroRecalculateMovement( void ) const; bool ExtHeroAllowTranscribingScroll( void ) const; bool ExtHeroArenaCanChoiseAnySkills( void ) const; bool ExtUnionsAllowCastleVisiting( void ) const; @@ -335,7 +331,6 @@ class Settings } protected: - void Parse( const std::string & left, const std::string & right ); void PostLoad( void ); private: From 78c6d48b9cf9ebdfad322392d53d95c481d217ca Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Mon, 1 Mar 2021 21:49:04 +0800 Subject: [PATCH 57/84] Fix assertion at the start of battle (#2889) relates to #2888 --- src/fheroes2/battle/battle_board.cpp | 4 ++-- src/fheroes2/battle/battle_board.h | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/fheroes2/battle/battle_board.cpp b/src/fheroes2/battle/battle_board.cpp index c9c9d598923..65bbc7029c9 100644 --- a/src/fheroes2/battle/battle_board.cpp +++ b/src/fheroes2/battle/battle_board.cpp @@ -808,7 +808,7 @@ void Battle::Board::SetCobjObjects( const Maps::Tiles & tile ) const bool checkRightCell = isTwoHexObject( objs[i] ); int32_t dest = GetRandomObstaclePosition(); - while ( at( dest ).GetObject() != 0 && ( !checkRightCell || at( dest + 1 ).GetObject() != 0 ) ) { + while ( at( dest ).GetObject() != 0 || ( checkRightCell && at( dest + 1 ).GetObject() != 0 ) ) { dest = GetRandomObstaclePosition(); } @@ -816,7 +816,7 @@ void Battle::Board::SetCobjObjects( const Maps::Tiles & tile ) } } -void Battle::Board::SetCobjObject( int icn, s32 dst ) +void Battle::Board::SetCobjObject( const int icn, const int32_t dst ) { at( dst ).SetObject( 0x80 + ( icn - ICN::COBJ0000 ) ); diff --git a/src/fheroes2/battle/battle_board.h b/src/fheroes2/battle/battle_board.h index 2f06bea5793..7932681812a 100644 --- a/src/fheroes2/battle/battle_board.h +++ b/src/fheroes2/battle/battle_board.h @@ -65,7 +65,6 @@ namespace Battle void SetScanPassability( const Unit & ); void SetCobjObjects( const Maps::Tiles & ); - void SetCobjObject( int icn, s32 ); void SetCovrObjects( int icn ); static std::string GetMoatInfo( void ); @@ -107,6 +106,9 @@ namespace Battle CASTLE_TOP_GATE_TOWER_POS = 40, CASTLE_BOTTOM_GATE_TOWER_POS = 62 }; + + private: + void SetCobjObject( const int icn, const int32_t dst ); }; } From eee4c1b951f40f49f9bd732bd414eecf26d397bb Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Mon, 1 Mar 2021 22:06:23 +0800 Subject: [PATCH 58/84] Add PS Vita background image (#2891) --- files/images/platform/psv/background.png | Bin 0 -> 269940 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 files/images/platform/psv/background.png diff --git a/files/images/platform/psv/background.png b/files/images/platform/psv/background.png new file mode 100644 index 0000000000000000000000000000000000000000..e86d2278b6b205a45020236fd34195bfa0cab9ec GIT binary patch literal 269940 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�D=BbT3kfceeL4YrkX(~ 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%0A7+!>HYImK-GCVuS$3DiJLTUw0|(t%ZYO2KMMu>o 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

V`D-3{-c=bnpb4aKx4$y- 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 a|9=1x-0?gs?Bf#v0000 Date: Mon, 1 Mar 2021 23:09:25 +0800 Subject: [PATCH 59/84] Add Playstation Vita icon (#2892) --- files/images/platform/psv/icon.png | Bin 0 -> 22583 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 files/images/platform/psv/icon.png diff --git a/files/images/platform/psv/icon.png b/files/images/platform/psv/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c2ec2556fad8775454294b0619f330215e8ad99d GIT binary patch literal 22583 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 literal 0 HcmV?d00001 From 7ef42ee669c8d5766b4aaa66a19a8c4122a032a2 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Tue, 2 Mar 2021 20:19:12 +0800 Subject: [PATCH 60/84] Update image tools (#2893) --- src/engine/image_tool.cpp | 94 +++++++++++++++++++++++++++++++-------- src/engine/image_tool.h | 6 ++- 2 files changed, 79 insertions(+), 21 deletions(-) diff --git a/src/engine/image_tool.cpp b/src/engine/image_tool.cpp index 335586b03ac..0a1846eba10 100644 --- a/src/engine/image_tool.cpp +++ b/src/engine/image_tool.cpp @@ -64,9 +64,19 @@ namespace const uint8_t * in = image.image(); if ( surface->format->Amask > 0 ) { - for ( ; out != outEnd; ++out, ++in ) { - const uint8_t * value = currentPalette + *in * 3; - *out = SDL_MapRGBA( surface->format, *( value ), *( value + 1 ), *( value + 2 ), 255 ); + const uint8_t * transform = image.transform(); + + for ( ; out != outEnd; ++out, ++in, ++transform ) { + if ( *transform == 1 ) { + *out = SDL_MapRGBA( surface->format, 0, 0, 0, 0 ); + } + else if ( *transform == 2 ) { + *out = SDL_MapRGBA( surface->format, 0, 0, 0, 64 ); + } + else { + const uint8_t * value = currentPalette + *in * 3; + *out = SDL_MapRGBA( surface->format, *( value ), *( value + 1 ), *( value + 2 ), 255 ); + } } } else { @@ -110,6 +120,14 @@ namespace fheroes2 return SaveImage( temp, path ); } + bool Save( const Image & image, const std::string & path ) + { + if ( image.empty() || path.empty() ) + return false; + + return SaveImage( image, path ); + } + bool Load( const std::string & path, Image & image ) { SDL_Surface * surface = SDL_LoadBMP( path.c_str() ); @@ -117,28 +135,66 @@ namespace fheroes2 return false; } - if ( surface->format->BytesPerPixel != 3 ) { - SDL_FreeSurface( surface ); - return false; - } + if ( surface->format->BytesPerPixel == 3 ) { + image.resize( surface->w, surface->h ); + memset( image.transform(), 0, surface->w * surface->h ); - image.resize( surface->w, surface->h ); - memset( image.transform(), 0, surface->w * surface->h ); + const uint8_t * inY = reinterpret_cast( surface->pixels ); + uint8_t * outY = image.image(); - const uint8_t * inY = reinterpret_cast( surface->pixels ); - uint8_t * outY = image.image(); + const uint8_t * inYEnd = inY + surface->h * surface->pitch; - const uint8_t * inYEnd = inY + surface->h * surface->pitch; + for ( ; inY != inYEnd; inY += surface->pitch, outY += surface->w ) { + const uint8_t * inX = inY; + uint8_t * outX = outY; + const uint8_t * inXEnd = inX + surface->w * 3; - for ( ; inY != inYEnd; inY += surface->pitch, outY += surface->w ) { - const uint8_t * inX = inY; - uint8_t * outX = outY; - const uint8_t * inXEnd = inX + surface->w * 3; - - for ( ; inX != inXEnd; inX += 3, ++outX ) { - *outX = GetColorId( *( inX + 2 ), *( inX + 1 ), *( inX ) ); + for ( ; inX != inXEnd; inX += 3, ++outX ) { + *outX = GetColorId( *( inX + 2 ), *( inX + 1 ), *( inX ) ); + } } } + else if ( surface->format->BytesPerPixel == 4 ) { + image.resize( surface->w, surface->h ); + image.reset(); + + const uint8_t * inY = reinterpret_cast( surface->pixels ); + uint8_t * outY = image.image(); + uint8_t * transformY = image.transform(); + + const uint8_t * inYEnd = inY + surface->h * surface->pitch; + + for ( ; inY != inYEnd; inY += surface->pitch, outY += surface->w, transformY += surface->w ) { + const uint8_t * inX = inY; + uint8_t * outX = outY; + uint8_t * transformX = transformY; + const uint8_t * inXEnd = inX + surface->w * 4; + + for ( ; inX != inXEnd; inX += 4, ++outX, ++transformX ) { + const uint8_t alpha = *( inX + 3 ); + if ( alpha < 255 ) { + if ( alpha == 0 ) { + *transformX = 1; + } + else if ( *( inX ) == 0 && *( inX + 1 ) == 0 && *( inX + 2 ) == 0 ) { + *transformX = 2; + } + else { + *outX = GetColorId( *( inX + 2 ), *( inX + 1 ), *( inX ) ); + *transformX = 0; + } + } + else { + *outX = GetColorId( *( inX + 2 ), *( inX + 1 ), *( inX ) ); + *transformX = 0; + } + } + } + } + else { + SDL_FreeSurface( surface ); + return false; + } SDL_FreeSurface( surface ); diff --git a/src/engine/image_tool.h b/src/engine/image_tool.h index 4df61de2da5..5c144cb05c0 100644 --- a/src/engine/image_tool.h +++ b/src/engine/image_tool.h @@ -24,8 +24,10 @@ namespace fheroes2 { - // Save an image into file. 'background' represents palette index from the original palette - bool Save( const Image & image, const std::string & path, uint8_t background = 23 ); + bool Save( const Image & image, const std::string & path ); + + // Save an image into file. 'background' represents palette index from the original palette. Default value is 23. + bool Save( const Image & image, const std::string & path, uint8_t background ); bool Load( const std::string & path, Image & image ); From f4e8e5a84149596d4cd6556238cdf2e6e60d3254 Mon Sep 17 00:00:00 2001 From: eos428 <38433056+eos428@users.noreply.github.com> Date: Tue, 2 Mar 2021 22:35:04 +0700 Subject: [PATCH 61/84] Fix Split-related issues, and Implement some new ones (#2850) close #2823 close #2824 close #2679 close #2671 --- src/fheroes2/army/army.cpp | 55 ++++++--- src/fheroes2/army/army.h | 2 +- src/fheroes2/army/army_bar.cpp | 130 +++++++++++++-------- src/fheroes2/dialog/dialog.h | 2 +- src/fheroes2/dialog/dialog_selectcount.cpp | 114 +++++++++--------- 5 files changed, 169 insertions(+), 134 deletions(-) diff --git a/src/fheroes2/army/army.cpp b/src/fheroes2/army/army.cpp index bf9cb73b021..b9b9740341b 100644 --- a/src/fheroes2/army/army.cpp +++ b/src/fheroes2/army/army.cpp @@ -767,27 +767,44 @@ void Troops::DrawMons32Line( int32_t cx, int32_t cy, uint32_t width, uint32_t fi } } -void Troops::SplitTroopIntoFreeSlots( const Troop & troop, u32 slots ) -{ - if ( slots && slots <= ( Size() - GetCount() ) ) { - u32 chunk = troop.GetCount() / slots; - u32 limits = slots; - std::vector iters; - - for ( iterator it = begin(); it != end(); ++it ) - if ( !( *it )->isValid() && limits ) { - iters.push_back( it ); - ( *it )->Set( troop.GetMonster(), chunk ); - --limits; - } +void Troops::SplitTroopIntoFreeSlots( const Troop & troop, const Troop & selectedSlot, const uint32_t slots ) +{ + if ( slots < 1 || slots > ( Size() - GetCount() ) ) + return; - u32 last = troop.GetCount() - chunk * slots; + const uint32_t chunk = troop.GetCount() / slots; + uint32_t remainingCount = troop.GetCount() % slots; + uint32_t remainingSlots = slots; - for ( std::vector::iterator it = iters.begin(); it != iters.end(); ++it ) - if ( last ) { - ( **it )->SetCount( ( **it )->GetCount() + 1 ); - --last; - } + auto TryCreateTroopChunk = [&remainingSlots, &remainingCount, chunk, troop]( Troop & newTroop ) { + if ( remainingSlots <= 0 ) + return; + + if ( !newTroop.isValid() ) { + newTroop.Set( troop.GetMonster(), remainingCount > 0 ? chunk + 1 : chunk ); + --remainingSlots; + + if ( remainingCount > 0 ) + --remainingCount; + } + }; + + const iterator selectedSlotIterator = std::find( begin(), end(), &selectedSlot ); + + // this means the selected slot is actually not part of the army, which is not the intended logic + if ( selectedSlotIterator == end() ) + return; + + const size_t iteratorIndex = selectedSlotIterator - begin(); + + // try to create chunks to the right of the selected slot + for ( size_t i = iteratorIndex + 1; i < Size(); ++i ) { + TryCreateTroopChunk( *GetTroop( i ) ); + } + + // this time, try to create chunks to the left of the selected slot + for ( int i = static_cast( iteratorIndex ) - 1; i >= 0; --i ) { + TryCreateTroopChunk( *GetTroop( i ) ); } } diff --git a/src/fheroes2/army/army.h b/src/fheroes2/army/army.h index 2fed3537a2e..861094a91ee 100644 --- a/src/fheroes2/army/army.h +++ b/src/fheroes2/army/army.h @@ -106,7 +106,7 @@ class Troops : protected std::vector void KeepOnlyWeakest( Troops &, bool ); void DrawMons32Line( int32_t, int32_t, uint32_t, uint32_t, uint32_t, uint32_t, bool, bool ) const; - void SplitTroopIntoFreeSlots( const Troop &, u32 slots ); + void SplitTroopIntoFreeSlots( const Troop & troop, const Troop & selectedSlot, const uint32_t slots ); void AssignToFirstFreeSlot( const Troop &, const uint32_t splitCount ); void JoinAllTroopsOfType( const Troop & targetTroop ); }; diff --git a/src/fheroes2/army/army_bar.cpp b/src/fheroes2/army/army_bar.cpp index e51102294de..71bda82ec68 100644 --- a/src/fheroes2/army/army_bar.cpp +++ b/src/fheroes2/army/army_bar.cpp @@ -45,55 +45,94 @@ void RedistributeArmy( ArmyTroop & troopFrom, ArmyTroop & troopTarget, Army * ar isTroopInfoVisible = false; } else { - const uint32_t freeSlots = ( armyFrom == armyTarget ? 1 : 0 ) + armyTarget->Size() - armyTarget->GetCount(); + uint32_t freeSlots = 1 + armyTarget->Size() - armyTarget->GetCount(); + const bool isSameTroopType = troopFrom.GetID() == troopTarget.GetID(); + + if ( isSameTroopType ) + ++freeSlots; + const uint32_t maxCount = saveLastTroop ? troopFrom.GetCount() - 1 : troopFrom.GetCount(); - uint32_t redistributeCount = troopFrom.GetCount() / 2; - const uint32_t slots = Dialog::ArmySplitTroop( ( freeSlots > maxCount ? maxCount : freeSlots ), maxCount, redistributeCount, saveLastTroop ); - - switch ( slots ) { - case 3: - case 4: - case 5: - if ( saveLastTroop ) { - const Troop troop( troopFrom, troopFrom.GetCount() - 1 ); - troopFrom.SetCount( 1 ); - armyTarget->SplitTroopIntoFreeSlots( troop, slots ); - } - else { - const Troop troop( troopFrom ); - troopFrom.Reset(); - armyTarget->SplitTroopIntoFreeSlots( troop, slots ); - } - break; + uint32_t redistributeCount = isSameTroopType ? 1 : troopFrom.GetCount() / 2; + + // if splitting to the same troop type, use this bool to turn off fast split option at the beginning of the dialog + bool useFastSplit = !isSameTroopType; + const uint32_t slots = Dialog::ArmySplitTroop( ( freeSlots > maxCount ? maxCount : freeSlots ), maxCount, saveLastTroop, redistributeCount, useFastSplit ); + + if ( slots < 2 || slots > 6 ) + return; + + uint32_t totalSplitTroopCount = troopFrom.GetCount(); - case 2: + if ( !useFastSplit && slots == 2 ) { // this logic is used when splitting to a stack with the same unit if ( troopFrom.GetID() == troopTarget.GetID() ) troopTarget.SetCount( troopTarget.GetCount() + redistributeCount ); else troopTarget.Set( troopFrom, redistributeCount ); - troopFrom.SetCount( troopFrom.GetCount() - redistributeCount ); - break; + troopFrom.SetCount( totalSplitTroopCount - redistributeCount ); + } + else { + if ( isSameTroopType ) + totalSplitTroopCount += troopTarget.GetCount(); - default: - break; + const uint32_t troopFromSplitCount = ( totalSplitTroopCount + slots - 1 ) / slots; + troopFrom.SetCount( troopFromSplitCount ); + + const uint32_t troopTargetSplitCount = ( totalSplitTroopCount + slots - 2 ) / slots; + + if ( !isSameTroopType ) + troopTarget.SetMonster( troopFrom.GetID() ); + + troopTarget.SetCount( troopTargetSplitCount ); + + totalSplitTroopCount -= troopFromSplitCount; + totalSplitTroopCount -= troopTargetSplitCount; + armyTarget->SplitTroopIntoFreeSlots( Troop( troopFrom, totalSplitTroopCount ), troopTarget, slots - 2 ); } } } -void RedistributeArmyByOne( ArmyTroop & troopFrom, Army * armyTarget ) +void RedistributeTroopToFirstFreeSlot( ArmyTroop & troopFrom, Army * armyTarget, const uint32_t count ) { - // can't split up a stack with just 1 unit... - if ( troopFrom.GetCount() <= 1 ) + // can't split up a stack with just 1 unit, and obviously on count == 0, there's no splitting at all + if ( troopFrom.GetCount() <= 1 || count == 0 ) return; const uint32_t freeSlots = armyTarget->Size() - armyTarget->GetCount(); if ( freeSlots == 0 ) return; - armyTarget->AssignToFirstFreeSlot( troopFrom, 1 ); - troopFrom.SetCount( troopFrom.GetCount() - 1 ); + armyTarget->AssignToFirstFreeSlot( troopFrom, count ); + troopFrom.SetCount( troopFrom.GetCount() - count ); +} + +void RedistributeTroopByOne( ArmyTroop & troopFrom, Army * armyTarget ) +{ + RedistributeTroopToFirstFreeSlot( troopFrom, armyTarget, 1 ); +} + +void RedistributeTroopEvenly( ArmyTroop & troopFrom, Army * armyTarget ) +{ + RedistributeTroopToFirstFreeSlot( troopFrom, armyTarget, troopFrom.GetCount() / 2 ); +} + +bool IsSplitHotkeyUsed( ArmyTroop & troopFrom, Army * armyTarget ) +{ + if ( Game::HotKeyHoldEvent( Game::EVENT_STACKSPLIT_CTRL ) ) { + RedistributeTroopByOne( troopFrom, armyTarget ); + return true; + } + else if ( Game::HotKeyHoldEvent( Game::EVENT_JOINSTACKS ) ) { + armyTarget->JoinAllTroopsOfType( troopFrom ); + return true; + } + else if ( Game::HotKeyHoldEvent( Game::EVENT_STACKSPLIT_SHIFT ) ) { + RedistributeTroopEvenly( troopFrom, armyTarget ); + return true; + } + + return false; } ArmyBar::ArmyBar( Army * ptr, bool mini, bool ro, bool change /* false */ ) @@ -240,7 +279,7 @@ void ArmyBar::Redraw( fheroes2::Image & dstsf ) bool ArmyBar::ActionBarCursor( ArmyTroop & troop ) { if ( troop.isValid() && LocalEvent::Get().MouseClickMiddle() ) { - RedistributeArmyByOne( troop, _army ); + RedistributeTroopByOne( troop, _army ); return true; } @@ -304,12 +343,11 @@ bool ArmyBar::ActionBarLeftMouseSingleClick( ArmyTroop & troop ) { if ( isSelected() ) { ArmyTroop * selectedTroop = GetSelectedItem(); - if ( selectedTroop && selectedTroop->isValid() && Game::HotKeyHoldEvent( Game::EVENT_STACKSPLIT_SHIFT ) ) { // redistribute when clicked troop is empty or is the same one as the selected troop if ( !troop.isValid() || troop.GetID() == selectedTroop->GetID() ) { - ResetSelected(); RedistributeArmy( *selectedTroop, troop, _army, _isTroopInfoVisible ); + ResetSelected(); return false; } @@ -330,14 +368,8 @@ bool ArmyBar::ActionBarLeftMouseSingleClick( ArmyTroop & troop ) else if ( troop.isValid() ) { if ( !read_only ) // select { - if ( Game::HotKeyHoldEvent( Game::EVENT_STACKSPLIT_CTRL ) ) { - RedistributeArmyByOne( troop, _army ); - return false; - } - else if ( Game::HotKeyHoldEvent( Game::EVENT_JOINSTACKS ) ) { - _army->JoinAllTroopsOfType( troop ); + if ( IsSplitHotkeyUsed( troop, _army ) ) return false; - } Cursor::Get().Hide(); spcursor.hide(); @@ -392,8 +424,8 @@ bool ArmyBar::ActionBarLeftMouseSingleClick( ArmyTroop & destTroop, ArmyTroop & { if ( Game::HotKeyHoldEvent( Game::EVENT_STACKSPLIT_SHIFT ) ) { if ( destTroop.isEmpty() || destTroop.GetID() == selectedTroop.GetID() ) { - ResetSelected(); RedistributeArmy( selectedTroop, destTroop, _army, _isTroopInfoVisible ); + ResetSelected(); } return false; } @@ -431,15 +463,9 @@ bool ArmyBar::ActionBarLeftMouseSingleClick( ArmyTroop & destTroop, ArmyTroop & bool ArmyBar::ActionBarLeftMouseDoubleClick( ArmyTroop & troop ) { - if ( troop.isValid() && !read_only ) { - if ( Game::HotKeyHoldEvent( Game::EVENT_STACKSPLIT_CTRL ) ) { - RedistributeArmyByOne( troop, _army ); - return false; - } - else if ( Game::HotKeyHoldEvent( Game::EVENT_JOINSTACKS ) ) { - _army->JoinAllTroopsOfType( troop ); - return false; - } + if ( troop.isValid() && !read_only && IsSplitHotkeyUsed( troop, _army ) ) { + ResetSelected(); + return false; } const ArmyTroop * troop2 = GetSelectedItem(); @@ -535,8 +561,9 @@ bool ArmyBar::ActionBarRightMouseSingleClick( ArmyTroop & troop ) return false; if ( !troop.isValid() || selectedTroop.GetID() == troop.GetID() ) { - ResetSelected(); RedistributeArmy( selectedTroop, troop, _army, _isTroopInfoVisible ); + ResetSelected(); + return true; } @@ -546,8 +573,9 @@ bool ArmyBar::ActionBarRightMouseSingleClick( ArmyTroop & troop ) bool ArmyBar::ActionBarRightMouseSingleClick( ArmyTroop & destTroop, ArmyTroop & selectedTroop ) { if ( !destTroop.isValid() || destTroop.GetID() == selectedTroop.GetID() ) { - ResetSelected(); RedistributeArmy( selectedTroop, destTroop, _army, _isTroopInfoVisible ); + ResetSelected(); + return true; } diff --git a/src/fheroes2/dialog/dialog.h b/src/fheroes2/dialog/dialog.h index 668fd4a983e..d5cbca2d08a 100644 --- a/src/fheroes2/dialog/dialog.h +++ b/src/fheroes2/dialog/dialog.h @@ -108,7 +108,7 @@ namespace Dialog int ArmyInfo( const Troop & troop, int flags, bool isReflected = false ); int ArmyJoinFree( const Troop &, Heroes & ); int ArmyJoinWithCost( const Troop &, u32 join, u32 gold, Heroes & ); - int ArmySplitTroop( int free_slots, u32 max, u32 &, bool ); + int ArmySplitTroop( const uint32_t freeSlots, const uint32_t redistributeMax, const bool saveLastTroop, uint32_t & redistributeCount, bool & useFastSplit ); void Marketplace( bool fromTradingPost = false ); void MakeGiftResource( void ); int BuyBoat( bool enable ); diff --git a/src/fheroes2/dialog/dialog_selectcount.cpp b/src/fheroes2/dialog/dialog_selectcount.cpp index 4a208663a4e..84505b88a09 100644 --- a/src/fheroes2/dialog/dialog_selectcount.cpp +++ b/src/fheroes2/dialog/dialog_selectcount.cpp @@ -28,6 +28,7 @@ #include "settings.h" #include "text.h" #include "ui_button.h" +#include namespace { @@ -312,8 +313,10 @@ bool Dialog::InputString( const std::string & header, std::string & res ) return !res.empty(); } -int Dialog::ArmySplitTroop( int free_slots, u32 max, u32 & cur, bool savelast ) +int Dialog::ArmySplitTroop( const uint32_t freeSlots, const uint32_t redistributeMax, const bool savelastTroop, uint32_t & redistributeCount, bool & useFastSplit ) { + assert( freeSlots > 0 ); + fheroes2::Display & display = fheroes2::Display::instance(); // cursor @@ -324,11 +327,11 @@ int Dialog::ArmySplitTroop( int free_slots, u32 max, u32 & cur, bool savelast ) const int spacer = 10; const int defaultYPosition = 160; - const int boxHeight = free_slots > 2 ? 90 + spacer : 45; + const int boxHeight = freeSlots > 1 ? 90 + spacer : 45; const int boxYPosition = defaultYPosition + ( ( display.height() - display.DEFAULT_HEIGHT ) / 2 ) - boxHeight; NonFixedFrameBox box( boxHeight, boxYPosition, true ); - SelectValue sel( min, max, cur, 1 ); + SelectValue sel( min, redistributeMax, redistributeCount, 1 ); Text text; const fheroes2::Rect & pos = box.GetArea(); @@ -341,63 +344,46 @@ int Dialog::ArmySplitTroop( int free_slots, u32 max, u32 & cur, bool savelast ) sel.Redraw(); fheroes2::MovableSprite ssp; - fheroes2::Sprite sp3; - fheroes2::Sprite sp4; - fheroes2::Sprite sp5; - - std::vector vrts( 3 ); - - fheroes2::Rect & rt3 = vrts[0]; - fheroes2::Rect & rt4 = vrts[1]; - fheroes2::Rect & rt5 = vrts[2]; - - switch ( free_slots ) { - case 0: - break; - - case 3: - sp3 = fheroes2::AGG::GetICN( ICN::REQUESTS, 22 ); - rt3 = fheroes2::Rect( center - sp3.width() / 2, pos.y + 95, sp3.width(), sp3.height() ); - break; - - case 4: - sp3 = fheroes2::AGG::GetICN( ICN::REQUESTS, 22 ); - sp4 = fheroes2::AGG::GetICN( ICN::REQUESTS, 23 ); - rt3 = fheroes2::Rect( center - 5 - sp3.width(), pos.y + 95, sp3.width(), sp3.height() ); - rt4 = fheroes2::Rect( center + 5, pos.y + 95, sp4.width(), sp4.height() ); - break; - - case 5: - sp3 = fheroes2::AGG::GetICN( ICN::REQUESTS, 22 ); - sp4 = fheroes2::AGG::GetICN( ICN::REQUESTS, 23 ); - sp5 = fheroes2::AGG::GetICN( ICN::REQUESTS, 24 ); - rt3 = fheroes2::Rect( center - sp3.width() / 2 - 10 - sp3.width(), pos.y + 95, sp3.width(), sp3.height() ); - rt4 = fheroes2::Rect( center - sp4.width() / 2, pos.y + 95, sp4.width(), sp4.height() ); - rt5 = fheroes2::Rect( center + sp5.width() / 2 + 10, pos.y + 95, sp5.width(), sp5.height() ); - break; - } + std::vector vrts( freeSlots - 1 ); + + if ( freeSlots > 1 ) { + std::vector sprites( freeSlots - 1 ); + + int spriteIconIdx = 21; + const int deltaX = 10; + const int deltaXStart = static_cast( freeSlots - 2 ) * -5; + + for ( uint32_t i = 0; i < freeSlots - 1; ++i ) { + sprites[i] = fheroes2::AGG::GetICN( ICN::REQUESTS, spriteIconIdx ); + ++spriteIconIdx; + + const int spriteWidth = sprites[i].width(); + const int offset = spriteWidth * ( i - freeSlots / 2 ) + spriteWidth / 2; + vrts[i] = fheroes2::Rect( center + offset + deltaXStart + i * deltaX, pos.y + 95, spriteWidth, sprites[i].height() ); + } - if ( !sp3.empty() ) { text.Set( _( "Fast separation into slots:" ), Font::BIG ); text.Blit( center - text.w() / 2, pos.y + 65 ); - fheroes2::Blit( sp3, display, rt3.x, rt3.y ); - - if ( !sp4.empty() ) - fheroes2::Blit( sp4, display, rt4.x, rt4.y ); - if ( !sp5.empty() ) - fheroes2::Blit( sp5, display, rt5.x, rt5.y ); + for ( uint32_t i = 0; i < freeSlots - 1; ++i ) { + fheroes2::Blit( sprites[i], display, vrts[i].x, vrts[i].y ); + } - ssp.hide(); - ssp.resize( sp3.width(), sp3.height() ); + ssp.resize( sprites[0].width(), sprites[0].height() ); ssp.reset(); + fheroes2::DrawBorder( ssp, 214 ); + + if ( useFastSplit ) { + ssp.setPosition( vrts[0].x, vrts[0].y ); + ssp.show(); + } } fheroes2::ButtonGroup btnGroups( box.GetArea(), Dialog::OK | Dialog::CANCEL ); btnGroups.draw(); - const uint32_t maximumAcceptedValue = savelast ? max : max - 1; + const uint32_t maximumAcceptedValue = savelastTroop ? redistributeMax : redistributeMax - 1; const fheroes2::Point minMaxButtonOffset( pos.x + 165, pos.y + 30 ); fheroes2::ButtonSprite buttonMax( minMaxButtonOffset.x, minMaxButtonOffset.y ); @@ -409,7 +395,7 @@ int Dialog::ArmySplitTroop( int free_slots, u32 max, u32 & cur, bool savelast ) buttonMin.setSprite( fheroes2::Crop( fheroes2::AGG::GetICN( ICN::BTNMIN, 0 ), buttonArea.x, buttonArea.y, buttonArea.w, buttonArea.h ), fheroes2::Crop( fheroes2::AGG::GetICN( ICN::BTNMIN, 1 ), buttonArea.x, buttonArea.y, buttonArea.w, buttonArea.h ) ); - SwitchMaxMinButtons( buttonMin, buttonMax, cur, maximumAcceptedValue ); + SwitchMaxMinButtons( buttonMin, buttonMax, redistributeCount, maximumAcceptedValue ); LocalEvent & le = LocalEvent::Get(); @@ -425,19 +411,19 @@ int Dialog::ArmySplitTroop( int free_slots, u32 max, u32 & cur, bool savelast ) if ( buttonMin.isVisible() ) le.MousePressLeft( buttonMin.area() ) ? buttonMin.drawOnPress() : buttonMin.drawOnRelease(); - if ( PressIntKey( max, cur ) ) { - sel.SetCur( cur ); + if ( PressIntKey( redistributeMax, redistributeCount ) ) { + sel.SetCur( redistributeCount ); redraw_count = true; } else if ( buttonMax.isVisible() && le.MouseClickLeft( buttonMax.area() ) ) { le.MousePressLeft( buttonMax.area() ) ? buttonMax.drawOnPress() : buttonMax.drawOnRelease(); - cur = maximumAcceptedValue; + redistributeCount = maximumAcceptedValue; sel.SetCur( maximumAcceptedValue ); redraw_count = true; } else if ( buttonMin.isVisible() && le.MouseClickLeft( buttonMin.area() ) ) { le.MousePressLeft( buttonMin.area() ) ? buttonMin.drawOnPress() : buttonMin.drawOnRelease(); - cur = min; + redistributeCount = min; sel.SetCur( min ); redraw_count = true; } @@ -456,7 +442,7 @@ int Dialog::ArmySplitTroop( int free_slots, u32 max, u32 & cur, bool savelast ) } if ( redraw_count ) { - SwitchMaxMinButtons( buttonMin, buttonMax, cur, maximumAcceptedValue ); + SwitchMaxMinButtons( buttonMin, buttonMax, redistributeCount, maximumAcceptedValue ); cursor.Hide(); if ( !ssp.empty() ) ssp.hide(); @@ -479,20 +465,24 @@ int Dialog::ArmySplitTroop( int free_slots, u32 max, u32 & cur, bool savelast ) int result = 0; if ( bres == Dialog::OK ) { - cur = sel(); + redistributeCount = sel(); if ( !ssp.isHidden() ) { const fheroes2::Rect rt( ssp.x(), ssp.y(), ssp.width(), ssp.height() ); - if ( rt == rt3 ) - result = 3; - else if ( rt == rt4 ) - result = 4; - else if ( rt == rt5 ) - result = 5; + for ( uint32_t i = 0; i < freeSlots - 1; ++i ) { + if ( rt == vrts[i] ) { + result = i + 2; + break; + } + } + + useFastSplit = true; } - else + else { result = 2; + useFastSplit = false; + } } return result; From 8fc132bbcfcc246fd055856c7dd4f0f025df6008 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Wed, 3 Mar 2021 06:44:45 -0500 Subject: [PATCH 62/84] AI: improve retreat condition (#2890) Now AI should be less likely to retreat early: checking if unit stack was lost or army was small in the first place. Also army power ratio now depends on game difficulty (Easy AI should rarely run). --- src/fheroes2/ai/normal/ai_normal.h | 5 +- src/fheroes2/ai/normal/ai_normal_battle.cpp | 57 +++++++++++++-------- src/fheroes2/game/difficulty.cpp | 16 ++++++ src/fheroes2/game/difficulty.h | 1 + 4 files changed, 57 insertions(+), 22 deletions(-) diff --git a/src/fheroes2/ai/normal/ai_normal.h b/src/fheroes2/ai/normal/ai_normal.h index acf3330a3ff..34a80d9b095 100644 --- a/src/fheroes2/ai/normal/ai_normal.h +++ b/src/fheroes2/ai/normal/ai_normal.h @@ -50,9 +50,9 @@ namespace AI // decision-making helpers bool isUnitFaster( const Battle::Unit & currentUnit, const Battle::Unit & target ) const; - bool isHeroWorthSaving( const Heroes * hero ) const; + bool isHeroWorthSaving( const Heroes & hero ) const; bool isCommanderCanSpellcast( const Battle::Arena & arena, const HeroBase * commander ) const; - bool checkRetreatCondition( double myArmy, double enemy ) const; + bool checkRetreatCondition( const Heroes & hero ) const; private: // to be exposed later once every BattlePlanner will be re-initialized at combat start @@ -71,6 +71,7 @@ namespace AI int _highestDamageExpected = 0; bool _attackingCastle = false; bool _defendingCastle = false; + bool _considerRetreat = false; }; class Normal : public Base diff --git a/src/fheroes2/ai/normal/ai_normal_battle.cpp b/src/fheroes2/ai/normal/ai_normal_battle.cpp index 55dcb2206e8..1139b080c4d 100644 --- a/src/fheroes2/ai/normal/ai_normal_battle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_battle.cpp @@ -29,6 +29,8 @@ #include "battle_tower.h" #include "battle_troop.h" #include "castle.h" +#include "difficulty.h" +#include "game.h" #include "heroes.h" #include "logging.h" @@ -50,9 +52,9 @@ namespace AI } } - bool BattlePlanner::isHeroWorthSaving( const Heroes * hero ) const + bool BattlePlanner::isHeroWorthSaving( const Heroes & hero ) const { - return hero && ( hero->GetLevel() > 2 || !hero->GetBagArtifacts().empty() ); + return hero.GetLevel() > 2 || !hero.GetBagArtifacts().empty(); } bool BattlePlanner::isCommanderCanSpellcast( const Arena & arena, const HeroBase * commander ) const @@ -61,14 +63,12 @@ namespace AI && !commander->Modes( Heroes::SPELLCASTED ) && !arena.isSpellcastDisabled(); } - bool BattlePlanner::checkRetreatCondition( double myArmy, double enemy ) const + bool BattlePlanner::checkRetreatCondition( const Heroes & hero ) const { - // FIXME: more sophisticated logic to see if remaining units are under threat - // Consider taking speed/turn order into account as well - // Pass in ( const Units & friendly, const Units & enemies ) instead - - // Retreat if remaining army strength is 10% of enemy's army - return myArmy * 10 < enemy; + // Retreat if remaining army strength is a fraction of enemy's + // Consider taking speed/turn order into account in the future + const double ratio = Difficulty::GetAIRetreatRatio( Game::getDifficulty() ); + return _considerRetreat && _myArmyStrength * ratio < _enemyArmyStrength && !hero.isControlHuman() && isHeroWorthSaving( hero ); } bool BattlePlanner::isUnitFaster( const Unit & currentUnit, const Unit & target ) const @@ -146,8 +146,7 @@ namespace AI // Step 2. Check retreat/surrender condition const Heroes * actualHero = dynamic_cast( commander ); - if ( actualHero && !actualHero->isControlHuman() && arena.CanRetreatOpponent( _myColor ) && isHeroWorthSaving( actualHero ) - && checkRetreatCondition( _myArmyStrength, _enemyArmyStrength ) ) { + if ( actualHero && arena.CanRetreatOpponent( _myColor ) && checkRetreatCondition( *actualHero ) ) { // Cast maximum damage spell actions = forceSpellcastBeforeRetreat( arena, commander ); @@ -225,19 +224,19 @@ namespace AI const Force & friendlyForce = arena.GetForce( _myColor ); const Force & enemyForce = arena.GetForce( _myColor, true ); - // This should filter out all invalid units - const Units friendly( friendlyForce, true ); - const Units enemies( enemyForce, true ); - // Friendly and enemy army analysis _myArmyStrength = 0; _enemyArmyStrength = 0; _myShooterStr = 0; _enemyShooterStr = 0; _highestDamageExpected = 0; + _considerRetreat = false; + + for ( Unit * unitPtr : enemyForce ) { + if ( !unitPtr || !unitPtr->isValid() ) + continue; - for ( Units::const_iterator it = enemies.begin(); it != enemies.end(); ++it ) { - const Unit & unit = **it; + const Unit & unit = *unitPtr; const double unitStr = unit.GetStrength(); _enemyArmyStrength += unitStr; @@ -250,15 +249,33 @@ namespace AI _highestDamageExpected = dmg; } - for ( Units::const_iterator it = friendly.begin(); it != friendly.end(); ++it ) { - const Unit & unit = **it; - const double unitStr = unit.GetStrength(); + uint32_t initialUnitCount = 0; + for ( Unit * unitPtr : friendlyForce ) { + // Do not check isValid() here to handle dead troops + if ( !unitPtr ) + continue; + const Unit & unit = *unitPtr; + const uint32_t count = unit.GetCount(); + const uint32_t dead = unit.GetDead(); + + // Count all valid troops in army (both alive and dead) + if ( count > 0 || dead > 0 ) { + ++initialUnitCount; + } + // Dead unit: trigger retreat condition and skip strength calculation + if ( count == 0 && dead > 0 ) { + _considerRetreat = true; + continue; + } + + const double unitStr = unit.GetStrength(); _myArmyStrength += unitStr; if ( unit.isArchers() ) { _myShooterStr += unitStr; } } + _considerRetreat = _considerRetreat || initialUnitCount < 4; // Add castle siege (and battle arena) modifiers _attackingCastle = false; diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index 53636574f1b..25e678c1977 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -123,3 +123,19 @@ int Difficulty::GetHeroMovementBonus( int difficulty ) } return 0; } + +double Difficulty::GetAIRetreatRatio( int difficulty ) +{ + switch ( difficulty ) { + case Difficulty::NORMAL: + return 100.0 / 7.5; + case Difficulty::HARD: // fall-through + case Difficulty::EXPERT: + return 100.0 / 8.5; + case Difficulty::IMPOSSIBLE: + return 100.0 / 10.0; + default: + break; + } + return 100.0 / 6.0; +} diff --git a/src/fheroes2/game/difficulty.h b/src/fheroes2/game/difficulty.h index 33f8c683c00..c5d6ce82171 100644 --- a/src/fheroes2/game/difficulty.h +++ b/src/fheroes2/game/difficulty.h @@ -43,6 +43,7 @@ namespace Difficulty double GetUnitGrowthBonus( int difficulty ); double GetBattleExperienceBonus( int difficulty ); int GetHeroMovementBonus( int difficulty ); + double GetAIRetreatRatio( int difficulty ); } #endif From 2d95db622257af183170f0d5d66e4fb9624755dc Mon Sep 17 00:00:00 2001 From: vasilenkoalexey Date: Wed, 3 Mar 2021 16:06:00 +0300 Subject: [PATCH 63/84] Add barrier fading animation (#2884) close #2399 --- src/fheroes2/heroes/heroes_action.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fheroes2/heroes/heroes_action.cpp b/src/fheroes2/heroes/heroes_action.cpp index 0c22baa8154..50fb9aa618e 100644 --- a/src/fheroes2/heroes/heroes_action.cpp +++ b/src/fheroes2/heroes/heroes_action.cpp @@ -3057,9 +3057,9 @@ void ActionToBarrier( Heroes & hero, u32 obj, s32 dst_index ) _( "A magical barrier stands tall before you, blocking your way. Runes on the arch read,\n\"Speak the key and you may pass.\"\nAs you speak the magic word, the glowing barrier dissolves into nothingness." ), Font::BIG, Dialog::OK ); + AnimationRemoveObject( tile ); tile.SetObject( hero.GetMapsObject() ); hero.SetMapsObject( MP2::OBJ_ZERO ); - AnimationRemoveObject( tile ); tile.RemoveObjectSprite(); // TODO: fix pathfinding if ( tile.GetIndex() == hero.GetIndex() ) { From 1d444d5141f17df8ffeb985763932f69cb030065 Mon Sep 17 00:00:00 2001 From: JeKkich <79770404+JeKkich@users.noreply.github.com> Date: Wed, 3 Mar 2021 18:16:12 +0300 Subject: [PATCH 64/84] Fix "Artifacts" category in Oracle Screen (#2887) close #1591 --- src/fheroes2/dialog/dialog_thievesguild.cpp | 49 +++++++++++++++------ src/fheroes2/kingdom/kingdom.cpp | 8 ++++ src/fheroes2/kingdom/kingdom.h | 2 + 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/fheroes2/dialog/dialog_thievesguild.cpp b/src/fheroes2/dialog/dialog_thievesguild.cpp index dd9fd33bdbf..0f420985609 100644 --- a/src/fheroes2/dialog/dialog_thievesguild.cpp +++ b/src/fheroes2/dialog/dialog_thievesguild.cpp @@ -149,6 +149,18 @@ void GetObelisksInfo( std::vector & v, const Colors & colors ) std::sort( v.begin(), v.end(), ValueColors::SortValueGreat ); } +void GetArtifactsInfo( std::vector & v, const Colors & colors ) +{ + v.clear(); + + for ( Colors::const_iterator color = colors.begin(); color != colors.end(); ++color ) { + const int value = world.GetKingdom( *color ).GetCountArtifacts(); + UpdateValuesColors( v, value, *color ); + } + + std::sort( v.begin(), v.end(), ValueColors::SortValueGreat ); +} + void GetArmyInfo( std::vector & v, const Colors & colors ) { v.clear(); @@ -308,7 +320,7 @@ void Dialog::ThievesGuild( bool oracle ) text.Set( _( "Number of Castles:" ) ); dst_pt.x = cur_pt.x + textx - text.w(); - dst_pt.y = cur_pt.y + 54; + dst_pt.y = cur_pt.y + 52; text.Blit( dst_pt.x, dst_pt.y ); dst_pt.x = cur_pt.x + startx; @@ -317,7 +329,7 @@ void Dialog::ThievesGuild( bool oracle ) text.Set( _( "Number of Heroes:" ) ); dst_pt.x = cur_pt.x + textx - text.w(); - dst_pt.y = cur_pt.y + 80; + dst_pt.y = cur_pt.y + 76; text.Blit( dst_pt.x, dst_pt.y ); dst_pt.x = cur_pt.x + startx; @@ -326,7 +338,7 @@ void Dialog::ThievesGuild( bool oracle ) text.Set( _( "Gold in Treasury:" ) ); dst_pt.x = cur_pt.x + textx - text.w(); - dst_pt.y = cur_pt.y + 106; + dst_pt.y = cur_pt.y + 100; text.Blit( dst_pt.x, dst_pt.y ); dst_pt.x = cur_pt.x + startx; @@ -336,7 +348,7 @@ void Dialog::ThievesGuild( bool oracle ) text.Set( _( "Wood & Ore:" ) ); dst_pt.x = cur_pt.x + textx - text.w(); - dst_pt.y = cur_pt.y + 132; + dst_pt.y = cur_pt.y + 124; text.Blit( dst_pt.x, dst_pt.y ); dst_pt.x = cur_pt.x + startx; @@ -346,7 +358,7 @@ void Dialog::ThievesGuild( bool oracle ) text.Set( _( "Gems, Cr, Slf & Mer:" ) ); dst_pt.x = cur_pt.x + textx - text.w(); - dst_pt.y = cur_pt.y + 158; + dst_pt.y = cur_pt.y + 148; text.Blit( dst_pt.x, dst_pt.y ); dst_pt.x = cur_pt.x + startx; @@ -356,7 +368,7 @@ void Dialog::ThievesGuild( bool oracle ) text.Set( _( "Obelisks Found:" ) ); dst_pt.x = cur_pt.x + textx - text.w(); - dst_pt.y = cur_pt.y + 184; + dst_pt.y = cur_pt.y + 172; text.Blit( dst_pt.x, dst_pt.y ); dst_pt.x = cur_pt.x + startx; @@ -364,9 +376,20 @@ void Dialog::ThievesGuild( bool oracle ) if ( 2 < count ) DrawFlags( v, dst_pt, maxw, colors.size() ); + text.Set( _( "Artifacts:" ) ); + dst_pt.x = cur_pt.x + textx - text.w(); + dst_pt.y = cur_pt.y + 196; + text.Blit( dst_pt.x, dst_pt.y ); + + dst_pt.x = cur_pt.x + startx; + GetArtifactsInfo( v, colors ); + if ( count > 2 ) { + DrawFlags( v, dst_pt, maxw, colors.size() ); + } + text.Set( _( "Total Army Strength:" ) ); dst_pt.x = cur_pt.x + textx - text.w(); - dst_pt.y = cur_pt.y + 210; + dst_pt.y = cur_pt.y + 220; text.Blit( dst_pt.x, dst_pt.y ); dst_pt.x = cur_pt.x + startx; @@ -376,7 +399,7 @@ void Dialog::ThievesGuild( bool oracle ) text.Set( _( "Income:" ) ); dst_pt.x = cur_pt.x + textx - text.w(); - dst_pt.y = cur_pt.y + 238; + dst_pt.y = cur_pt.y + 248; text.Blit( dst_pt.x, dst_pt.y ); dst_pt.x = cur_pt.x + startx; @@ -389,14 +412,14 @@ void Dialog::ThievesGuild( bool oracle ) for ( Colors::const_iterator color = colors.begin(); color != colors.end(); ++color ) { text.Set( Color::String( *color ) ); dst_pt.x = cur_pt.x + startx + maxw / ( colors.size() * 2 ) + ii * maxw / colors.size() - text.w() / 2; - dst_pt.y = cur_pt.y + 270; + dst_pt.y = cur_pt.y + 276; text.Blit( dst_pt.x, dst_pt.y ); ++ii; } text.Set( _( "Best Hero:" ) ); dst_pt.x = cur_pt.x + textx - text.w(); - dst_pt.y = cur_pt.y + 306; + dst_pt.y = cur_pt.y + 312; text.Blit( dst_pt.x, dst_pt.y ); dst_pt.x = cur_pt.x + startx; @@ -405,7 +428,7 @@ void Dialog::ThievesGuild( bool oracle ) text.Set( _( "Best Hero Stats:" ) ); dst_pt.x = cur_pt.x + textx - text.w(); - dst_pt.y = cur_pt.y + 347; + dst_pt.y = cur_pt.y + 353; text.Blit( dst_pt.x, dst_pt.y ); dst_pt.x = cur_pt.x + startx; @@ -414,7 +437,7 @@ void Dialog::ThievesGuild( bool oracle ) text.Set( _( "Personality:" ) ); dst_pt.x = cur_pt.x + textx - text.w(); - dst_pt.y = cur_pt.y + 388; + dst_pt.y = cur_pt.y + 394; text.Blit( dst_pt.x, dst_pt.y ); dst_pt.x = cur_pt.x + startx; @@ -423,7 +446,7 @@ void Dialog::ThievesGuild( bool oracle ) text.Set( _( "Best Monster:" ) ); dst_pt.x = cur_pt.x + textx - text.w(); - dst_pt.y = cur_pt.y + 429; + dst_pt.y = cur_pt.y + 435; text.Blit( dst_pt.x, dst_pt.y ); dst_pt.x = cur_pt.x + startx; diff --git a/src/fheroes2/kingdom/kingdom.cpp b/src/fheroes2/kingdom/kingdom.cpp index b54720f0c57..7a7af56fb90 100644 --- a/src/fheroes2/kingdom/kingdom.cpp +++ b/src/fheroes2/kingdom/kingdom.cpp @@ -329,6 +329,14 @@ uint32_t Kingdom::GetCountThievesGuild() const std::count_if( castles.begin(), castles.end(), []( const Castle * castle ) { return Castle::PredicateIsBuildBuilding( castle, BUILD_THIEVESGUILD ); } ) ); } +uint32_t Kingdom::GetCountArtifacts() const +{ + uint32_t result = 0; + for ( const Heroes * hero : heroes ) + result += hero->GetCountArtifacts(); + return result; +} + bool Kingdom::AllowPayment( const Funds & funds ) const { return ( resource.wood >= funds.wood || 0 == funds.wood ) && ( resource.mercury >= funds.mercury || 0 == funds.mercury ) diff --git a/src/fheroes2/kingdom/kingdom.h b/src/fheroes2/kingdom/kingdom.h index 4b3c98c71b2..2c67a404078 100644 --- a/src/fheroes2/kingdom/kingdom.h +++ b/src/fheroes2/kingdom/kingdom.h @@ -115,6 +115,8 @@ class Kingdom : public BitModes, public Control u32 GetCountBuilding( u32 ) const; uint32_t GetCountThievesGuild() const; + uint32_t GetCountArtifacts() const; + Recruits & GetRecruits( void ); const KingdomHeroes & GetHeroes( void ) const From 80d561d5ac9340890aef42ff047f7cff48274e78 Mon Sep 17 00:00:00 2001 From: Biswapriyo Nath Date: Thu, 4 Mar 2021 17:38:26 +0530 Subject: [PATCH 65/84] Fix endian.h include, absent in MinGW (#2894) --- src/engine/endian_h2.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/endian_h2.h b/src/engine/endian_h2.h index 922df65728d..7c0ff4ddbcf 100644 --- a/src/engine/endian_h2.h +++ b/src/engine/endian_h2.h @@ -21,7 +21,7 @@ #ifndef ENDIAN_H2_H #define ENDIAN_H2_H -#if defined( __linux__ ) || defined( __MINGW32__ ) +#if defined( __linux__ ) #include #elif defined( __FreeBSD__ ) From f73ba73d8c40ff4242d7633eaa329b3d66dc44a8 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Thu, 4 Mar 2021 21:44:34 +0800 Subject: [PATCH 66/84] Update to 0.9.1 verson (#2877) --- CONTRIBUTING.md | 2 +- Makefile | 23 ++++++++++-- README.md | 2 +- appveyor.yml | 2 +- changelog.txt | 58 ++++++++++++++++++++++++++++++ sonar-project.properties | 2 +- src/Makefile | 21 +++++++++-- src/dist/Makefile | 22 ++++++++++-- src/engine/Makefile | 22 ++++++++++-- src/fheroes2/game/game_credits.cpp | 10 +++++- src/fheroes2/system/gamedefs.h | 2 +- src/tools/Makefile | 21 +++++++++-- 12 files changed, 168 insertions(+), 19 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a4b22e0092f..8fbfe20c4eb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,7 +28,7 @@ #### **Do you want to contribute to the project?** -fheroes2 is a volunteer effort. We encourage you to pitch in and [join the team](https://github.com/ihhub/fheroes2/wiki/Contributors)! If you see that your name is not present in the list, please be free to contact us and report about such terrible mistake. +fheroes2 is a volunteer effort. We encourage you to pitch in and join the team! Thanks! :heart: :heart: :heart: diff --git a/Makefile b/Makefile index cf46181c1fa..c7bb91f2ebd 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,23 @@ -# makefile -# project: Free Heroes of Might and Magic II (https://github.com/ihhub/fheroes2) -# +########################################################################### +# Free Heroes of Might and Magic II: https://github.com/ihhub/fheroes2 # +# Copyright (C) 2021 # +# # +# 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. # +########################################################################### + # Options: # DEBUG: build in debug mode # RELEASE: build with addons extensions diff --git a/README.md b/README.md index 6b6b2f86891..e2be5d415a1 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ MacOS and Linux Contribution --------------------------- -We welcome and appreciate any help, even if it is a tiny text or code change. Please read [contribution](https://github.com/ihhub/fheroes2/blob/master/CONTRIBUTING.md) page before starting work on a pull request. All contributors are listed in the project's wiki [page](https://github.com/ihhub/fheroes2/wiki/Contributors). +We welcome and appreciate any help, even if it is a tiny text or code change. Please read [contribution](https://github.com/ihhub/fheroes2/blob/master/CONTRIBUTING.md) page before starting work on a pull request. Not sure what to start with? Feel free to refer to [good first issue](https://github.com/ihhub/fheroes2/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) or [help wanted](https://github.com/ihhub/fheroes2/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) tags. License diff --git a/appveyor.yml b/appveyor.yml index 415e815f816..b27b4f35ea0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,7 +10,7 @@ skip_commits: skip_tags: true # version format -version: 0.9.0.{build} +version: 0.9.1.{build} # Build worker image (VM template) image: Visual Studio 2015 diff --git a/changelog.txt b/changelog.txt index 7b5c7de9937..565ff8cfb89 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,61 @@ +version 0.9.1 (04 March 2021) +- add "Artifacts" category in Oracle Screen +- add barrier fading animation +- improve AI retreat condition +- fix Split-related issues and implement some new techniques +- disable retreat for auto battle mode +- fix object's visited info and grammar +- fix issues with path finding during combat to avoid being stopped by moat +- fix incorrect names of Ballista and Turret during battle +- add missing shadow for arrow cursor on SDL2 +- fix ACCEPT button font for Good Interface +- fix AI wide units pathfding and moat logic +- allow to modify a hero during level up +- fix summon boat logic +- fix monster movement nearby moat +- AI does not chase faster units during battle +- fix hero shadow drawing on world map +- fix missing animation frame for wince +- fix object fading animation on world map +- fix missing last lost hero condition reset +- fix simultaneous animation of boat while during another building construction +- change logic in monster hiring window +- fix save loading crash for broken saves +- add middle resolution status support +- do not show waiting cursor for an exhausted hero +- fix post Daemon Cave missing music +- fix well's max button drawing +- fix incorrect castle focus while visiting by a hero +- fix fog drawings +- reduce income window area in towns +- fix autosave option logic +- fix overlapped battleground objects +- remember scenario difficulty while restarting or choosing a new scenario +- fix multiple game frezes with MIDI music playback +- add resizable window support on SDL2 +- make deterministic bonus for hero level-up +- fix wrong controller pointer speed option name and touch coordinates translation +- add auto battle resolve mode to the game +- add logic to support video frame rescaling +- optimize screen resolution logic +- fix garrison strength estimation by AI +- fix magic gardens priority if object capture is enabled +- optimize AI's troop placement before the battle +- use same colour for all battlegrounds +- change text in map size hint, to be displayed in more pleasant manner +- load game button from UI should lead to common load game logic +- initial implementation of "View World" +- fix application crash for drawings +- change building status message for Dwellings +- fix crash when entering ally castle +- add Campaign continuation initial support +- speed up rendering of World Map +- fix spell book status on hovering +- save fullscreen mode in configuration file while switching between modes +- AI should avoid spellcasting if has advantage in battle +- fix rendering with mouse emulation +- fix extensive memory usage by dialog windows + version 0.9.0 (04 February 2021) - make AI to buy magic books in castles - fix infinite AI turns diff --git a/sonar-project.properties b/sonar-project.properties index fe31cf72cb2..e9fd8d528ab 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,7 +3,7 @@ sonar.organization=ihhub # This is the name and version displayed in the SonarCloud UI. sonar.projectName=fheroes2 -sonar.projectVersion=0.9.0 +sonar.projectVersion=0.9.1 # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. sonar.sources=. diff --git a/src/Makefile b/src/Makefile index 2343b8f6dc9..9be31a21c86 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,5 +1,22 @@ -# makefile -# project: Free Heroes of Might and Magic II (https://github.com/ihhub/fheroes2) +########################################################################### +# Free Heroes of Might and Magic II: https://github.com/ihhub/fheroes2 # +# Copyright (C) 2021 # +# # +# 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. # +########################################################################### TARGET := fheroes2 diff --git a/src/dist/Makefile b/src/dist/Makefile index 54d8f3b8686..3128c466b61 100644 --- a/src/dist/Makefile +++ b/src/dist/Makefile @@ -1,6 +1,22 @@ -# makefile -# project: Free Heroes2 -# +########################################################################### +# Free Heroes of Might and Magic II: https://github.com/ihhub/fheroes2 # +# Copyright (C) 2021 # +# # +# 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. # +########################################################################### ifeq ($(PREFIX),) PREFIX := /usr/local diff --git a/src/engine/Makefile b/src/engine/Makefile index cddeef8aa9a..6c36bfea174 100644 --- a/src/engine/Makefile +++ b/src/engine/Makefile @@ -1,6 +1,22 @@ -# makefile -# project: Free Heroes2 -# libSDL C++ wrapper engine +########################################################################### +# Free Heroes of Might and Magic II: https://github.com/ihhub/fheroes2 # +# Copyright (C) 2021 # +# # +# 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. # +########################################################################### TARGET := libengine CFLAGS := $(CFLAGS) -I../thirdparty/libsmacker diff --git a/src/fheroes2/game/game_credits.cpp b/src/fheroes2/game/game_credits.cpp index 14af1ff6f38..3635fbcb8eb 100644 --- a/src/fheroes2/game/game_credits.cpp +++ b/src/fheroes2/game/game_credits.cpp @@ -96,7 +96,15 @@ void Game::ShowCredits() title.Blit( screenOffset.x + 2 * columnStep + ( columnStep - title.w() ) / 2, offsetY ); offsetY += title.h(); - const std::string contributors( "LeHerosInconnu\nshprotru\nundef21\nvincent-grosbois\neos428\nAndrey Starodubtsev\nVasilenko Alexey\nemotionalamoeba\ntau3\n" + const std::string contributors( "LeHerosInconnu\n" + "shprotru\n" + "undef21\n" + "vincent-grosbois\n" + "eos428\n" + "Andrii Kurdiumov\n" + "Vasilenko Alexey\n" + "Andrey Starodubtsev\n" + "dimag0g\n" "and many other contributors!" ); name.Set( contributors, Font::BIG, textWidth ); diff --git a/src/fheroes2/system/gamedefs.h b/src/fheroes2/system/gamedefs.h index acdebf20e54..8c677a88b4f 100644 --- a/src/fheroes2/system/gamedefs.h +++ b/src/fheroes2/system/gamedefs.h @@ -27,7 +27,7 @@ #define MAJOR_VERSION 0 #define MINOR_VERSION 9 -#define INTERMEDIATE_VERSION 0 +#define INTERMEDIATE_VERSION 1 // hardcore defines: kingdom #define KINGDOMMAX 6 diff --git a/src/tools/Makefile b/src/tools/Makefile index be3284831aa..163c70aee95 100644 --- a/src/tools/Makefile +++ b/src/tools/Makefile @@ -1,5 +1,22 @@ -# makefile -# project: Free Heroes2 Tools +########################################################################### +# Free Heroes of Might and Magic II: https://github.com/ihhub/fheroes2 # +# Copyright (C) 2021 # +# # +# 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. # +########################################################################### ifdef FHEROES2_SDL1 SDL_LIBS := $(shell sdl-config --libs) From 6d6a213f17ea1a006465d0db3ced3e94ac362c47 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Fri, 5 Mar 2021 18:05:03 +0300 Subject: [PATCH 67/84] Fix inability to reorganize troops in Kingdom View dialog (#2897) close #2715 --- src/fheroes2/kingdom/kingdom_overview.cpp | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/fheroes2/kingdom/kingdom_overview.cpp b/src/fheroes2/kingdom/kingdom_overview.cpp index 0fadfb00d59..e008ade95b9 100644 --- a/src/fheroes2/kingdom/kingdom_overview.cpp +++ b/src/fheroes2/kingdom/kingdom_overview.cpp @@ -323,7 +323,6 @@ class StatsCastlesList : public Interface::ListBox { public: StatsCastlesList( const Point & pt, KingdomCastles & ); - void Refresh(); virtual void RedrawItem( const CstlRow &, s32, s32, bool ) override; virtual void RedrawBackground( const Point & ) override; @@ -504,15 +503,6 @@ void StatsCastlesList::RedrawBackground( const Point & dst ) } } -// Make sure that our list doesn't refer to incorrect castle data after castle window was entered -// We don't need to change the size of the vector as castles can't be added / removed from this view -void StatsCastlesList::Refresh() -{ - for ( CstlRow & row : content ) { - row.Init( row.castle ); - } -} - std::string CapturedExtInfoString( int res, int color, const Funds & funds ) { std::ostringstream os; @@ -692,8 +682,6 @@ void Kingdom::OverviewDialog( void ) Dialog::ResourceInfo( _( "Income" ), "", GetIncome( INCOME_ALL ), 0 ); if ( !cursor.isVisible() || redraw ) { - listCastles.Refresh(); - // check if graphics in main world map window should change, this can happen in several situations: // - hero dismissed -> hero icon list is updated and world map focus changed // - hero hired -> hero icon list is updated From ff570485d7f1ea2ab8981746f468c7fc8b628bca Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sat, 6 Mar 2021 06:33:25 +0300 Subject: [PATCH 68/84] Do not allow the visiting hero to learn Library spells if the Library hasn't been built yet (#2898) close #2743 --- src/fheroes2/castle/mageguild.cpp | 16 ++++++++++++++-- src/fheroes2/spell/spell_storage.cpp | 8 +++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/fheroes2/castle/mageguild.cpp b/src/fheroes2/castle/mageguild.cpp index ddcda689191..e005fd45df4 100644 --- a/src/fheroes2/castle/mageguild.cpp +++ b/src/fheroes2/castle/mageguild.cpp @@ -58,12 +58,24 @@ void MageGuild::initialize( int race, bool libraryCap ) --spellCountByLevel[guaranteedDamageSpellLevel - 1]; --spellCountByLevel[guaranteedNonDamageSpellLevel - 1]; + SpellStorage all( general ); + for ( int i = 0; i < 5; ++i ) { for ( int j = 0; j < spellCountByLevel[i]; ++j ) { - const Spell spell = GetUniqueSpellCompatibility( general, race, i + 1 ); + const Spell spell = GetUniqueSpellCompatibility( all, race, i + 1 ); + + if ( spell == Spell::NONE ) { + continue; + } - if ( spell != Spell::NONE ) + if ( libraryCap && j == spellCountByLevel[i] - 1 ) { + library.Append( spell ); + } + else { general.Append( spell ); + } + + all.Append( spell ); } } } diff --git a/src/fheroes2/spell/spell_storage.cpp b/src/fheroes2/spell/spell_storage.cpp index 98607ec04d6..e3a9e35f5dc 100644 --- a/src/fheroes2/spell/spell_storage.cpp +++ b/src/fheroes2/spell/spell_storage.cpp @@ -51,9 +51,11 @@ void SpellStorage::Append( const Spell & sp ) void SpellStorage::Append( const SpellStorage & st ) { - insert( end(), st.begin(), st.end() ); - std::sort( begin(), end() ); - resize( std::unique( begin(), end() ) - begin() ); + for ( const Spell & sp : st ) { + if ( std::find( begin(), end(), sp ) == end() ) { + push_back( sp ); + } + } } bool SpellStorage::isPresentSpell( const Spell & spell ) const From b637a5378e55f8ed25ead0dac78fb040f3882fa2 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sat, 6 Mar 2021 17:36:42 +0300 Subject: [PATCH 69/84] Preserve unit direction in battle if it moves strictly vertically. (#2902) close #2724 --- src/fheroes2/battle/battle_troop.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fheroes2/battle/battle_troop.cpp b/src/fheroes2/battle/battle_troop.cpp index 51089a08519..8c33a531f3b 100644 --- a/src/fheroes2/battle/battle_troop.cpp +++ b/src/fheroes2/battle/battle_troop.cpp @@ -187,7 +187,7 @@ void Battle::Unit::UpdateDirection( void ) bool Battle::Unit::UpdateDirection( const Rect & pos ) { - bool need = position.GetRect().x > pos.x; + bool need = position.GetRect().x == pos.x ? reflect : position.GetRect().x > pos.x; if ( need != reflect ) { SetReflection( need ); From 89a81c8622c841ff983e96890f6452b966bf18d4 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sat, 6 Mar 2021 17:50:53 +0300 Subject: [PATCH 70/84] Simplify handling of mouse events, make MouseRelease* events non-recurring. (#2895) close #2842 --- src/engine/localevent.cpp | 108 ++++++++++++++++++++++---------------- src/engine/localevent.h | 12 ++--- 2 files changed, 68 insertions(+), 52 deletions(-) diff --git a/src/engine/localevent.cpp b/src/engine/localevent.cpp index 524367d103c..c678bcd4d35 100644 --- a/src/engine/localevent.cpp +++ b/src/engine/localevent.cpp @@ -952,9 +952,8 @@ LocalEvent & LocalEvent::GetClean() le.ResetModes( KEY_PRESSED ); le.ResetModes( MOUSE_MOTION ); le.ResetModes( MOUSE_PRESSED ); - le.ResetModes( CLICK_LEFT ); - le.ResetModes( CLICK_RIGHT ); - le.ResetModes( CLICK_MIDDLE ); + le.ResetModes( MOUSE_RELEASED ); + le.ResetModes( MOUSE_CLICKED ); le.ResetModes( KEY_HOLD ); return le; } @@ -968,6 +967,8 @@ bool LocalEvent::HandleEvents( bool delay, bool allowExit ) SDL_Event event; ResetModes( MOUSE_MOTION ); + ResetModes( MOUSE_RELEASED ); + ResetModes( MOUSE_CLICKED ); #if SDL_VERSION_ATLEAST( 2, 0, 0 ) if ( _gameController != nullptr ) { // fast map scroll with dpad @@ -980,9 +981,6 @@ bool LocalEvent::HandleEvents( bool delay, bool allowExit ) #else ResetModes( KEY_PRESSED ); #endif - ResetModes( CLICK_LEFT ); - ResetModes( CLICK_MIDDLE ); - ResetModes( CLICK_RIGHT ); mouse_wm = Point(); @@ -1168,14 +1166,17 @@ void LocalEvent::HandleTouchEvent( const SDL_TouchFingerEvent & event ) if ( event.type == SDL_FINGERDOWN ) { mouse_pl = mouse_cu; - SetModes( CLICK_LEFT ); + SetModes( MOUSE_PRESSED ); } else if ( event.type == SDL_FINGERUP ) { mouse_rl = mouse_cu; - SetModes( CLICK_LEFT ); + ResetModes( MOUSE_PRESSED ); + SetModes( MOUSE_RELEASED ); + SetModes( MOUSE_CLICKED ); } + mouse_button = SDL_BUTTON_LEFT; } } @@ -1215,30 +1216,37 @@ void LocalEvent::HandleControllerButtonEvent( const SDL_ControllerButtonEvent & else if ( button.state == SDL_RELEASED ) ResetModes( KEY_PRESSED ); - if ( button.button == SDL_CONTROLLER_BUTTON_A ) { - SetModes( CLICK_LEFT ); + if ( button.button == SDL_CONTROLLER_BUTTON_A || button.button == SDL_CONTROLLER_BUTTON_B ) { if ( modes & KEY_PRESSED ) { - mouse_pl = mouse_cu; SetModes( MOUSE_PRESSED ); } else { - mouse_rl = mouse_cu; ResetModes( MOUSE_PRESSED ); + SetModes( MOUSE_RELEASED ); + SetModes( MOUSE_CLICKED ); } - mouse_button = SDL_BUTTON_LEFT; - ResetModes( KEY_PRESSED ); - } - else if ( button.button == SDL_CONTROLLER_BUTTON_B ) { - SetModes( CLICK_RIGHT ); - if ( modes & KEY_PRESSED ) { - mouse_pr = mouse_cu; - SetModes( MOUSE_PRESSED ); + + if ( button.button == SDL_CONTROLLER_BUTTON_A ) { + if ( modes & KEY_PRESSED ) { + mouse_pl = mouse_cu; + } + else { + mouse_rl = mouse_cu; + } + + mouse_button = SDL_BUTTON_LEFT; } - else { - mouse_rr = mouse_cu; - ResetModes( MOUSE_PRESSED ); + else if ( button.button == SDL_CONTROLLER_BUTTON_B ) { + if ( modes & KEY_PRESSED ) { + mouse_pr = mouse_cu; + } + else { + mouse_rr = mouse_cu; + } + + mouse_button = SDL_BUTTON_RIGHT; } - mouse_button = SDL_BUTTON_RIGHT; + ResetModes( KEY_PRESSED ); } else if ( modes & KEY_PRESSED ) { @@ -1347,7 +1355,7 @@ bool LocalEvent::MousePressLeft( void ) const bool LocalEvent::MouseReleaseLeft( void ) const { - return !( modes & MOUSE_PRESSED ) && SDL_BUTTON_LEFT == mouse_button; + return ( modes & MOUSE_RELEASED ) && SDL_BUTTON_LEFT == mouse_button; } bool LocalEvent::MousePressMiddle( void ) const @@ -1357,7 +1365,7 @@ bool LocalEvent::MousePressMiddle( void ) const bool LocalEvent::MouseReleaseMiddle( void ) const { - return !( modes & MOUSE_PRESSED ) && SDL_BUTTON_MIDDLE == mouse_button; + return ( modes & MOUSE_RELEASED ) && SDL_BUTTON_MIDDLE == mouse_button; } bool LocalEvent::MousePressRight( void ) const @@ -1367,7 +1375,7 @@ bool LocalEvent::MousePressRight( void ) const bool LocalEvent::MouseReleaseRight( void ) const { - return !( modes & MOUSE_PRESSED ) && SDL_BUTTON_RIGHT == mouse_button; + return ( modes & MOUSE_RELEASED ) && SDL_BUTTON_RIGHT == mouse_button; } void LocalEvent::HandleKeyboardEvent( const SDL_KeyboardEvent & event ) @@ -1400,7 +1408,15 @@ void LocalEvent::HandleMouseMotionEvent( const SDL_MouseMotionEvent & motion ) void LocalEvent::HandleMouseButtonEvent( const SDL_MouseButtonEvent & button ) { - button.state == SDL_PRESSED ? SetModes( MOUSE_PRESSED ) : ResetModes( MOUSE_PRESSED ); + if ( button.state == SDL_PRESSED ) { + SetModes( MOUSE_PRESSED ); + } + else { + ResetModes( MOUSE_PRESSED ); + SetModes( MOUSE_RELEASED ); + SetModes( MOUSE_CLICKED ); + } + mouse_button = button.button; mouse_cu.x = button.x; @@ -1421,17 +1437,14 @@ void LocalEvent::HandleMouseButtonEvent( const SDL_MouseButtonEvent & button ) #endif case SDL_BUTTON_LEFT: mouse_pl = mouse_cu; - SetModes( CLICK_LEFT ); break; case SDL_BUTTON_MIDDLE: mouse_pm = mouse_cu; - SetModes( CLICK_MIDDLE ); break; case SDL_BUTTON_RIGHT: mouse_pr = mouse_cu; - SetModes( CLICK_RIGHT ); break; default: @@ -1448,17 +1461,14 @@ void LocalEvent::HandleMouseButtonEvent( const SDL_MouseButtonEvent & button ) #endif case SDL_BUTTON_LEFT: - SetModes( CLICK_LEFT ); mouse_rl = mouse_cu; break; case SDL_BUTTON_MIDDLE: - SetModes( CLICK_MIDDLE ); mouse_rm = mouse_cu; break; case SDL_BUTTON_RIGHT: - SetModes( CLICK_RIGHT ); mouse_rr = mouse_cu; break; @@ -1479,8 +1489,9 @@ void LocalEvent::HandleMouseWheelEvent( const SDL_MouseWheelEvent & wheel ) bool LocalEvent::MouseClickLeft( void ) { - if ( MouseReleaseLeft() && ( CLICK_LEFT & modes ) ) { - ResetModes( CLICK_LEFT ); + if ( ( modes & MOUSE_CLICKED ) && SDL_BUTTON_LEFT == mouse_button ) { + ResetModes( MOUSE_CLICKED ); + return true; } @@ -1489,8 +1500,9 @@ bool LocalEvent::MouseClickLeft( void ) bool LocalEvent::MouseClickLeft( const Rect & rt ) { - if ( MouseReleaseLeft() && ( rt & mouse_pl ) && ( rt & mouse_rl ) && ( CLICK_LEFT & modes ) ) { - ResetModes( CLICK_LEFT ); + if ( ( modes & MOUSE_CLICKED ) && SDL_BUTTON_LEFT == mouse_button && ( rt & mouse_pl ) && ( rt & mouse_rl ) ) { + ResetModes( MOUSE_CLICKED ); + return true; } @@ -1499,8 +1511,9 @@ bool LocalEvent::MouseClickLeft( const Rect & rt ) bool LocalEvent::MouseClickMiddle( void ) { - if ( MouseReleaseMiddle() && ( CLICK_MIDDLE & modes ) ) { - ResetModes( CLICK_MIDDLE ); + if ( ( modes & MOUSE_CLICKED ) && SDL_BUTTON_MIDDLE == mouse_button ) { + ResetModes( MOUSE_CLICKED ); + return true; } @@ -1509,8 +1522,9 @@ bool LocalEvent::MouseClickMiddle( void ) bool LocalEvent::MouseClickMiddle( const Rect & rt ) { - if ( MouseReleaseMiddle() && ( rt & mouse_pm ) && ( rt & mouse_rm ) && ( CLICK_MIDDLE & modes ) ) { - ResetModes( CLICK_MIDDLE ); + if ( ( modes & MOUSE_CLICKED ) && SDL_BUTTON_MIDDLE == mouse_button && ( rt & mouse_pm ) && ( rt & mouse_rm ) ) { + ResetModes( MOUSE_CLICKED ); + return true; } @@ -1519,8 +1533,9 @@ bool LocalEvent::MouseClickMiddle( const Rect & rt ) bool LocalEvent::MouseClickRight( void ) { - if ( MouseReleaseRight() && ( CLICK_RIGHT & modes ) ) { - ResetModes( CLICK_RIGHT ); + if ( ( modes & MOUSE_CLICKED ) && SDL_BUTTON_RIGHT == mouse_button ) { + ResetModes( MOUSE_CLICKED ); + return true; } @@ -1529,8 +1544,9 @@ bool LocalEvent::MouseClickRight( void ) bool LocalEvent::MouseClickRight( const Rect & rt ) { - if ( MouseReleaseRight() && ( rt & mouse_pr ) && ( rt & mouse_rr ) && ( CLICK_RIGHT & modes ) ) { - ResetModes( CLICK_RIGHT ); + if ( ( modes & MOUSE_CLICKED ) && SDL_BUTTON_RIGHT == mouse_button && ( rt & mouse_pr ) && ( rt & mouse_rr ) ) { + ResetModes( MOUSE_CLICKED ); + return true; } diff --git a/src/engine/localevent.h b/src/engine/localevent.h index 73f9b5fb4bf..31aa3368f32 100644 --- a/src/engine/localevent.h +++ b/src/engine/localevent.h @@ -300,14 +300,14 @@ class LocalEvent { KEY_PRESSED = 0x0001, MOUSE_MOTION = 0x0002, - MOUSE_PRESSED = 0x0004, + MOUSE_PRESSED = 0x0004, // mouse button is currently pressed GLOBAL_FILTER = 0x0008, - CLICK_LEFT = 0x0010, // either there is a click on left button or it was just released - CLICK_RIGHT = 0x0020, // either there is a click on right button or it was just released - CLICK_MIDDLE = 0x0040, // either there is a click on middle button or it was just released - UNUSED_1 = 0x0080, + MOUSE_RELEASED = 0x0010, // mouse button has just been released + MOUSE_CLICKED = 0x0020, // mouse button has been clicked + UNUSED_1 = 0x0040, + UNUSED_2 = 0x0080, MOUSE_OFFSET = 0x0100, - UNUSED_2 = 0x0200, + UNUSED_3 = 0x0200, MOUSE_WHEEL = 0x0400, KEY_HOLD = 0x0800 }; From 141030a4304e30710f54c6f60e690771f0cad8a9 Mon Sep 17 00:00:00 2001 From: tau3 Date: Sat, 6 Mar 2021 15:02:33 +0000 Subject: [PATCH 71/84] Cache released disabled button sprite (#2818) --- src/fheroes2/gui/ui_button.cpp | 14 +++++++++----- src/fheroes2/gui/ui_button.h | 7 ++++++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/fheroes2/gui/ui_button.cpp b/src/fheroes2/gui/ui_button.cpp index 9c72d936ca1..9f6841ae3f7 100644 --- a/src/fheroes2/gui/ui_button.cpp +++ b/src/fheroes2/gui/ui_button.cpp @@ -55,6 +55,8 @@ namespace fheroes2 , _isPressed( false ) , _isEnabled( true ) , _isVisible( true ) + , _releasedSprite( nullptr ) + , _releasedDisabled() {} ButtonBase::~ButtonBase() {} @@ -136,7 +138,7 @@ namespace fheroes2 _offsetY = offsetY_; } - void ButtonBase::draw( Image & area ) const + void ButtonBase::draw( Image & area ) { if ( !isVisible() ) return; @@ -152,10 +154,12 @@ namespace fheroes2 Blit( sprite, area, _offsetX + sprite.x(), _offsetY + sprite.y() ); } else { - // TODO: cache this Sprite to speed up everything - Sprite image = sprite; - ApplyPalette( image, PAL::GetPalette( PAL::PaletteType::DARKENING ) ); - Blit( image, area, _offsetX + sprite.x(), _offsetY + sprite.y() ); + if ( !_releasedDisabled || ( _releasedSprite != &sprite ) ) { + _releasedSprite = &sprite; + _releasedDisabled.reset( new Sprite( sprite ) ); + ApplyPalette( *_releasedDisabled, PAL::GetPalette( PAL::PaletteType::DARKENING ) ); + } + Blit( *_releasedDisabled, area, _offsetX + _releasedDisabled->x(), _offsetY + _releasedDisabled->y() ); } } } diff --git a/src/fheroes2/gui/ui_button.h b/src/fheroes2/gui/ui_button.h index 48ec792f166..4ea552f7f4c 100644 --- a/src/fheroes2/gui/ui_button.h +++ b/src/fheroes2/gui/ui_button.h @@ -21,6 +21,7 @@ #pragma once #include "screen.h" +#include namespace fheroes2 { @@ -29,6 +30,7 @@ namespace fheroes2 { public: ActionObject(); + virtual ~ActionObject() {} void subscribe( ActionObject * receiver ); void unsubscribe(); @@ -65,7 +67,7 @@ namespace fheroes2 void setPosition( int32_t offsetX_, int32_t offsetY_ ); - void draw( Image & area = Display::instance() ) const; // will draw on screen by default + void draw( Image & area = Display::instance() ); // will draw on screen by default bool drawOnPress( Image & area = Display::instance() ); // will draw on screen by default. Returns true in case of state change bool drawOnRelease( Image & area = Display::instance() ); // will draw on screen by default. Returns true in case of state change @@ -82,6 +84,9 @@ namespace fheroes2 bool _isPressed; bool _isEnabled; bool _isVisible; + + const Sprite * _releasedSprite; + std::unique_ptr _releasedDisabled; }; class Button : public ButtonBase From 0d6f16b7a0912e1061b5343de9b3352c9cb190b2 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sun, 7 Mar 2021 06:56:43 +0300 Subject: [PATCH 72/84] Don't give an extra day of life to a player without castles. (#2909) close #2647 --- src/fheroes2/kingdom/kingdom.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fheroes2/kingdom/kingdom.cpp b/src/fheroes2/kingdom/kingdom.cpp index 7a7af56fb90..f922636ec0b 100644 --- a/src/fheroes2/kingdom/kingdom.cpp +++ b/src/fheroes2/kingdom/kingdom.cpp @@ -72,7 +72,7 @@ void Kingdom::clear( void ) color = Color::NONE; visited_tents_colors = 0; - lost_town_days = Game::GetLostTownDays() + 1; + lost_town_days = Game::GetLostTownDays(); heroes.clear(); castles.clear(); @@ -281,7 +281,7 @@ void Kingdom::AddCastle( const Castle * castle ) AI::Get().CastleAdd( *castle ); } - lost_town_days = Game::GetLostTownDays() + 1; + lost_town_days = Game::GetLostTownDays(); } void Kingdom::RemoveCastle( const Castle * castle ) From 29e2ebd30e5bcaa40d97424e8874628c475758d6 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Sun, 7 Mar 2021 13:01:30 +0800 Subject: [PATCH 73/84] Fix Wildcard warnings by Visual Studio (#2911) --- fheroes2-vs2015.vcxproj | 370 +++++++++++++++++++++++++++++++++++++++- fheroes2-vs2019.vcxproj | 370 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 726 insertions(+), 14 deletions(-) diff --git a/fheroes2-vs2015.vcxproj b/fheroes2-vs2015.vcxproj index ea31b780788..cb27ea88c14 100644 --- a/fheroes2-vs2015.vcxproj +++ b/fheroes2-vs2015.vcxproj @@ -170,15 +170,371 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fheroes2-vs2019.vcxproj b/fheroes2-vs2019.vcxproj index 790b227b9a1..a0b085d8419 100644 --- a/fheroes2-vs2019.vcxproj +++ b/fheroes2-vs2019.vcxproj @@ -171,15 +171,371 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From b36a5f412f9a6f71a8dfa11c2950d04cf51025c2 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Sun, 7 Mar 2021 17:48:40 +0800 Subject: [PATCH 74/84] Add new button sprites (#2912) close #2907 --- src/fheroes2/agg/agg.cpp | 44 ++++++++++++++++++++++++++++ src/fheroes2/agg/icn.h | 5 ++++ src/fheroes2/heroes/heroes_spell.cpp | 7 ++--- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/fheroes2/agg/agg.cpp b/src/fheroes2/agg/agg.cpp index 8a01f7e8eda..d7b6b3721bd 100644 --- a/src/fheroes2/agg/agg.cpp +++ b/src/fheroes2/agg/agg.cpp @@ -1320,6 +1320,50 @@ namespace fheroes2 ReplaceColorId( _icnVsSprite[id][0], 28, 56 ); } return true; + case ICN::NON_UNIFORM_GOOD_OKAY_BUTTON: + LoadOriginalICN( ICN::CAMPXTRG ); + if ( _icnVsSprite[ICN::CAMPXTRG].size() >= 6 ) { + _icnVsSprite[id].resize( 2 ); + _icnVsSprite[id][0] = Crop( _icnVsSprite[ICN::CAMPXTRG][4], 6, 0, 96, 25 ); + _icnVsSprite[id][0].setPosition( 0, 0 ); + + _icnVsSprite[id][1] = _icnVsSprite[ICN::CAMPXTRG][5]; + _icnVsSprite[id][1].setPosition( 0, 0 ); + } + return true; + case ICN::NON_UNIFORM_GOOD_CANCEL_BUTTON: + LoadOriginalICN( ICN::CAMPXTRG ); + if ( _icnVsSprite[ICN::CAMPXTRG].size() >= 8 ) { + _icnVsSprite[id].resize( 2 ); + _icnVsSprite[id][0] = Crop( _icnVsSprite[ICN::CAMPXTRG][6], 6, 0, 96, 25 ); + _icnVsSprite[id][0].setPosition( 0, 0 ); + + _icnVsSprite[id][1] = _icnVsSprite[ICN::CAMPXTRG][7]; + _icnVsSprite[id][1].setPosition( 0, 0 ); + } + return true; + case ICN::NON_UNIFORM_EVIL_OKAY_BUTTON: + LoadOriginalICN( ICN::CAMPXTRE ); + if ( _icnVsSprite[ICN::CAMPXTRE].size() >= 6 ) { + _icnVsSprite[id].resize( 2 ); + _icnVsSprite[id][0] = Crop( _icnVsSprite[ICN::CAMPXTRE][4], 4, 0, 96, 25 ); + _icnVsSprite[id][0].setPosition( 0, 0 ); + + _icnVsSprite[id][1] = _icnVsSprite[ICN::CAMPXTRE][5]; + _icnVsSprite[id][1].setPosition( 0, 0 ); + } + return true; + case ICN::NON_UNIFORM_EVIL_CANCEL_BUTTON: + LoadOriginalICN( ICN::CAMPXTRE ); + if ( _icnVsSprite[ICN::CAMPXTRE].size() >= 8 ) { + _icnVsSprite[id].resize( 2 ); + _icnVsSprite[id][0] = Crop( _icnVsSprite[ICN::CAMPXTRE][6], 4, 0, 96, 25 ); + _icnVsSprite[id][0].setPosition( 0, 0 ); + + _icnVsSprite[id][1] = _icnVsSprite[ICN::CAMPXTRE][7]; + _icnVsSprite[id][1].setPosition( 0, 0 ); + } + return true; default: break; } diff --git a/src/fheroes2/agg/icn.h b/src/fheroes2/agg/icn.h index c21a4791d49..73d9462d425 100644 --- a/src/fheroes2/agg/icn.h +++ b/src/fheroes2/agg/icn.h @@ -919,6 +919,11 @@ namespace ICN MONSTER_SWITCH_LEFT_ARROW, MONSTER_SWITCH_RIGHT_ARROW, + NON_UNIFORM_GOOD_OKAY_BUTTON, + NON_UNIFORM_GOOD_CANCEL_BUTTON, + NON_UNIFORM_EVIL_OKAY_BUTTON, + NON_UNIFORM_EVIL_CANCEL_BUTTON, + // IMPORTANT! Put any new entry just above this one. LASTICN }; diff --git a/src/fheroes2/heroes/heroes_spell.cpp b/src/fheroes2/heroes/heroes_spell.cpp index 030f920e965..0c1678902a3 100644 --- a/src/fheroes2/heroes/heroes_spell.cpp +++ b/src/fheroes2/heroes/heroes_spell.cpp @@ -536,10 +536,9 @@ bool ActionSpellTownPortal( Heroes & hero ) listbox.Redraw(); fheroes2::ButtonGroup btnGroups; - const int buttonIcnId = isEvilInterface ? ICN::SYSTEME : ICN::REQUESTS; - - btnGroups.createButton( area.x, area.y + 222, buttonIcnId, 1, 2, Dialog::OK ); - btnGroups.createButton( area.x + 182, area.y + 222, buttonIcnId, 3, 4, Dialog::CANCEL ); + btnGroups.createButton( area.x, area.y + 222, isEvilInterface ? ICN::NON_UNIFORM_EVIL_OKAY_BUTTON : ICN::NON_UNIFORM_GOOD_OKAY_BUTTON, 0, 1, Dialog::OK ); + btnGroups.createButton( area.x + 182, area.y + 222, isEvilInterface ? ICN::NON_UNIFORM_EVIL_CANCEL_BUTTON : ICN::NON_UNIFORM_GOOD_CANCEL_BUTTON, 0, 1, + Dialog::CANCEL ); btnGroups.draw(); cursor.Show(); From fa69f054a000d52f2295956e3c0593d9fedb4e5a Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Mon, 8 Mar 2021 08:40:14 +0800 Subject: [PATCH 75/84] Fix MIN and MAX button drawings (#2914) close #2899 --- src/fheroes2/agg/agg.cpp | 183 ++++++++++++++++----- src/fheroes2/agg/icn.cpp | 1 - src/fheroes2/agg/icn.h | 7 +- src/fheroes2/dialog/dialog_recrut.cpp | 2 +- src/fheroes2/dialog/dialog_selectcount.cpp | 10 +- 5 files changed, 154 insertions(+), 49 deletions(-) diff --git a/src/fheroes2/agg/agg.cpp b/src/fheroes2/agg/agg.cpp index d7b6b3721bd..4ecd0aa4536 100644 --- a/src/fheroes2/agg/agg.cpp +++ b/src/fheroes2/agg/agg.cpp @@ -940,15 +940,16 @@ namespace fheroes2 Blit( GetICN( ICN::BTNHOTST, i ), 72 - i, 21, out, 84 - i, 28, 13, 13 ); } return true; - case ICN::BTNMIN: + case ICN::NON_UNIFORM_GOOD_MIN_BUTTON: _icnVsSprite[id].resize( 2 ); for ( uint32_t i = 0; i < static_cast( _icnVsSprite[id].size() ); ++i ) { Sprite & out = _icnVsSprite[id][i]; out = GetICN( ICN::RECRUIT, 4 + i ); // clean the button - Blit( GetICN( ICN::SYSTEM, 11 + i ), 10, 6, out, 30, 4, 31, 15 ); + Blit( GetICN( ICN::SYSTEM, 11 + i ), 10, 6 + i, out, 30 - 2 * i, 5 + i, 31, 15 ); // add 'IN' - Blit( GetICN( ICN::APANEL, 4 + i ), 23, 20, out, 30, 4, 25, 15 ); + Copy( GetICN( ICN::APANEL, 4 + i ), 23 - i, 22 + i, out, 33 - i, 6 + i, 8, 14 ); // letter 'I' + Copy( GetICN( ICN::APANEL, 4 + i ), 31 - i, 22 + i, out, 44 - i, 6 + i, 17, 14 ); // letter 'N' } return true; case ICN::SPELLS: @@ -1321,49 +1322,153 @@ namespace fheroes2 } return true; case ICN::NON_UNIFORM_GOOD_OKAY_BUTTON: - LoadOriginalICN( ICN::CAMPXTRG ); - if ( _icnVsSprite[ICN::CAMPXTRG].size() >= 6 ) { - _icnVsSprite[id].resize( 2 ); - _icnVsSprite[id][0] = Crop( _icnVsSprite[ICN::CAMPXTRG][4], 6, 0, 96, 25 ); - _icnVsSprite[id][0].setPosition( 0, 0 ); - - _icnVsSprite[id][1] = _icnVsSprite[ICN::CAMPXTRG][5]; - _icnVsSprite[id][1].setPosition( 0, 0 ); - } + _icnVsSprite[id].resize( 2 ); + _icnVsSprite[id][0] = Crop( GetICN( ICN::CAMPXTRG, 4 ), 6, 0, 96, 25 ); + _icnVsSprite[id][0].setPosition( 0, 0 ); + + _icnVsSprite[id][1] = GetICN( ICN::CAMPXTRG, 5 ); + _icnVsSprite[id][1].setPosition( 0, 0 ); return true; case ICN::NON_UNIFORM_GOOD_CANCEL_BUTTON: - LoadOriginalICN( ICN::CAMPXTRG ); - if ( _icnVsSprite[ICN::CAMPXTRG].size() >= 8 ) { - _icnVsSprite[id].resize( 2 ); - _icnVsSprite[id][0] = Crop( _icnVsSprite[ICN::CAMPXTRG][6], 6, 0, 96, 25 ); - _icnVsSprite[id][0].setPosition( 0, 0 ); - - _icnVsSprite[id][1] = _icnVsSprite[ICN::CAMPXTRG][7]; - _icnVsSprite[id][1].setPosition( 0, 0 ); - } + _icnVsSprite[id].resize( 2 ); + _icnVsSprite[id][0] = Crop( GetICN( ICN::CAMPXTRG, 6 ), 6, 0, 96, 25 ); + _icnVsSprite[id][0].setPosition( 0, 0 ); + + _icnVsSprite[id][1] = GetICN( ICN::CAMPXTRG, 7 ); + _icnVsSprite[id][1].setPosition( 0, 0 ); return true; case ICN::NON_UNIFORM_EVIL_OKAY_BUTTON: - LoadOriginalICN( ICN::CAMPXTRE ); - if ( _icnVsSprite[ICN::CAMPXTRE].size() >= 6 ) { - _icnVsSprite[id].resize( 2 ); - _icnVsSprite[id][0] = Crop( _icnVsSprite[ICN::CAMPXTRE][4], 4, 0, 96, 25 ); - _icnVsSprite[id][0].setPosition( 0, 0 ); - - _icnVsSprite[id][1] = _icnVsSprite[ICN::CAMPXTRE][5]; - _icnVsSprite[id][1].setPosition( 0, 0 ); - } + _icnVsSprite[id].resize( 2 ); + _icnVsSprite[id][0] = Crop( GetICN( ICN::CAMPXTRE, 4 ), 4, 0, 96, 25 ); + _icnVsSprite[id][0].setPosition( 0, 0 ); + + _icnVsSprite[id][1] = GetICN( ICN::CAMPXTRE, 5 ); + _icnVsSprite[id][1].setPosition( 0, 0 ); return true; case ICN::NON_UNIFORM_EVIL_CANCEL_BUTTON: - LoadOriginalICN( ICN::CAMPXTRE ); - if ( _icnVsSprite[ICN::CAMPXTRE].size() >= 8 ) { - _icnVsSprite[id].resize( 2 ); - _icnVsSprite[id][0] = Crop( _icnVsSprite[ICN::CAMPXTRE][6], 4, 0, 96, 25 ); - _icnVsSprite[id][0].setPosition( 0, 0 ); - - _icnVsSprite[id][1] = _icnVsSprite[ICN::CAMPXTRE][7]; - _icnVsSprite[id][1].setPosition( 0, 0 ); - } + _icnVsSprite[id].resize( 2 ); + _icnVsSprite[id][0] = Crop( GetICN( ICN::CAMPXTRE, 6 ), 4, 0, 96, 25 ); + _icnVsSprite[id][0].setPosition( 0, 0 ); + + _icnVsSprite[id][1] = GetICN( ICN::CAMPXTRE, 7 ); + _icnVsSprite[id][1].setPosition( 0, 0 ); return true; + case ICN::UNIFORM_GOOD_MAX_BUTTON: { + _icnVsSprite[id].resize( 2 ); + + // Generate background + Image background( 60, 25 ); + Copy( GetICN( ICN::SYSTEM, 12 ), 0, 0, background, 0, 0, 53, 25 ); + Copy( GetICN( ICN::SYSTEM, 12 ), 89, 0, background, 53, 0, 7, 25 ); + + // Released button + Image temp( 60, 25 ); + Copy( GetICN( ICN::SYSTEM, 11 ), 0, 0, temp, 0, 0, 53, 25 ); + Copy( GetICN( ICN::SYSTEM, 11 ), 89, 0, temp, 53, 0, 7, 25 ); + Copy( GetICN( ICN::RECRUIT, 4 ), 9, 2, temp, 4, 2, 52, 19 ); + + _icnVsSprite[id][0] = background; + Blit( temp, _icnVsSprite[id][0] ); + _icnVsSprite[id][0].setPosition( 0, 0 ); + + // Pressed button + _icnVsSprite[id][1] = background; + Copy( GetICN( ICN::RECRUIT, 5 ), 9, 2, _icnVsSprite[id][1], 3, 2, 53, 19 ); + + return true; + } + case ICN::UNIFORM_GOOD_MIN_BUTTON: { + _icnVsSprite[id].resize( 2 ); + + // Generate background + Image background( 60, 25 ); + Copy( GetICN( ICN::SYSTEM, 12 ), 0, 0, background, 0, 0, 53, 25 ); + Copy( GetICN( ICN::SYSTEM, 12 ), 89, 0, background, 53, 0, 7, 25 ); + + // Released button + Image temp( 60, 25 ); + Copy( GetICN( ICN::SYSTEM, 11 ), 0, 0, temp, 0, 0, 53, 25 ); + Copy( GetICN( ICN::SYSTEM, 11 ), 89, 0, temp, 53, 0, 7, 25 ); + Copy( GetICN( ICN::RECRUIT, 4 ), 9, 2, temp, 4, 2, 21, 19 ); // letter 'M' + Copy( GetICN( ICN::APANEL, 4 ), 23, 21, temp, 28, 5, 8, 14 ); // letter 'I' + Copy( GetICN( ICN::APANEL, 4 ), 31, 21, temp, 39, 5, 17, 14 ); // letter 'N' + + _icnVsSprite[id][0] = background; + Blit( temp, _icnVsSprite[id][0] ); + _icnVsSprite[id][0].setPosition( 0, 0 ); + + // Pressed button + _icnVsSprite[id][1] = background; + Copy( GetICN( ICN::RECRUIT, 5 ), 9, 3, _icnVsSprite[id][1], 3, 3, 19, 17 ); // letter 'M' + Copy( GetICN( ICN::APANEL, 5 ), 21, 22, _icnVsSprite[id][1], 25, 6, 8, 14 ); // letter 'I' + Copy( GetICN( ICN::APANEL, 5 ), 30, 21, _icnVsSprite[id][1], 37, 5, 17, 15 ); // letter 'N' + + return true; + } + case ICN::UNIFORM_EVIL_MAX_BUTTON: { + _icnVsSprite[id].resize( 2 ); + + // Generate background + Image background( 60, 25 ); + Copy( GetICN( ICN::SYSTEME, 12 ), 0, 0, background, 0, 0, 53, 25 ); + Copy( GetICN( ICN::SYSTEME, 12 ), 89, 0, background, 53, 0, 7, 25 ); + + // Released button + Image temp( 60, 25 ); + Copy( GetICN( ICN::SYSTEME, 11 ), 0, 0, temp, 0, 0, 53, 25 ); + Copy( GetICN( ICN::SYSTEME, 11 ), 89, 0, temp, 53, 0, 7, 25 ); + Copy( GetICN( ICN::CPANELE, 0 ), 46, 28, temp, 6, 5, 19, 14 ); // letter 'M' + Copy( GetICN( ICN::CSPANBTE, 0 ), 49, 5, temp, 25, 5, 13, 14 ); // letter 'A' + Copy( GetICN( ICN::CSPANBTE, 0 ), 62, 10, temp, 38, 10, 2, 9 ); // rest of letter 'A' + Copy( GetICN( ICN::LGNDXTRE, 4 ), 28, 5, temp, 41, 5, 15, 14 ); // letter 'X' + + _icnVsSprite[id][0] = background; + Blit( temp, _icnVsSprite[id][0] ); + _icnVsSprite[id][0].setPosition( 0, 0 ); + + // Pressed button + _icnVsSprite[id][1] = background; + Copy( GetICN( ICN::CPANELE, 1 ), 45, 29, _icnVsSprite[id][1], 4, 6, 19, 14 ); // letter 'M' + Copy( GetICN( ICN::CSPANBTE, 1 ), 49, 6, _icnVsSprite[id][1], 23, 6, 12, 14 ); // letter 'A' + Copy( GetICN( ICN::CSPANBTE, 1 ), 61, 11, _icnVsSprite[id][1], 35, 11, 3, 9 ); // rest of letter 'A' + Copy( GetICN( ICN::LGNDXTRE, 5 ), 26, 4, _icnVsSprite[id][1], 38, 4, 15, 16 ); // letter 'X' + _icnVsSprite[id][1].image()[353] = 21; + _icnVsSprite[id][1].image()[622] = 21; + _icnVsSprite[id][1].image()[964] = 21; + + return true; + } + case ICN::UNIFORM_EVIL_MIN_BUTTON: { + _icnVsSprite[id].resize( 2 ); + + // Generate background + Image background( 60, 25 ); + Copy( GetICN( ICN::SYSTEME, 12 ), 0, 0, background, 0, 0, 53, 25 ); + Copy( GetICN( ICN::SYSTEME, 12 ), 89, 0, background, 53, 0, 7, 25 ); + + // Released button + Image temp( 60, 25 ); + Copy( GetICN( ICN::SYSTEME, 11 ), 0, 0, temp, 0, 0, 53, 25 ); + Copy( GetICN( ICN::SYSTEME, 11 ), 89, 0, temp, 53, 0, 7, 25 ); + Copy( GetICN( ICN::CPANELE, 0 ), 46, 28, temp, 6, 5, 19, 14 ); // letter 'M' + Copy( GetICN( ICN::APANELE, 4 ), 23, 21, temp, 28, 5, 8, 14 ); // letter 'I' + Copy( GetICN( ICN::APANELE, 4 ), 31, 21, temp, 39, 5, 17, 14 ); // letter 'N' + + _icnVsSprite[id][0] = background; + Blit( temp, _icnVsSprite[id][0] ); + _icnVsSprite[id][0].setPosition( 0, 0 ); + + // Pressed button + _icnVsSprite[id][1] = background; + Copy( GetICN( ICN::CPANELE, 1 ), 45, 29, _icnVsSprite[id][1], 4, 6, 19, 14 ); // letter 'M' + Copy( GetICN( ICN::APANELE, 5 ), 21, 22, _icnVsSprite[id][1], 25, 6, 8, 14 ); // letter 'I' + Copy( GetICN( ICN::APANELE, 5 ), 30, 21, _icnVsSprite[id][1], 37, 5, 17, 15 ); // letter 'N' + _icnVsSprite[id][1].image()[622] = 21; + _icnVsSprite[id][1].image()[964] = 21; + _icnVsSprite[id][1].image()[1162] = 21; + + return true; + } default: break; } diff --git a/src/fheroes2/agg/icn.cpp b/src/fheroes2/agg/icn.cpp index fdb698054ad..abff903b8d5 100644 --- a/src/fheroes2/agg/icn.cpp +++ b/src/fheroes2/agg/icn.cpp @@ -920,7 +920,6 @@ namespace ICN {BTNBATTLEONLY, "BTNBONLY.ICN"}, {BOAT12, "BOAT12.ICN"}, {BTNGIFT, "BTNGIFT.ICN"}, - {BTNMIN, "BTNMIN.ICN"}, {CSLMARKER, "CSLMARKER.ICN"}}; } diff --git a/src/fheroes2/agg/icn.h b/src/fheroes2/agg/icn.h index 73d9462d425..0469bbdb3d0 100644 --- a/src/fheroes2/agg/icn.h +++ b/src/fheroes2/agg/icn.h @@ -908,7 +908,7 @@ namespace ICN BTNGIFT, // not in use anymore BTNGIFT_GOOD, BTNGIFT_EVIL, - BTNMIN, + NON_UNIFORM_GOOD_MIN_BUTTON, CSLMARKER, GRAY_FONT, @@ -924,6 +924,11 @@ namespace ICN NON_UNIFORM_EVIL_OKAY_BUTTON, NON_UNIFORM_EVIL_CANCEL_BUTTON, + UNIFORM_GOOD_MAX_BUTTON, + UNIFORM_GOOD_MIN_BUTTON, + UNIFORM_EVIL_MAX_BUTTON, + UNIFORM_EVIL_MIN_BUTTON, + // IMPORTANT! Put any new entry just above this one. LASTICN }; diff --git a/src/fheroes2/dialog/dialog_recrut.cpp b/src/fheroes2/dialog/dialog_recrut.cpp index 15d61dd6def..9281765a8f4 100644 --- a/src/fheroes2/dialog/dialog_recrut.cpp +++ b/src/fheroes2/dialog/dialog_recrut.cpp @@ -267,7 +267,7 @@ Troop Dialog::RecruitMonster( const Monster & monster0, u32 available, bool ext dst_pt.x = pos.x + 230; dst_pt.y = pos.y + 155; fheroes2::Button buttonMax( dst_pt.x, dst_pt.y, ICN::RECRUIT, 4, 5 ); - fheroes2::Button buttonMin( dst_pt.x, dst_pt.y, ICN::BTNMIN, 0, 1 ); + fheroes2::Button buttonMin( dst_pt.x, dst_pt.y, ICN::NON_UNIFORM_GOOD_MIN_BUTTON, 0, 1 ); dst_pt.x = pos.x + 205; dst_pt.y = pos.y + 154; diff --git a/src/fheroes2/dialog/dialog_selectcount.cpp b/src/fheroes2/dialog/dialog_selectcount.cpp index 84505b88a09..5bc493a2a62 100644 --- a/src/fheroes2/dialog/dialog_selectcount.cpp +++ b/src/fheroes2/dialog/dialog_selectcount.cpp @@ -386,15 +386,11 @@ int Dialog::ArmySplitTroop( const uint32_t freeSlots, const uint32_t redistribut const uint32_t maximumAcceptedValue = savelastTroop ? redistributeMax : redistributeMax - 1; const fheroes2::Point minMaxButtonOffset( pos.x + 165, pos.y + 30 ); - fheroes2::ButtonSprite buttonMax( minMaxButtonOffset.x, minMaxButtonOffset.y ); - fheroes2::ButtonSprite buttonMin( minMaxButtonOffset.x, minMaxButtonOffset.y ); + const bool isEvilInterface = Settings::Get().ExtGameEvilInterface(); + fheroes2::Button buttonMax( minMaxButtonOffset.x, minMaxButtonOffset.y, isEvilInterface ? ICN::UNIFORM_EVIL_MAX_BUTTON : ICN::UNIFORM_GOOD_MAX_BUTTON, 0, 1 ); + fheroes2::Button buttonMin( minMaxButtonOffset.x, minMaxButtonOffset.y, isEvilInterface ? ICN::UNIFORM_EVIL_MIN_BUTTON : ICN::UNIFORM_GOOD_MIN_BUTTON, 0, 1 ); const Rect buttonArea( 5, 0, 61, 25 ); - buttonMax.setSprite( fheroes2::Crop( fheroes2::AGG::GetICN( ICN::RECRUIT, 4 ), buttonArea.x, buttonArea.y, buttonArea.w, buttonArea.h ), - fheroes2::Crop( fheroes2::AGG::GetICN( ICN::RECRUIT, 5 ), buttonArea.x, buttonArea.y, buttonArea.w, buttonArea.h ) ); - buttonMin.setSprite( fheroes2::Crop( fheroes2::AGG::GetICN( ICN::BTNMIN, 0 ), buttonArea.x, buttonArea.y, buttonArea.w, buttonArea.h ), - fheroes2::Crop( fheroes2::AGG::GetICN( ICN::BTNMIN, 1 ), buttonArea.x, buttonArea.y, buttonArea.w, buttonArea.h ) ); - SwitchMaxMinButtons( buttonMin, buttonMax, redistributeCount, maximumAcceptedValue ); LocalEvent & le = LocalEvent::Get(); From 6b4cfa78deb3272f7e4c7ad0cc0a0f933861b40b Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Mon, 8 Mar 2021 13:42:00 +0300 Subject: [PATCH 76/84] Fix the pathfinding near monsters: do not allow the hero to pass through the monster. (#2913) close #2888 --- src/fheroes2/world/world_pathfinding.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/fheroes2/world/world_pathfinding.cpp b/src/fheroes2/world/world_pathfinding.cpp index 931fb5b5ba5..8f5bd56a9b9 100644 --- a/src/fheroes2/world/world_pathfinding.cpp +++ b/src/fheroes2/world/world_pathfinding.cpp @@ -259,6 +259,11 @@ std::list PlayerWorldPathfinder::buildPath( int targetIndex ) const // Follows regular (for user's interface) passability rules void PlayerWorldPathfinder::processCurrentNode( std::vector & nodesToExplore, int pathStart, int currentNodeIdx, bool fromWater ) { + // if current tile contains a monster, skip it + if ( world.GetTiles( currentNodeIdx ).GetObject() == MP2::OBJ_MONSTER ) { + return; + } + const MapsIndexes & monsters = Maps::GetTilesUnderProtection( currentNodeIdx ); // check if current tile is protected, can move only to adjacent monster From afc3e0832790e50fbcdd7f12e561795e51b78412 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Mon, 8 Mar 2021 06:31:38 -0500 Subject: [PATCH 77/84] AI: improvements for wide unit pathfinding (#2908) Fixed wide unit tail check when planning paths to make sure attacker can squeeze through tight spaces. This should eliminate cases of units going around instead of attacking what's in front of them. --- src/fheroes2/battle/battle_pathfinding.cpp | 49 ++++++++++++---------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/src/fheroes2/battle/battle_pathfinding.cpp b/src/fheroes2/battle/battle_pathfinding.cpp index 31a96ed4dc2..b65b99945b5 100644 --- a/src/fheroes2/battle/battle_pathfinding.cpp +++ b/src/fheroes2/battle/battle_pathfinding.cpp @@ -93,35 +93,38 @@ namespace Battle const uint32_t moatPenalty = unit.GetSpeed(); // Initialize the starting cells - const int32_t headIdx = unitHead->GetIndex(); - _cache[headIdx]._cost = 0; - _cache[headIdx]._isOpen = false; - _cache[headIdx]._isLeftDirection = unit.isReflect(); + const int32_t pathStart = unitHead->GetIndex(); + _cache[pathStart]._cost = 0; + _cache[pathStart]._isOpen = false; + _cache[pathStart]._isLeftDirection = unitIsWide && unit.isReflect(); if ( unitIsWide ) { const int32_t tailIdx = unitTail->GetIndex(); - _cache[tailIdx]._from = headIdx; + _cache[tailIdx]._from = pathStart; _cache[tailIdx]._cost = 0; _cache[tailIdx]._isOpen = true; - _cache[headIdx]._isLeftDirection = !unit.isReflect(); + _cache[tailIdx]._isLeftDirection = !unit.isReflect(); } if ( unit.isFlying() ) { const Board & board = *Arena::GetBoard(); + // Find all free spaces on the battle board - flyers can move to any of them for ( Board::const_iterator it = board.begin(); it != board.end(); ++it ) { const int32_t idx = it->GetIndex(); ArenaNode & node = _cache[idx]; - if ( it->isPassable1( true ) ) { + // isPassable3 checks if there's space for unit tail (for wide units) + if ( it->isPassable3( unit, false ) ) { node._isOpen = true; - node._from = headIdx; - node._cost = Battle::Board::GetDistance( headIdx, idx ); + node._from = pathStart; + node._cost = Battle::Board::GetDistance( pathStart, idx ); } else { node._isOpen = false; } } + // Once board movement is determined we look for units save shortest flight path to them for ( Board::const_iterator it = board.begin(); it != board.end(); ++it ) { const Unit * boardUnit = it->GetUnit(); if ( boardUnit && boardUnit->GetUID() != unit.GetUID() ) { @@ -130,7 +133,7 @@ namespace Battle const Indexes & around = Battle::Board::GetAroundIndexes( unitIdx ); for ( const int32_t cell : around ) { - const uint32_t flyingDist = static_cast( Battle::Board::GetDistance( headIdx, cell ) ); + const uint32_t flyingDist = static_cast( Battle::Board::GetDistance( pathStart, cell ) ); if ( hexIsPassable( cell ) && ( flyingDist < unitNode._cost ) ) { unitNode._isOpen = false; unitNode._from = cell; @@ -141,8 +144,9 @@ namespace Battle } } else { + // Walkers - explore moves sequentially from both head and tail cells std::vector nodesToExplore; - nodesToExplore.push_back( headIdx ); + nodesToExplore.push_back( pathStart ); if ( unitIsWide ) nodesToExplore.push_back( unitTail->GetIndex() ); @@ -150,32 +154,35 @@ namespace Battle const int32_t fromNode = nodesToExplore[lastProcessedNode]; ArenaNode & previousNode = _cache[fromNode]; - Indexes aroundCellIds; + Indexes availableMoves; if ( !unitIsWide ) - aroundCellIds = Board::GetAroundIndexes( fromNode ); + availableMoves = Board::GetAroundIndexes( fromNode ); else if ( previousNode._from < 0 ) - aroundCellIds = Board::GetMoveWideIndexes( fromNode, unit.isReflect() ); + availableMoves = Board::GetMoveWideIndexes( fromNode, unit.isReflect() ); else - aroundCellIds = Board::GetMoveWideIndexes( fromNode, ( RIGHT_SIDE & Board::GetDirection( fromNode, previousNode._from ) ) ); + availableMoves = Board::GetMoveWideIndexes( fromNode, ( RIGHT_SIDE & Board::GetDirection( fromNode, previousNode._from ) ) ); - for ( const int32_t newNode : aroundCellIds ) { + for ( const int32_t newNode : availableMoves ) { const Cell * headCell = Board::GetCell( newNode ); - const bool isLeftDirection = unitIsWide && Board::IsLeftDirection( fromNode, newNode, previousNode._isLeftDirection ); - const Cell * tailCell = unitIsWide ? Board::GetCell( isLeftDirection ? newNode + 1 : newNode - 1 ) : nullptr; - if ( headCell->isPassable1( false ) && ( !tailCell || tailCell->isPassable1( false ) ) && ( isPassableBridge || !Board::isBridgeIndex( newNode ) ) ) { + const int32_t newTailIndex = isLeftDirection ? newNode + 1 : newNode - 1; + const Cell * tailCell = ( unitIsWide && pathStart != newTailIndex ) ? Board::GetCell( newTailIndex ) : nullptr; + + // Special case: headCell is *allowed* to have another unit in it, that's why we check isPassable1( false ) instead of isPassable4 + if ( headCell->isPassable1( false ) && ( !tailCell || tailCell->isPassable1( true ) ) && ( isPassableBridge || !Board::isBridgeIndex( newNode ) ) ) { const uint32_t cost = _cache[fromNode]._cost; ArenaNode & node = _cache[newNode]; // Check if we're turning back. No movement at all. - uint32_t additionalCost = ( isLeftDirection != previousNode._isLeftDirection ) ? 0 : 1; + uint32_t additionalCost = ( isLeftDirection != previousNode._isLeftDirection ) ? 0u : 1u; - // Moat penalty consumes all remaining movement. Be careful when dealing with unsigned values + // Moat penalty consumes all remaining movement. Be careful when dealing with unsigned values. if ( isMoatBuilt && Board::isMoatIndex( newNode ) ) { additionalCost += ( moatPenalty > previousNode._cost ) ? moatPenalty - previousNode._cost : 1u; } + // Now we check if headCell has a unit - this determines if hex is passable or just accessible (for attack) if ( headCell->GetUnit() && cost < node._cost ) { node._isOpen = false; node._from = fromNode; From 11e3cf247473f2e4b1f490e9a26a88d7e9253f50 Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Mon, 8 Mar 2021 09:09:51 -0500 Subject: [PATCH 78/84] AI: use mass damage spells in combat (#2903) AI now will consider using crowd favorite damage spells such as Chain Lightning or Armageddon. Refactored spell selection logic and moved it into separate implementation file. More spells to come. --- fheroes2-vs2015.vcxproj | 1 + fheroes2-vs2019.vcxproj | 1 + src/fheroes2/ai/normal/ai_normal.h | 22 ++- src/fheroes2/ai/normal/ai_normal_battle.cpp | 79 +++-------- src/fheroes2/ai/normal/ai_normal_spell.cpp | 144 ++++++++++++++++++++ src/fheroes2/battle/battle_action.cpp | 18 +-- src/fheroes2/battle/battle_arena.h | 2 +- 7 files changed, 197 insertions(+), 70 deletions(-) create mode 100644 src/fheroes2/ai/normal/ai_normal_spell.cpp diff --git a/fheroes2-vs2015.vcxproj b/fheroes2-vs2015.vcxproj index cb27ea88c14..5d6c9513801 100644 --- a/fheroes2-vs2015.vcxproj +++ b/fheroes2-vs2015.vcxproj @@ -212,6 +212,7 @@ + diff --git a/fheroes2-vs2019.vcxproj b/fheroes2-vs2019.vcxproj index a0b085d8419..84c77622113 100644 --- a/fheroes2-vs2019.vcxproj +++ b/fheroes2-vs2019.vcxproj @@ -213,6 +213,7 @@ + diff --git a/src/fheroes2/ai/normal/ai_normal.h b/src/fheroes2/ai/normal/ai_normal.h index 34a80d9b095..7ac3db27997 100644 --- a/src/fheroes2/ai/normal/ai_normal.h +++ b/src/fheroes2/ai/normal/ai_normal.h @@ -24,6 +24,11 @@ #include "ai.h" #include "world_pathfinding.h" +namespace Battle +{ + class Units; +} + namespace AI { struct RegionStats @@ -42,6 +47,18 @@ namespace AI const Battle::Unit * unit = nullptr; }; + struct SpellSeletion + { + int spellID = -1; + int32_t cell = -1; + }; + + struct SpellcastOutcome + { + int32_t cell = -1; + double value = 0.0; + }; + class BattlePlanner { public: @@ -60,9 +77,12 @@ namespace AI Battle::Actions archerDecision( Battle::Arena & arena, const Battle::Unit & currentUnit ); BattleTargetPair meleeUnitOffense( Battle::Arena & arena, const Battle::Unit & currentUnit ); BattleTargetPair meleeUnitDefense( Battle::Arena & arena, const Battle::Unit & currentUnit ); - Battle::Actions forceSpellcastBeforeRetreat( Battle::Arena & arena, const HeroBase * commander ); + SpellSeletion selectBestSpell( Battle::Arena & arena, bool retreating ) const; + SpellcastOutcome spellDamageValue( const Spell & spell, Battle::Arena & arena, const Battle::Units & friendly, const Battle::Units & enemies, + bool retreating ) const; // turn variables that wouldn't persist + const HeroBase * _commander = nullptr; int _myColor = Color::NONE; double _myArmyStrength = 0; double _enemyArmyStrength = 0; diff --git a/src/fheroes2/ai/normal/ai_normal_battle.cpp b/src/fheroes2/ai/normal/ai_normal_battle.cpp index 1139b080c4d..5c0b83c1070 100644 --- a/src/fheroes2/ai/normal/ai_normal_battle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_battle.cpp @@ -78,48 +78,6 @@ namespace AI return currentUnit.isFlying(); } - Actions BattlePlanner::forceSpellcastBeforeRetreat( Arena & arena, const HeroBase * commander ) - { - Actions result; - if ( !isCommanderCanSpellcast( arena, commander ) ) { - return result; - } - - const std::vector allSpells = commander->GetSpells(); - int bestSpell = -1; - double bestHeuristic = 0; - int targetIdx = -1; - - const Units friendly( arena.GetForce( commander->GetColor() ), true ); - const Units enemies( arena.GetForce( commander->GetColor(), true ), true ); - - const int spellPower = commander->GetPower(); - for ( const Spell & spell : allSpells ) { - if ( !commander->HaveSpellPoints( spell ) ) - continue; - - // TODO: add mass spells - if ( spell.isCombat() && spell.isDamage() && spell.isSingleTarget() ) { - const uint32_t totalDamage = spell.Damage() * spellPower; - for ( const Unit * enemy : enemies ) { - const double spellHeuristic - = enemy->GetMonsterStrength() * enemy->HowManyWillKilled( totalDamage * ( 100 - enemy->GetMagicResist( spell, spellPower ) ) / 100 ); - - if ( spellHeuristic > bestHeuristic ) { - bestHeuristic = spellHeuristic; - bestSpell = spell.GetID(); - targetIdx = enemy->GetHeadIndex(); - } - } - } - } - - if ( bestSpell != -1 ) { - result.emplace_back( MSG_BATTLE_CAST, bestSpell, targetIdx ); - } - return result; - } - Actions BattlePlanner::planUnitTurn( Arena & arena, const Unit & currentUnit ) { if ( currentUnit.Modes( SP_BERSERKER ) != 0 ) { @@ -132,7 +90,6 @@ namespace AI analyzeBattleState( arena, currentUnit ); const Force & enemyForce = arena.GetForce( _myColor, true ); - const HeroBase * commander = currentUnit.GetCommander(); DEBUG_LOG( DBG_BATTLE, DBG_TRACE, currentUnit.GetName() << " start their turn. Side: " << _myColor ); @@ -145,10 +102,16 @@ namespace AI "Tactic " << defensiveTactics << " chosen. Archers: " << _myShooterStr << ", vs enemy " << _enemyShooterStr << " ratio is " << enemyArcherRatio ); // Step 2. Check retreat/surrender condition - const Heroes * actualHero = dynamic_cast( commander ); + const Heroes * actualHero = dynamic_cast( _commander ); if ( actualHero && arena.CanRetreatOpponent( _myColor ) && checkRetreatCondition( *actualHero ) ) { - // Cast maximum damage spell - actions = forceSpellcastBeforeRetreat( arena, commander ); + if ( isCommanderCanSpellcast( arena, _commander ) ) { + // Cast maximum damage spell + const SpellSeletion & bestSpell = selectBestSpell( arena, true ); + + if ( bestSpell.spellID != -1 ) { + actions.emplace_back( MSG_BATTLE_CAST, bestSpell.spellID, bestSpell.cell ); + } + } actions.emplace_back( MSG_BATTLE_RETREAT ); actions.emplace_back( MSG_BATTLE_END_TURN, currentUnit.GetUID() ); @@ -158,17 +121,12 @@ namespace AI // Step 3. Calculate spell heuristics // Hero should conserve spellpoints if fighting against monsters or AI and has advantage - if ( !( myOverpoweredArmy && enemyForce.GetControl() == CONTROL_AI ) && isCommanderCanSpellcast( arena, commander ) ) { - // 1. For damage spells - maximum amount of enemy threat lost - // 2. For buffs - friendly unit strength gained - // 3. For debuffs - enemy unit threat lost - // 4. For dispell, resurrect and cure - amount of unit strength recovered - // 5. For antimagic - based on enemy hero spellcasting abilities multiplied by friendly unit strength - - // 6. Cast best spell with highest heuristic on target pointer saved - - // Temporary: force damage spell - actions = forceSpellcastBeforeRetreat( arena, commander ); + if ( !( myOverpoweredArmy && enemyForce.GetControl() == CONTROL_AI ) && isCommanderCanSpellcast( arena, _commander ) ) { + const SpellSeletion & bestSpell = selectBestSpell( arena, false ); + if ( bestSpell.spellID != -1 ) { + actions.emplace_back( MSG_BATTLE_CAST, bestSpell.spellID, bestSpell.cell ); + return actions; + } } // Step 4. Current unit decision tree @@ -220,7 +178,9 @@ namespace AI void BattlePlanner::analyzeBattleState( Arena & arena, const Unit & currentUnit ) { + _commander = currentUnit.GetCommander(); _myColor = currentUnit.GetColor(); + const Force & friendlyForce = arena.GetForce( _myColor ); const Force & enemyForce = arena.GetForce( _myColor, true ); @@ -580,7 +540,8 @@ namespace AI { const Actions & plannedActions = _battlePlanner.planUnitTurn( arena, currentUnit ); actions.insert( actions.end(), plannedActions.begin(), plannedActions.end() ); - - actions.emplace_back( MSG_BATTLE_END_TURN, currentUnit.GetUID() ); + // Do not end the turn if we only cast a spell + if ( plannedActions.size() != 1 || !plannedActions.front().isType( MSG_BATTLE_CAST ) ) + actions.emplace_back( MSG_BATTLE_END_TURN, currentUnit.GetUID() ); } } diff --git a/src/fheroes2/ai/normal/ai_normal_spell.cpp b/src/fheroes2/ai/normal/ai_normal_spell.cpp new file mode 100644 index 00000000000..03f34f1b34d --- /dev/null +++ b/src/fheroes2/ai/normal/ai_normal_spell.cpp @@ -0,0 +1,144 @@ +/*************************************************************************** + * Free Heroes of Might and Magic II: https://github.com/ihhub/fheroes2 * + * Copyright (C) 2021 * + * * + * 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 "ai_normal.h" +#include "battle_arena.h" +#include "battle_army.h" +#include "battle_board.h" +#include "battle_command.h" +#include "battle_troop.h" +#include "heroes.h" +#include "logging.h" + +using namespace Battle; + +namespace AI +{ + SpellSeletion BattlePlanner::selectBestSpell( Arena & arena, bool retreating ) const + { + // Cast best spell with highest heuristic on target pointer saved + SpellSeletion bestSpell; + + // Commander must be set before calling this function! Check both debug/release version + assert( _commander != nullptr ); + if ( _commander == nullptr ) { + return bestSpell; + } + + const std::vector allSpells = _commander->GetSpells(); + const Units friendly( arena.GetForce( _myColor ), true ); + const Units enemies( arena.GetForce( _myColor, true ), true ); + + double bestHeuristic = 0; + + for ( const Spell & spell : allSpells ) { + if ( !_commander->HaveSpellPoints( spell ) || !spell.isCombat() || ( !spell.isDamage() && retreating ) ) + continue; + + if ( spell.isDamage() ) { + const SpellcastOutcome & outcome = spellDamageValue( spell, arena, friendly, enemies, retreating ); + if ( outcome.value > bestHeuristic ) { + bestHeuristic = outcome.value; + bestSpell.spellID = spell.GetID(); + bestSpell.cell = outcome.cell; + } + } + } + return bestSpell; + } + + SpellcastOutcome BattlePlanner::spellDamageValue( const Spell & spell, Arena & arena, const Units & friendly, const Units & enemies, bool retreating ) const + { + SpellcastOutcome bestOutcome; + if ( !spell.isDamage() ) + return bestOutcome; + + const int spellPower = _commander->GetPower(); + const uint32_t totalDamage = spell.Damage() * spellPower; + + auto damageHeuristic = [&totalDamage, &spell, &spellPower, &retreating]( const Unit * unit ) { + const uint32_t damage = totalDamage * ( 100 - unit->GetMagicResist( spell, spellPower ) ) / 100; + // If we're retreating we don't care about partial damage, only actual units killed + if ( retreating ) + return unit->GetMonsterStrength() * unit->HowManyWillKilled( damage ); + // Otherwise calculate amount of strength lost (% of unit times total strength) + return std::min( static_cast( damage ) / unit->GetHitPoints(), 1.0 ) * unit->GetStrength(); + }; + + if ( spell.isSingleTarget() ) { + for ( const Unit * enemy : enemies ) { + const double spellHeuristic = damageHeuristic( enemy ); + + if ( spellHeuristic > bestOutcome.value ) { + bestOutcome.value = spellHeuristic; + bestOutcome.cell = enemy->GetHeadIndex(); + } + } + } + else if ( spell.isApplyWithoutFocusObject() ) { + double spellHeuristic = 0; + for ( const Unit * enemy : enemies ) { + spellHeuristic += damageHeuristic( enemy ); + } + for ( const Unit * unit : friendly ) { + spellHeuristic -= damageHeuristic( unit ); + } + + if ( spellHeuristic > bestOutcome.value ) { + bestOutcome.value = spellHeuristic; + } + } + else { + // Area of effect spells like Fireball + auto areaOfEffectCheck = [&damageHeuristic, &bestOutcome]( const TargetsInfo & targets, const int32_t index, int myColor ) { + double spellHeuristic = 0; + for ( const TargetInfo & target : targets ) { + if ( target.defender->GetCurrentColor() == myColor ) { + spellHeuristic -= damageHeuristic( target.defender ); + } + else { + spellHeuristic += damageHeuristic( target.defender ); + } + } + + if ( spellHeuristic > bestOutcome.value ) { + bestOutcome.value = spellHeuristic; + bestOutcome.cell = index; + } + }; + + if ( spell.GetID() == Spell::CHAINLIGHTNING ) { + for ( const Unit * enemy : enemies ) { + const int32_t index = enemy->GetHeadIndex(); + areaOfEffectCheck( arena.GetTargetsForSpells( _commander, spell, index, false ), index, _myColor ); + } + } + else { + const Board & board = *Arena::GetBoard(); + for ( const Cell & cell : board ) { + const int32_t index = cell.GetIndex(); + areaOfEffectCheck( arena.GetTargetsForSpells( _commander, spell, index, false ), index, _myColor ); + } + } + } + + return bestOutcome; + } +} diff --git a/src/fheroes2/battle/battle_action.cpp b/src/fheroes2/battle/battle_action.cpp index 319d199915d..0dee4816671 100644 --- a/src/fheroes2/battle/battle_action.cpp +++ b/src/fheroes2/battle/battle_action.cpp @@ -102,7 +102,7 @@ void Battle::Arena::BattleProcess( Unit & attacker, Unit & defender, s32 dst, in // magic attack if ( defender.isValid() && spell.isValid() ) { const std::string name( attacker.GetName() ); - targets = GetTargetsForSpells( attacker.GetCommander(), spell, defender.GetHeadIndex() ); + targets = GetTargetsForSpells( attacker.GetCommander(), spell, defender.GetHeadIndex(), true ); bool validSpell = true; if ( attacker == Monster::ARCHMAGE && !defender.Modes( IS_GOOD_MAGIC ) ) @@ -681,7 +681,7 @@ Battle::TargetsInfo Battle::Arena::TargetsForChainLightning( const HeroBase * he return targets; } -Battle::TargetsInfo Battle::Arena::GetTargetsForSpells( const HeroBase * hero, const Spell & spell, s32 dst ) +Battle::TargetsInfo Battle::Arena::GetTargetsForSpells( const HeroBase * hero, const Spell & spell, int32_t dest, bool showMessages ) { TargetsInfo targets; targets.reserve( 8 ); @@ -690,7 +690,7 @@ Battle::TargetsInfo Battle::Arena::GetTargetsForSpells( const HeroBase * hero, c bool playResistSound = true; TargetInfo res; - Unit * target = GetTroopBoard( dst ); + Unit * target = GetTroopBoard( dest ); // from spells switch ( spell() ) { @@ -711,8 +711,8 @@ Battle::TargetsInfo Battle::Arena::GetTargetsForSpells( const HeroBase * hero, c } // resurrect spell? get target from graveyard - if ( NULL == target && GraveyardAllowResurrect( dst, spell ) ) { - target = GetTroopUID( graveyard.GetLastTroopUID( dst ) ); + if ( NULL == target && GraveyardAllowResurrect( dest, spell ) ) { + target = GetTroopUID( graveyard.GetLastTroopUID( dest ) ); if ( target && target->AllowApplySpell( spell, hero ) ) { res.defender = target; @@ -723,7 +723,7 @@ Battle::TargetsInfo Battle::Arena::GetTargetsForSpells( const HeroBase * hero, c // check other spells switch ( spell() ) { case Spell::CHAINLIGHTNING: { - TargetsInfo targetsForSpell = TargetsForChainLightning( hero, dst ); + TargetsInfo targetsForSpell = TargetsForChainLightning( hero, dest ); targets.insert( targets.end(), targetsForSpell.begin(), targetsForSpell.end() ); ignoreMagicResistance = true; playResistSound = false; @@ -734,7 +734,7 @@ Battle::TargetsInfo Battle::Arena::GetTargetsForSpells( const HeroBase * hero, c case Spell::METEORSHOWER: case Spell::COLDRING: case Spell::FIREBLAST: { - const Indexes positions = Board::GetDistanceIndexes( dst, ( spell == Spell::FIREBLAST ? 2 : 1 ) ); + const Indexes positions = Board::GetDistanceIndexes( dest, ( spell == Spell::FIREBLAST ? 2 : 1 ) ); for ( Indexes::const_iterator it = positions.begin(); it != positions.end(); ++it ) { Unit * targetUnit = GetTroopBoard( *it ); @@ -787,7 +787,7 @@ Battle::TargetsInfo Battle::Arena::GetTargetsForSpells( const HeroBase * hero, c const u32 resist = ( *it ).defender->GetMagicResist( spell, hero ? hero->GetPower() : 0 ); if ( 0 < resist && 100 > resist && resist >= Rand::Get( 1, 100 ) ) { - if ( interface ) + if ( showMessages && interface ) interface->RedrawActionResistSpell( *( *it ).defender, playResistSound ); it = targets.erase( it ); @@ -890,7 +890,7 @@ void Battle::Arena::ApplyActionSpellDefaults( Command & cmd, const Spell & spell const int32_t dst = cmd.GetValue(); - TargetsInfo targets = GetTargetsForSpells( current_commander, spell, dst ); + TargetsInfo targets = GetTargetsForSpells( current_commander, spell, dst, true ); if ( interface ) interface->RedrawActionSpellCastPart1( spell, dst, current_commander, current_commander->GetName(), targets ); diff --git a/src/fheroes2/battle/battle_arena.h b/src/fheroes2/battle/battle_arena.h index 50608f2cdb8..fa1865efcac 100644 --- a/src/fheroes2/battle/battle_arena.h +++ b/src/fheroes2/battle/battle_arena.h @@ -114,7 +114,7 @@ namespace Battle TargetsInfo GetTargetsForDamage( const Unit &, Unit &, s32 ); void TargetsApplyDamage( Unit &, const Unit &, TargetsInfo & ); - TargetsInfo GetTargetsForSpells( const HeroBase *, const Spell &, s32 ); + TargetsInfo GetTargetsForSpells( const HeroBase * hero, const Spell & spell, int32_t dest, bool showMessages ); void TargetsApplySpell( const HeroBase *, const Spell &, TargetsInfo & ); bool isSpellcastDisabled() const; From 4f1ef53420633a5621b1bf0e2894c25a7d715197 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Tue, 9 Mar 2021 02:54:19 +0300 Subject: [PATCH 79/84] Show the build window with the OKAY button disabled if there is not enough resources to build this building - to match the original game in this aspect. (#2924) close #1009 --- src/fheroes2/castle/buildinginfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fheroes2/castle/buildinginfo.cpp b/src/fheroes2/castle/buildinginfo.cpp index 2ec842b59c9..d76e11deebf 100644 --- a/src/fheroes2/castle/buildinginfo.cpp +++ b/src/fheroes2/castle/buildinginfo.cpp @@ -509,7 +509,7 @@ bool BuildingInfo::QueueEventProcessing( fheroes2::ButtonBase & exitButton ) LocalEvent & le = LocalEvent::Get(); if ( le.MouseClickLeft( area ) ) { - if ( bcond == ALLOW_BUILD ) { + if ( bcond == LACK_RESOURCES || bcond == ALLOW_BUILD ) { fheroes2::ButtonRestorer exitRestorer( exitButton ); return DialogBuyBuilding( true ); } From 708dc37d7f8f1191b6826894c62b8b7745ca964f Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Tue, 9 Mar 2021 10:15:14 -0500 Subject: [PATCH 80/84] Fix double hex attack target for AI troops (#2916) close #2723 --- src/fheroes2/ai/normal/ai_normal_battle.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/fheroes2/ai/normal/ai_normal_battle.cpp b/src/fheroes2/ai/normal/ai_normal_battle.cpp index 5c0b83c1070..20a9d1001e3 100644 --- a/src/fheroes2/ai/normal/ai_normal_battle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_battle.cpp @@ -45,6 +45,16 @@ namespace AI const double STRENGTH_DISTANCE_FACTOR = 5.0; const std::vector underWallsIndicies = {7, 28, 49, 72, 95}; + int32_t correctAttackTarget( const Position & target, const int32_t from ) + { + const Cell * tail = target.GetTail(); + if ( tail && Board::isNearIndexes( from, tail->GetIndex() ) ) { + return tail->GetIndex(); + } + const Cell * head = target.GetHead(); + return head ? head->GetIndex() : -1; + } + void Normal::HeroesPreBattle( HeroBase & hero, bool isAttacking ) { if ( isAttacking ) { @@ -159,7 +169,8 @@ namespace AI actions.emplace_back( MSG_BATTLE_MOVE, currentUnit.GetUID(), target.cell ); if ( target.unit ) { - actions.emplace_back( MSG_BATTLE_ATTACK, currentUnit.GetUID(), target.unit->GetUID(), target.unit->GetHeadIndex(), 0 ); + actions.emplace_back( MSG_BATTLE_ATTACK, currentUnit.GetUID(), target.unit->GetUID(), correctAttackTarget( target.unit->GetPosition(), target.cell ), + 0 ); DEBUG_LOG( DBG_BATTLE, DBG_INFO, currentUnit.GetName() << " melee offense, focus enemy " << target.unit->GetName() << " threat level: " << target.unit->GetScoreQuality( currentUnit ) ); From 2ed96862c49197c1a6b8b682f001d7b03dec6d89 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Tue, 9 Mar 2021 18:53:11 +0300 Subject: [PATCH 81/84] If the unit attacks twice, apply the second attack to the same cell as the first attack. (#2923) close #1876 --- src/fheroes2/battle/battle_action.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fheroes2/battle/battle_action.cpp b/src/fheroes2/battle/battle_action.cpp index 0dee4816671..122931af564 100644 --- a/src/fheroes2/battle/battle_action.cpp +++ b/src/fheroes2/battle/battle_action.cpp @@ -258,7 +258,7 @@ void Battle::Arena::ApplyActionAttack( Command & cmd ) // twice attack if ( b1->isValid() && b1->isTwiceAttack() && !b1->Modes( SP_BLIND | IS_PARALYZE_MAGIC ) ) { DEBUG_LOG( DBG_BATTLE, DBG_TRACE, "twice attack" ); - BattleProcess( *b1, *b2 ); + BattleProcess( *b1, *b2, dst, dir ); } } From 89f4ba013fa6c930d78983960471db4494e0ca1a Mon Sep 17 00:00:00 2001 From: Ivan Shibanov Date: Tue, 9 Mar 2021 11:20:55 -0500 Subject: [PATCH 82/84] AI: ranged troops should move when blocked by a strong unit (#2915) close #2747 --- src/fheroes2/ai/normal/ai_normal_battle.cpp | 74 +++++++++++++-------- src/fheroes2/battle/battle_arena.cpp | 5 ++ src/fheroes2/battle/battle_arena.h | 1 + src/fheroes2/battle/battle_board.cpp | 8 +-- src/fheroes2/battle/battle_board.h | 2 +- src/fheroes2/battle/battle_pathfinding.cpp | 24 ++++++- src/fheroes2/battle/battle_pathfinding.h | 4 ++ src/fheroes2/battle/battle_troop.cpp | 7 +- src/fheroes2/battle/battle_troop.h | 1 + 9 files changed, 91 insertions(+), 35 deletions(-) diff --git a/src/fheroes2/ai/normal/ai_normal_battle.cpp b/src/fheroes2/ai/normal/ai_normal_battle.cpp index 20a9d1001e3..45592bb69fa 100644 --- a/src/fheroes2/ai/normal/ai_normal_battle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_battle.cpp @@ -141,6 +141,7 @@ namespace AI // Step 4. Current unit decision tree const size_t actionsSize = actions.size(); + Battle::Arena::GetBoard()->SetPositionQuality( currentUnit ); if ( currentUnit.isArchers() ) { const Actions & archerActions = archerDecision( arena, currentUnit ); @@ -148,12 +149,9 @@ namespace AI } else { // Melee unit decision tree (both flyers and walkers) - Board & board = *Battle::Arena::GetBoard(); - board.SetPositionQuality( currentUnit ); - - // Determine unit target/cell to move BattleTargetPair target; + // Determine unit target or cell to move to if ( defensiveTactics ) { target = meleeUnitDefense( arena, currentUnit ); } @@ -281,15 +279,13 @@ namespace AI { Actions actions; const Units enemies( arena.GetForce( _myColor, true ), true ); - const Unit * target = NULL; - int targetCell = -1; + BattleTargetPair target; if ( currentUnit.isHandFighting() ) { // Current ranged unit is blocked by the enemy - // force archer to fight back by setting initial expectation to lowest possible (if we're losing battle) + // Force archer to fight back by setting initial expectation to lowest possible (if we're losing battle) int bestOutcome = ( _myArmyStrength < _enemyArmyStrength ) ? -_highestDamageExpected : 0; - bool canOutrunEnemy = true; const Indexes & adjacentEnemies = Board::GetAdjacentEnemies( currentUnit ); for ( const int cell : adjacentEnemies ) { @@ -300,28 +296,48 @@ namespace AI if ( bestOutcome < damageDiff ) { bestOutcome = damageDiff; - target = enemy; - targetCell = cell; + target.unit = enemy; + target.cell = cell; } - - // try to determine if it's worth running away (canOutrunEnemy stays true ONLY if all enemies are slower) - if ( canOutrunEnemy && !isUnitFaster( currentUnit, *enemy ) ) - canOutrunEnemy = false; } else { DEBUG_LOG( DBG_BATTLE, DBG_WARN, "Board::GetAdjacentEnemies returned a cell " << cell << " that does not contain a unit!" ); } } - if ( target && targetCell != -1 ) { - // attack selected target + if ( target.unit && target.cell != -1 ) { + // Melee attack selected target DEBUG_LOG( DBG_BATTLE, DBG_INFO, currentUnit.GetName() << " archer deciding to fight back: " << bestOutcome ); - actions.emplace_back( MSG_BATTLE_ATTACK, currentUnit.GetUID(), target->GetUID(), targetCell, 0 ); + actions.emplace_back( MSG_BATTLE_ATTACK, currentUnit.GetUID(), target.unit->GetUID(), target.cell, 0 ); } - else if ( canOutrunEnemy ) { - // Kiting enemy - // Search for a safe spot unit can move away - DEBUG_LOG( DBG_BATTLE, DBG_INFO, currentUnit.GetName() << " archer kiting enemy" ); + else { + // Kiting enemy: Search for a safe spot unit can move to + double lowestThreat = _enemyArmyStrength; + + const Indexes & moves = arena.getAllAvailableMoves( currentUnit.GetMoveRange() ); + for ( const int moveIndex : moves ) { + if ( !Board::GetCell( moveIndex )->GetQuality() ) { + double cellThreatLevel = 0.0; + + for ( const Unit * enemy : enemies ) { + const double ratio = static_cast( Board::GetDistance( moveIndex, enemy->GetHeadIndex() ) ) / std::max( 1u, enemy->GetMoveRange() ); + cellThreatLevel += enemy->GetScoreQuality( currentUnit ) * ( 1.0 - ratio ); + } + + if ( cellThreatLevel < lowestThreat ) { + lowestThreat = cellThreatLevel; + target.cell = moveIndex; + } + } + } + + if ( target.cell != -1 ) { + actions.emplace_back( MSG_BATTLE_MOVE, currentUnit.GetUID(), target.cell ); + DEBUG_LOG( DBG_BATTLE, DBG_INFO, currentUnit.GetName() << " archer kiting enemy, moves to " << target.cell << " threat is " << lowestThreat ); + } + else { + DEBUG_LOG( DBG_BATTLE, DBG_TRACE, currentUnit.GetName() << " archer couldn't find a good hex to move out of " << moves.size() ); + } } // Worst case scenario - Skip turn } @@ -334,16 +350,17 @@ namespace AI if ( highestStrength < attackPriority ) { highestStrength = attackPriority; - target = enemy; + target.unit = enemy; DEBUG_LOG( DBG_BATTLE, DBG_TRACE, "- Set priority on " << enemy->GetName() << " value " << attackPriority ); } } - if ( target ) { - actions.emplace_back( MSG_BATTLE_ATTACK, currentUnit.GetUID(), target->GetUID(), target->GetHeadIndex(), 0 ); + if ( target.unit ) { + actions.emplace_back( MSG_BATTLE_ATTACK, currentUnit.GetUID(), target.unit->GetUID(), target.unit->GetHeadIndex(), 0 ); DEBUG_LOG( DBG_BATTLE, DBG_INFO, - currentUnit.GetName() << " archer focusing enemy " << target->GetName() << " threat level: " << target->GetScoreQuality( currentUnit ) ); + currentUnit.GetName() << " archer focusing enemy " << target.unit->GetName() + << " threat level: " << target.unit->GetScoreQuality( currentUnit ) ); } } @@ -355,7 +372,7 @@ namespace AI BattleTargetPair target; const Units enemies( arena.GetForce( _myColor, true ), true ); - const uint32_t currentUnitMoveRange = currentUnit.isFlying() ? MAXU16 : currentUnit.GetSpeed(); + const uint32_t currentUnitMoveRange = currentUnit.GetMoveRange(); const double attackDistanceModifier = _enemyArmyStrength / STRENGTH_DISTANCE_FACTOR; double maxPriority = attackDistanceModifier * ARENASIZE * -1; @@ -387,6 +404,9 @@ namespace AI target.cell = move.first; } } + else { + DEBUG_LOG( DBG_BATTLE, DBG_TRACE, currentUnit.GetName() << " is attacking " << target.unit->GetName() << " at " << target.cell ); + } } // Walkers: move closer to the castle walls during siege @@ -417,7 +437,7 @@ namespace AI const Units friendly( arena.GetForce( _myColor ), true ); const Units enemies( arena.GetForce( _myColor, true ), true ); - const uint32_t currentUnitMoveRange = currentUnit.isFlying() ? MAXU16 : currentUnit.GetSpeed(); + const uint32_t currentUnitMoveRange = currentUnit.GetMoveRange(); const int myHeadIndex = currentUnit.GetHeadIndex(); const double attackDistanceModifier = _enemyArmyStrength / STRENGTH_DISTANCE_FACTOR; diff --git a/src/fheroes2/battle/battle_arena.cpp b/src/fheroes2/battle/battle_arena.cpp index c3c4a2b7133..53edecb742f 100644 --- a/src/fheroes2/battle/battle_arena.cpp +++ b/src/fheroes2/battle/battle_arena.cpp @@ -619,6 +619,11 @@ bool Battle::Arena::hexIsPassable( int32_t indexTo ) return Board::isValidIndex( indexTo ) && _pathfinder.hexIsPassable( indexTo ); } +Battle::Indexes Battle::Arena::getAllAvailableMoves( uint32_t moveRange ) const +{ + return _pathfinder.getAllAvailableMoves( moveRange ); +} + Battle::Unit * Battle::Arena::GetTroopBoard( s32 index ) { return Board::isValidIndex( index ) ? board[index].GetUnit() : NULL; diff --git a/src/fheroes2/battle/battle_arena.h b/src/fheroes2/battle/battle_arena.h index fa1865efcac..70cdf5d8c5e 100644 --- a/src/fheroes2/battle/battle_arena.h +++ b/src/fheroes2/battle/battle_arena.h @@ -108,6 +108,7 @@ namespace Battle uint32_t CalculateMoveDistance( int32_t indexTo ); bool hexIsAccessible( int32_t indexTo ); bool hexIsPassable( int32_t indexTo ); + Indexes getAllAvailableMoves( uint32_t moveRange ) const; Indexes GetPath( const Unit &, const Position & ); void ApplyAction( Command & ); diff --git a/src/fheroes2/battle/battle_board.cpp b/src/fheroes2/battle/battle_board.cpp index 65bbc7029c9..d15c6fa5093 100644 --- a/src/fheroes2/battle/battle_board.cpp +++ b/src/fheroes2/battle/battle_board.cpp @@ -163,7 +163,7 @@ void Battle::Board::SetEnemyQuality( const Unit & unit ) } } -s32 Battle::Board::GetDistance( s32 index1, s32 index2 ) +uint32_t Battle::Board::GetDistance( s32 index1, s32 index2 ) { if ( isValidIndex( index1 ) && isValidIndex( index2 ) ) { const int dx = std::abs( ( index1 % ARENAW ) - ( index2 % ARENAW ) ); @@ -171,7 +171,7 @@ s32 Battle::Board::GetDistance( s32 index1, s32 index2 ) const int roundingUp = index1 / ARENAW % 2; // hexagonal grid: you only move half as much on X axis when diagonal! - return dy + std::max( dx - ( dy + roundingUp ) / 2, 0 ); + return static_cast( dy + std::max( dx - ( dy + roundingUp ) / 2, 0 ) ); } return 0; @@ -493,7 +493,7 @@ Battle::Indexes Battle::Board::GetPassableQualityPositions( const Unit & b ) std::vector Battle::Board::GetNearestTroops( const Unit * startUnit, const std::vector & blackList ) { - std::vector > foundUnits; + std::vector > foundUnits; for ( Cell & cell : *this ) { Unit * cellUnit = cell.GetUnit(); @@ -508,7 +508,7 @@ std::vector Battle::Board::GetNearestTroops( const Unit * startU } std::sort( foundUnits.begin(), foundUnits.end(), - []( const std::pair & first, const std::pair & second ) { return first.second < second.second; } ); + []( const std::pair & first, const std::pair & second ) { return first.second < second.second; } ); std::vector units; units.reserve( foundUnits.size() ); diff --git a/src/fheroes2/battle/battle_board.h b/src/fheroes2/battle/battle_board.h index 7932681812a..b91f69cb047 100644 --- a/src/fheroes2/battle/battle_board.h +++ b/src/fheroes2/battle/battle_board.h @@ -82,7 +82,7 @@ namespace Battle static bool isNegativeDistance( s32 index1, s32 index2 ); static int GetReflectDirection( int ); static int GetDirection( s32, s32 ); - static s32 GetDistance( s32, s32 ); + static uint32_t GetDistance( s32, s32 ); static bool isValidDirection( s32, int ); static s32 GetIndexDirection( s32, int ); static Indexes GetDistanceIndexes( s32, u32 ); diff --git a/src/fheroes2/battle/battle_pathfinding.cpp b/src/fheroes2/battle/battle_pathfinding.cpp index b65b99945b5..f8f22ece13a 100644 --- a/src/fheroes2/battle/battle_pathfinding.cpp +++ b/src/fheroes2/battle/battle_pathfinding.cpp @@ -55,7 +55,27 @@ namespace Battle bool ArenaPathfinder::hexIsPassable( int targetCell ) const { - return _cache[targetCell]._cost == 0 || ( _cache[targetCell]._isOpen && _cache[targetCell]._from != -1 ); + const size_t index = static_cast( targetCell ); + return index < _cache.size() && nodeIsAccessible( _cache[index] ); + } + + bool ArenaPathfinder::nodeIsAccessible( const ArenaNode & node ) const + { + return node._cost == 0 || ( node._isOpen && node._from != -1 ); + } + + Indexes ArenaPathfinder::getAllAvailableMoves( uint32_t moveRange ) const + { + Indexes result; + result.reserve( moveRange * 2u ); + + for ( size_t index = 0; index < _cache.size(); ++index ) { + const ArenaNode & node = _cache[index]; + if ( nodeIsAccessible( node ) && node._cost <= moveRange ) { + result.push_back( index ); + } + } + return result; } std::list ArenaPathfinder::buildPath( int targetCell ) const @@ -133,7 +153,7 @@ namespace Battle const Indexes & around = Battle::Board::GetAroundIndexes( unitIdx ); for ( const int32_t cell : around ) { - const uint32_t flyingDist = static_cast( Battle::Board::GetDistance( pathStart, cell ) ); + const uint32_t flyingDist = Battle::Board::GetDistance( pathStart, cell ); if ( hexIsPassable( cell ) && ( flyingDist < unitNode._cost ) ) { unitNode._isOpen = false; unitNode._from = cell; diff --git a/src/fheroes2/battle/battle_pathfinding.h b/src/fheroes2/battle/battle_pathfinding.h index 31b1c659ca3..76bcb349de6 100644 --- a/src/fheroes2/battle/battle_pathfinding.h +++ b/src/fheroes2/battle/battle_pathfinding.h @@ -62,5 +62,9 @@ namespace Battle std::list buildPath( int targetCell ) const; bool hexIsAccessible( int targetCell ) const; bool hexIsPassable( int targetCell ) const; + Indexes getAllAvailableMoves( uint32_t moveRange ) const; + + private: + bool nodeIsAccessible( const ArenaNode & node ) const; }; } diff --git a/src/fheroes2/battle/battle_troop.cpp b/src/fheroes2/battle/battle_troop.cpp index 8c33a531f3b..379037430f2 100644 --- a/src/fheroes2/battle/battle_troop.cpp +++ b/src/fheroes2/battle/battle_troop.cpp @@ -358,7 +358,7 @@ bool Battle::Unit::canReach( int index ) const const bool isIndirectAttack = isReflect() == Board::isNegativeDistance( GetHeadIndex(), index ); const int from = ( isWide() && isIndirectAttack ) ? GetTailIndex() : GetHeadIndex(); - return static_cast( Board::GetDistance( from, index ) ) <= GetSpeed( true ); + return Board::GetDistance( from, index ) <= GetSpeed( true ); } bool Battle::Unit::canReach( const Unit & unit ) const @@ -468,6 +468,11 @@ u32 Battle::Unit::GetSpeed( bool skip_standing_check ) const return speed; } +uint32_t Battle::Unit::GetMoveRange() const +{ + return isFlying() ? ARENASIZE : GetSpeed( false ); +} + uint32_t Battle::Unit::CalculateRetaliationDamage( uint32_t damageTaken ) const { // Check if there will be retaliation in the first place diff --git a/src/fheroes2/battle/battle_troop.h b/src/fheroes2/battle/battle_troop.h index bf6a3f37a71..4d3a21995a1 100644 --- a/src/fheroes2/battle/battle_troop.h +++ b/src/fheroes2/battle/battle_troop.h @@ -123,6 +123,7 @@ namespace Battle virtual int GetColor() const override; int GetCurrentColor() const; // the unit can be under spell what changes its affiliation int GetCurrentControl() const; + uint32_t GetMoveRange() const; u32 GetSpeed( bool skip_standing_check ) const; virtual int GetControl() const override; u32 GetDamage( const Unit & ) const; From 6fd4ab46785417ef61f6ed8e838c6c3f0e9190be Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Wed, 10 Mar 2021 00:21:27 +0800 Subject: [PATCH 83/84] Fix missing cursor in Army Bar for SDL 1 (#2927) --- src/fheroes2/army/army_bar.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/fheroes2/army/army_bar.cpp b/src/fheroes2/army/army_bar.cpp index 71bda82ec68..696ce1761c8 100644 --- a/src/fheroes2/army/army_bar.cpp +++ b/src/fheroes2/army/army_bar.cpp @@ -263,7 +263,6 @@ void ArmyBar::RedrawItem( ArmyTroop & troop, const Rect & pos, bool selected, fh void ArmyBar::ResetSelected( void ) { - Cursor::Get().Hide(); spcursor.hide(); _isTroopInfoVisible = true; Interface::ItemsActionBar::ResetSelected(); @@ -465,7 +464,7 @@ bool ArmyBar::ActionBarLeftMouseDoubleClick( ArmyTroop & troop ) { if ( troop.isValid() && !read_only && IsSplitHotkeyUsed( troop, _army ) ) { ResetSelected(); - return false; + return true; } const ArmyTroop * troop2 = GetSelectedItem(); From f1b569612dd23ebfae7ae291bf0680f7d8e2de01 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Wed, 10 Mar 2021 00:21:48 +0800 Subject: [PATCH 84/84] Fix double exit dialog appearence (#2928) try to close and press Esc button to get the second dialog --- src/fheroes2/game/game_mainmenu.cpp | 3 +++ src/fheroes2/game/game_scenarioinfo.cpp | 3 +++ src/fheroes2/game/game_startgame.cpp | 3 +++ 3 files changed, 9 insertions(+) diff --git a/src/fheroes2/game/game_mainmenu.cpp b/src/fheroes2/game/game_mainmenu.cpp index ece12f8223e..d1bc9f06918 100644 --- a/src/fheroes2/game/game_mainmenu.cpp +++ b/src/fheroes2/game/game_mainmenu.cpp @@ -137,6 +137,9 @@ int Game::MainMenu( bool isFirstGameRun ) // display.Fade(); break; } + else { + continue; + } } bool redrawScreen = false; diff --git a/src/fheroes2/game/game_scenarioinfo.cpp b/src/fheroes2/game/game_scenarioinfo.cpp index 6e2f82513e8..218c5299423 100644 --- a/src/fheroes2/game/game_scenarioinfo.cpp +++ b/src/fheroes2/game/game_scenarioinfo.cpp @@ -217,6 +217,9 @@ int Game::ScenarioInfo( void ) fheroes2::FadeDisplay(); return QUITGAME; } + else { + continue; + } } // press button diff --git a/src/fheroes2/game/game_startgame.cpp b/src/fheroes2/game/game_startgame.cpp index 9c001c4b8a8..4e3b933c29d 100644 --- a/src/fheroes2/game/game_startgame.cpp +++ b/src/fheroes2/game/game_startgame.cpp @@ -701,6 +701,9 @@ int Interface::Basic::HumanTurn( bool isload ) res = Game::QUITGAME; break; } + else { + continue; + } } if ( !isOngoingFastScrollEvent )

fXBzaL<&kuVb(#W3ra}_Sv;I&aAD&RSn#}M%c5& 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