瀏覽代碼

Merge branch 'vcmi/master' into 'vcmi/develop'

Ivan Savenko 1 年之前
父節點
當前提交
df83fa33a1
共有 100 個文件被更改,包括 1902 次插入602 次删除
  1. 5 5
      .github/workflows/github.yml
  2. 0 1
      AI/Nullkiller/AIGateway.h
  3. 5 229
      AI/Nullkiller/Behaviors/ExplorationBehavior.cpp
  4. 4 0
      AI/Nullkiller/CMakeLists.txt
  5. 2 1
      AI/Nullkiller/Goals/AbstractGoal.h
  6. 8 6
      AI/Nullkiller/Goals/CGoal.h
  7. 69 0
      AI/Nullkiller/Goals/ExploreNeighbourTile.cpp
  8. 45 0
      AI/Nullkiller/Goals/ExploreNeighbourTile.h
  9. 235 0
      AI/Nullkiller/Helpers/ExplorationHelper.cpp
  10. 51 0
      AI/Nullkiller/Helpers/ExplorationHelper.h
  11. 4 3
      AI/VCAI/Goals/CGoal.h
  12. 0 1
      AI/VCAI/VCAI.h
  13. 1 0
      CI/linux-qt6/before_install.sh
  14. 1 0
      CI/linux/before_install.sh
  15. 93 0
      ChangeLog.md
  16. 11 4
      Mods/vcmi/config/vcmi/chinese.json
  17. 7 0
      Mods/vcmi/config/vcmi/polish.json
  18. 4 1
      client/CMT.cpp
  19. 3 0
      client/CMakeLists.txt
  20. 15 12
      client/CPlayerInterface.cpp
  21. 3 2
      client/CPlayerInterface.h
  22. 20 1
      client/CServerHandler.cpp
  23. 1 1
      client/CServerHandler.h
  24. 16 0
      client/Client.cpp
  25. 1 0
      client/Client.h
  26. 0 8
      client/ClientCommandManager.cpp
  27. 0 3
      client/ClientCommandManager.h
  28. 77 0
      client/ConditionalWait.h
  29. 2 2
      client/HeroMovementController.cpp
  30. 1 0
      client/NetPacksLobbyClient.cpp
  31. 2 2
      client/adventureMap/AdventureMapInterface.cpp
  32. 1 1
      client/adventureMap/AdventureMapInterface.h
  33. 78 9
      client/adventureMap/AdventureMapShortcuts.cpp
  34. 5 0
      client/adventureMap/AdventureMapShortcuts.h
  35. 1 1
      client/adventureMap/AdventureOptions.cpp
  36. 0 3
      client/adventureMap/CInGameConsole.cpp
  37. 11 7
      client/battle/BattleInterface.cpp
  38. 3 2
      client/battle/BattleInterface.h
  39. 9 3
      client/battle/BattleInterfaceClasses.cpp
  40. 0 1
      client/battle/BattleStacksController.cpp
  41. 25 5
      client/battle/BattleWindow.cpp
  42. 2 1
      client/battle/BattleWindow.h
  43. 1 10
      client/eventsSDL/InputHandler.cpp
  44. 8 1
      client/eventsSDL/InputSourceKeyboard.cpp
  45. 2 1
      client/globalLobby/GlobalLobbyInviteWindow.cpp
  46. 3 4
      client/globalLobby/GlobalLobbyLoginWindow.cpp
  47. 3 4
      client/globalLobby/GlobalLobbyRoomWindow.cpp
  48. 3 2
      client/globalLobby/GlobalLobbyServerSetup.cpp
  49. 0 1
      client/gui/CGuiHandler.cpp
  50. 0 1
      client/gui/CGuiHandler.h
  51. 4 3
      client/gui/InterfaceObjectConfigurable.cpp
  52. 1 1
      client/gui/InterfaceObjectConfigurable.h
  53. 112 23
      client/gui/Shortcut.h
  54. 139 21
      client/gui/ShortcutHandler.cpp
  55. 17 5
      client/lobby/CBonusSelection.cpp
  56. 3 3
      client/lobby/CLobbyScreen.cpp
  57. 7 6
      client/lobby/CSelectionBase.cpp
  58. 35 16
      client/lobby/SelectionTab.cpp
  59. 1 0
      client/lobby/SelectionTab.h
  60. 10 7
      client/mainmenu/CHighScoreScreen.cpp
  61. 3 0
      client/mainmenu/CHighScoreScreen.h
  62. 6 6
      client/mainmenu/CMainMenu.cpp
  63. 3 1
      client/mainmenu/CPrologEpilogVideo.cpp
  64. 2 0
      client/mainmenu/CPrologEpilogVideo.h
  65. 2 0
      client/mapView/IMapRendererObserver.h
  66. 8 8
      client/mapView/MapRenderer.cpp
  67. 2 2
      client/mapView/MapView.cpp
  68. 1 1
      client/mapView/MapView.h
  69. 2 2
      client/mapView/MapViewActions.cpp
  70. 25 5
      client/mapView/MapViewController.cpp
  71. 8 2
      client/mapView/MapViewController.h
  72. 9 4
      client/mapView/mapHandler.cpp
  73. 1 0
      client/mapView/mapHandler.h
  74. 25 15
      client/widgets/CArtPlace.cpp
  75. 1 7
      client/widgets/CArtPlace.h
  76. 1 1
      client/widgets/CArtifactsOfHeroBackpack.cpp
  77. 13 13
      client/widgets/CArtifactsOfHeroBase.cpp
  78. 1 1
      client/widgets/CArtifactsOfHeroBase.h
  79. 14 36
      client/widgets/CArtifactsOfHeroMain.cpp
  80. 24 12
      client/widgets/CArtifactsOfHeroMain.h
  81. 1 0
      client/widgets/CGarrisonInt.cpp
  82. 4 3
      client/widgets/markets/CAltarArtifacts.cpp
  83. 3 2
      client/widgets/markets/CAltarCreatures.cpp
  84. 2 1
      client/widgets/markets/CArtifactsBuying.cpp
  85. 2 1
      client/widgets/markets/CArtifactsSelling.cpp
  86. 2 1
      client/widgets/markets/CFreelancerGuild.cpp
  87. 2 1
      client/widgets/markets/CMarketBase.cpp
  88. 2 1
      client/widgets/markets/CMarketResources.cpp
  89. 2 1
      client/widgets/markets/CTransferResources.cpp
  90. 68 24
      client/windows/CCastleInterface.cpp
  91. 3 1
      client/windows/CCastleInterface.h
  92. 383 0
      client/windows/CExchangeWindow.cpp
  93. 78 0
      client/windows/CExchangeWindow.h
  94. 5 5
      client/windows/CHeroWindow.cpp
  95. 4 4
      client/windows/CKingdomInterface.cpp
  96. 9 9
      client/windows/CMarketWindow.cpp
  97. 1 1
      client/windows/CQuestLog.cpp
  98. 4 4
      client/windows/CTutorialWindow.cpp
  99. 5 2
      client/windows/CWindowWithArtifacts.cpp
  100. 1 1
      client/windows/CreaturePurchaseCard.cpp

+ 5 - 5
.github/workflows/github.yml

@@ -20,11 +20,11 @@ jobs:
       matrix:
         include:
           - platform: linux-qt6
-            os: ubuntu-22.04
+            os: ubuntu-24.04
             test: 0
             preset: linux-clang-test
           - platform: linux
-            os: ubuntu-22.04
+            os: ubuntu-24.04
             test: 1
             preset: linux-gcc-test
           - platform: linux
@@ -124,7 +124,7 @@ jobs:
       # also, running it on multiple presets is redundant and slightly increases already long CI built times
       if: ${{ startsWith(matrix.preset, 'linux-clang-test') }}
       run: |
-        pip3 install jstyleson
+        sudo apt install python3-jstyleson
         python3 CI/linux-qt6/validate_json.py
 
     - name: Dependencies
@@ -201,8 +201,8 @@ jobs:
 
     - name: Configure
       run: |
-        if [[ ${{matrix.preset}} == linux-gcc-test ]]; then GCC12=1; fi
-        cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }} ${GCC12:+-DCMAKE_C_COMPILER=gcc-12 -DCMAKE_CXX_COMPILER=g++-12}
+        if [[ ${{matrix.preset}} == linux-gcc-test ]]; then GCC14=1; fi
+        cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }} ${GCC14:+-DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14}
 
     - name: Build
       run: |

+ 0 - 1
AI/Nullkiller/AIGateway.h

@@ -21,7 +21,6 @@
 #include "../../lib/CTownHandler.h"
 #include "../../lib/mapObjects/MiscObjects.h"
 #include "../../lib/spells/CSpellHandler.h"
-#include "../../lib/CondSh.h"
 #include "Pathfinding/AIPathfinder.h"
 #include "Engine/Nullkiller.h"
 

+ 5 - 229
AI/Nullkiller/Behaviors/ExplorationBehavior.cpp

@@ -15,233 +15,15 @@
 #include "../Goals/Composition.h"
 #include "../Goals/ExecuteHeroChain.h"
 #include "../Markers/ExplorationPoint.h"
-#include "CaptureObjectsBehavior.h"
 #include "../Goals/CaptureObject.h"
-#include "../../../lib/CPlayerState.h"
+#include "../Goals/ExploreNeighbourTile.h"
+#include "../Helpers/ExplorationHelper.h"
 
 namespace NKAI
 {
 
 using namespace Goals;
 
-struct ExplorationHelper
-{
-	const CGHeroInstance * hero;
-	int sightRadius;
-	float bestValue;
-	TSubgoal bestGoal;
-	int3 bestTile;
-	int bestTilesDiscovered;
-	const Nullkiller * ai;
-	CCallback * cbp;
-	const TeamState * ts;
-	int3 ourPos;
-	bool allowDeadEndCancellation;
-
-	ExplorationHelper(const CGHeroInstance * hero, const Nullkiller * ai)
-		:ai(ai), cbp(ai->cb.get()), hero(hero)
-	{
-		ts = cbp->getPlayerTeam(ai->playerID);
-		sightRadius = hero->getSightRadius();
-		bestGoal = sptr(Goals::Invalid());
-		bestValue = 0;
-		bestTilesDiscovered = 0;
-		ourPos = hero->visitablePos();
-		allowDeadEndCancellation = true;
-	}
-
-	TSubgoal makeComposition() const
-	{
-		Composition c;
-		c.addNext(ExplorationPoint(bestTile, bestTilesDiscovered));
-		c.addNext(bestGoal);
-		return sptr(c);
-	}
-
-	void scanSector(int scanRadius)
-	{
-		int3 tile = int3(0, 0, ourPos.z);
-
-		const auto & slice = (*(ts->fogOfWarMap))[ourPos.z];
-
-		for(tile.x = ourPos.x - scanRadius; tile.x <= ourPos.x + scanRadius; tile.x++)
-		{
-			for(tile.y = ourPos.y - scanRadius; tile.y <= ourPos.y + scanRadius; tile.y++)
-			{
-
-				if(cbp->isInTheMap(tile) && slice[tile.x][tile.y])
-				{
-					scanTile(tile);
-				}
-			}
-		}
-	}
-
-	void scanMap()
-	{
-		int3 mapSize = cbp->getMapSize();
-		int perimeter = 2 * sightRadius * (mapSize.x + mapSize.y);
-
-		std::vector<int3> from;
-		std::vector<int3> to;
-
-		from.reserve(perimeter);
-		to.reserve(perimeter);
-
-		foreach_tile_pos([&](const int3 & pos)
-			{
-				if((*(ts->fogOfWarMap))[pos.z][pos.x][pos.y])
-				{
-					bool hasInvisibleNeighbor = false;
-
-					foreach_neighbour(cbp, pos, [&](CCallback * cbp, int3 neighbour)
-						{
-							if(!(*(ts->fogOfWarMap))[neighbour.z][neighbour.x][neighbour.y])
-							{
-								hasInvisibleNeighbor = true;
-							}
-						});
-
-					if(hasInvisibleNeighbor)
-						from.push_back(pos);
-				}
-			});
-
-		logAi->debug("Exploration scan visible area perimeter for hero %s", hero->getNameTranslated());
-
-		for(const int3 & tile : from)
-		{
-			scanTile(tile);
-		}
-
-		if(!bestGoal->invalid())
-		{
-			return;
-		}
-
-		allowDeadEndCancellation = false;
-
-		for(int i = 0; i < sightRadius; i++)
-		{
-			getVisibleNeighbours(from, to);
-			vstd::concatenate(from, to);
-			vstd::removeDuplicates(from);
-		}
-
-		logAi->debug("Exploration scan all possible tiles for hero %s", hero->getNameTranslated());
-
-		for(const int3 & tile : from)
-		{
-			scanTile(tile);
-		}
-	}
-
-	void scanTile(const int3 & tile)
-	{
-		if(tile == ourPos
-			|| !ai->pathfinder->isTileAccessible(hero, tile)) //shouldn't happen, but it does
-			return;
-
-		int tilesDiscovered = howManyTilesWillBeDiscovered(tile);
-		if(!tilesDiscovered)
-			return;
-
-		auto paths = ai->pathfinder->getPathInfo(tile);
-		auto waysToVisit = CaptureObjectsBehavior::getVisitGoals(paths, ai, ai->cb->getTopObj(tile));
-
-		for(int i = 0; i != paths.size(); i++)
-		{
-			auto & path = paths[i];
-			auto goal = waysToVisit[i];
-
-			if(path.exchangeCount > 1 || path.targetHero != hero || path.movementCost() <= 0.0 || goal->invalid())
-				continue;
-
-			float ourValue = (float)tilesDiscovered * tilesDiscovered / path.movementCost();
-
-			if(ourValue > bestValue) //avoid costly checks of tiles that don't reveal much
-			{
-				auto obj = cb->getTopObj(tile);
-
-				// picking up resources does not yield any exploration at all.
-				// if it blocks the way to some explorable tile AIPathfinder will take care of it
-				if(obj && obj->isBlockedVisitable())
-				{
-					continue;
-				}
-
-				if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger()))
-				{
-					bestGoal = goal;
-					bestValue = ourValue;
-					bestTile = tile;
-					bestTilesDiscovered = tilesDiscovered;
-				}
-			}
-		}
-	}
-
-	void getVisibleNeighbours(const std::vector<int3> & tiles, std::vector<int3> & out) const
-	{
-		for(const int3 & tile : tiles)
-		{
-			foreach_neighbour(cbp, tile, [&](CCallback * cbp, int3 neighbour)
-				{
-					if((*(ts->fogOfWarMap))[neighbour.z][neighbour.x][neighbour.y])
-					{
-						out.push_back(neighbour);
-					}
-				});
-		}
-	}
-
-	int howManyTilesWillBeDiscovered(const int3 & pos) const
-	{
-		int ret = 0;
-		int3 npos = int3(0, 0, pos.z);
-
-		const auto & slice = (*(ts->fogOfWarMap))[pos.z];
-
-		for(npos.x = pos.x - sightRadius; npos.x <= pos.x + sightRadius; npos.x++)
-		{
-			for(npos.y = pos.y - sightRadius; npos.y <= pos.y + sightRadius; npos.y++)
-			{
-				if(cbp->isInTheMap(npos)
-					&& pos.dist2d(npos) - 0.5 < sightRadius
-					&& !slice[npos.x][npos.y])
-				{
-					if(allowDeadEndCancellation
-						&& !hasReachableNeighbor(npos))
-					{
-						continue;
-					}
-
-					ret++;
-				}
-			}
-		}
-
-		return ret;
-	}
-
-	bool hasReachableNeighbor(const int3 & pos) const
-	{
-		for(const int3 & dir : int3::getDirs())
-		{
-			int3 tile = pos + dir;
-			if(cbp->isInTheMap(tile))
-			{
-				auto isAccessible = ai->pathfinder->isTileAccessible(hero, tile);
-
-				if(isAccessible)
-					return true;
-			}
-		}
-
-		return false;
-	}
-};
-
 std::string ExplorationBehavior::toString() const
 {
 	return "Explore";
@@ -301,17 +83,13 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const
 	{
 		ExplorationHelper scanResult(hero, ai);
 
-		scanResult.scanSector(1);
-
-		if(!scanResult.bestGoal->invalid())
+		if(scanResult.scanSector(1))
 		{
 			tasks.push_back(scanResult.makeComposition());
 			continue;
 		}
 
-		scanResult.scanSector(15);
-
-		if(!scanResult.bestGoal->invalid())
+		if(scanResult.scanSector(15))
 		{
 			tasks.push_back(scanResult.makeComposition());
 			continue;
@@ -319,9 +97,7 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const
 
 		if(ai->getScanDepth() == ScanDepth::ALL_FULL)
 		{
-			scanResult.scanMap();
-
-			if(!scanResult.bestGoal->invalid())
+			if(scanResult.scanMap())
 			{
 				tasks.push_back(scanResult.makeComposition());
 			}

+ 4 - 0
AI/Nullkiller/CMakeLists.txt

@@ -40,6 +40,7 @@ set(Nullkiller_SRCS
 		Goals/ExchangeSwapTownHeroes.cpp
 		Goals/CompleteQuest.cpp
 		Goals/StayAtTown.cpp
+		Goals/ExploreNeighbourTile.cpp
 		Markers/ArmyUpgrade.cpp
 		Markers/HeroExchange.cpp
 		Markers/UnlockCluster.cpp
@@ -62,6 +63,7 @@ set(Nullkiller_SRCS
 		Behaviors/StayAtTownBehavior.cpp
 		Behaviors/ExplorationBehavior.cpp
 		Helpers/ArmyFormation.cpp
+		Helpers/ExplorationHelper.cpp
 		AIGateway.cpp
 )
 
@@ -113,6 +115,7 @@ set(Nullkiller_HEADERS
 		Goals/CompleteQuest.h
 		Goals/Goals.h
 		Goals/StayAtTown.h
+		Goals/ExploreNeighbourTile.h
 		Markers/ArmyUpgrade.h
 		Markers/HeroExchange.h
 		Markers/UnlockCluster.h
@@ -135,6 +138,7 @@ set(Nullkiller_HEADERS
 		Behaviors/StayAtTownBehavior.h
 		Behaviors/ExplorationBehavior.h
 		Helpers/ArmyFormation.h
+		Helpers/ExplorationHelper.h
 		AIGateway.h
 )
 

+ 2 - 1
AI/Nullkiller/Goals/AbstractGoal.h

@@ -75,7 +75,8 @@ namespace Goals
 		STAY_AT_TOWN_BEHAVIOR,
 		STAY_AT_TOWN,
 		EXPLORATION_BEHAVIOR,
-		EXPLORATION_POINT
+		EXPLORATION_POINT,
+		EXPLORE_NEIGHBOUR_TILE
 	};
 
 	class DLL_EXPORT TSubgoal : public std::shared_ptr<AbstractGoal>

+ 8 - 6
AI/Nullkiller/Goals/CGoal.h

@@ -18,10 +18,11 @@ class AIGateway;
 
 namespace Goals
 {
-	template<typename T> class DLL_EXPORT CGoal : public AbstractGoal
+	template<typename T>
+	class DLL_EXPORT CGoal : public AbstractGoal
 	{
 	public:
-		CGoal<T>(EGoals goal = INVALID) : AbstractGoal(goal)
+		CGoal(EGoals goal = INVALID) : AbstractGoal(goal)
 		{
 			isAbstract = true;
 			value = 0;
@@ -32,7 +33,7 @@ namespace Goals
 			town = nullptr;
 		}
 
-		CGoal<T> * clone() const override
+		CGoal * clone() const override
 		{
 			return new T(static_cast<T const &>(*this)); //casting enforces template instantiation
 		}
@@ -64,15 +65,16 @@ namespace Goals
 		}
 	};
 
-	template<typename T> class DLL_EXPORT ElementarGoal : public CGoal<T>, public ITask
+	template<typename T>
+	class DLL_EXPORT ElementarGoal : public CGoal<T>, public ITask
 	{
 	public:
-		ElementarGoal<T>(EGoals goal = INVALID) : CGoal<T>(goal), ITask()
+		ElementarGoal(EGoals goal = INVALID) : CGoal<T>(goal), ITask()
 		{
 			AbstractGoal::isAbstract = false;
 		}
 
-		ElementarGoal<T>(const ElementarGoal<T> & other) : CGoal<T>(other), ITask(other)
+		ElementarGoal(const ElementarGoal<T> & other) : CGoal<T>(other), ITask(other)
 		{
 		}
 

+ 69 - 0
AI/Nullkiller/Goals/ExploreNeighbourTile.cpp

@@ -0,0 +1,69 @@
+/*
+* ExploreNeighbourTile.cpp, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+#include "StdInc.h"
+#include "ExploreNeighbourTile.h"
+#include "../AIGateway.h"
+#include "../AIUtility.h"
+#include "../Helpers/ExplorationHelper.h"
+
+
+namespace NKAI
+{
+
+using namespace Goals;
+
+bool ExploreNeighbourTile::operator==(const ExploreNeighbourTile & other) const
+{
+	return false;
+}
+
+void ExploreNeighbourTile::accept(AIGateway * ai)
+{
+	ExplorationHelper h(hero, ai->nullkiller.get());
+
+	for(int i = 0; i < tilesToExplore && hero->movementPointsRemaining() > 0; i++)
+	{
+		int3 pos = hero->visitablePos();
+		float value = 0;
+		int3 target = int3(-1);
+		foreach_neighbour(pos, [&](int3 tile)
+			{
+				auto pathInfo = ai->myCb->getPathsInfo(hero)->getPathInfo(tile);
+
+				if(pathInfo->turns > 0)
+					return;
+
+				if(pathInfo->accessible == EPathAccessibility::ACCESSIBLE)
+				{
+					float newValue = h.howManyTilesWillBeDiscovered(tile);
+
+					newValue /= std::min(0.1f, pathInfo->getCost());
+
+					if(newValue > value)
+					{
+						value = newValue;
+						target = tile;
+					}
+				}
+			});
+
+		if(!target.valid() || !ai->moveHeroToTile(target, hero))
+		{
+			return;
+		}
+	}
+}
+
+std::string ExploreNeighbourTile::toString() const
+{
+	return "Explore neighbour tiles by " + hero->getNameTranslated();
+}
+
+}

+ 45 - 0
AI/Nullkiller/Goals/ExploreNeighbourTile.h

@@ -0,0 +1,45 @@
+/*
+* ExploreNeighbourTile.h, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+#pragma once
+
+#include "CGoal.h"
+
+namespace NKAI
+{
+
+class AIGateway;
+class FuzzyHelper;
+
+namespace Goals
+{
+	class DLL_EXPORT ExploreNeighbourTile : public ElementarGoal<ExploreNeighbourTile>
+	{
+	private:
+		int tilesToExplore;
+
+	public:
+		ExploreNeighbourTile(const CGHeroInstance * hero,  int amount)
+			: ElementarGoal(Goals::EXPLORE_NEIGHBOUR_TILE)
+		{
+			tilesToExplore = amount;
+			sethero(hero);
+		}
+
+		bool operator==(const ExploreNeighbourTile & other) const override;
+
+		void accept(AIGateway * ai) override;
+		std::string toString() const override;
+
+	private:
+		//TSubgoal decomposeSingle() const override;
+	};
+}
+
+}

+ 235 - 0
AI/Nullkiller/Helpers/ExplorationHelper.cpp

@@ -0,0 +1,235 @@
+/*
+* ExplorationHelper.cpp, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+#include "StdInc.h"
+#include "ExplorationHelper.h"
+#include "../../../lib/mapObjects/CGTownInstance.h"
+#include "../Engine/Nullkiller.h"
+#include "../Goals/Invalid.h"
+#include "../Goals/Composition.h"
+#include "../Goals/ExecuteHeroChain.h"
+#include "../Markers/ExplorationPoint.h"
+#include "../../../lib/CPlayerState.h"
+#include "../Behaviors/CaptureObjectsBehavior.h"
+#include "../Goals/ExploreNeighbourTile.h"
+
+namespace NKAI
+{
+
+using namespace Goals;
+
+ExplorationHelper::ExplorationHelper(const CGHeroInstance * hero, const Nullkiller * ai)
+	:ai(ai), cbp(ai->cb.get()), hero(hero)
+{
+	ts = cbp->getPlayerTeam(ai->playerID);
+	sightRadius = hero->getSightRadius();
+	bestGoal = sptr(Goals::Invalid());
+	bestValue = 0;
+	bestTilesDiscovered = 0;
+	ourPos = hero->visitablePos();
+	allowDeadEndCancellation = true;
+}
+
+TSubgoal ExplorationHelper::makeComposition() const
+{
+	Composition c;
+	c.addNext(ExplorationPoint(bestTile, bestTilesDiscovered));
+	c.addNextSequence({bestGoal, sptr(ExploreNeighbourTile(hero, 5))});
+	return sptr(c);
+}
+
+
+bool ExplorationHelper::scanSector(int scanRadius)
+{
+	int3 tile = int3(0, 0, ourPos.z);
+
+	const auto & slice = ts->fogOfWarMap[ourPos.z];
+
+	for(tile.x = ourPos.x - scanRadius; tile.x <= ourPos.x + scanRadius; tile.x++)
+	{
+		for(tile.y = ourPos.y - scanRadius; tile.y <= ourPos.y + scanRadius; tile.y++)
+		{
+			if(cbp->isInTheMap(tile) && slice[tile.x][tile.y])
+			{
+				scanTile(tile);
+			}
+		}
+	}
+
+	return !bestGoal->invalid();
+}
+
+bool ExplorationHelper::scanMap()
+{
+	int3 mapSize = cbp->getMapSize();
+	int perimeter = 2 * sightRadius * (mapSize.x + mapSize.y);
+
+	std::vector<int3> from;
+	std::vector<int3> to;
+
+	from.reserve(perimeter);
+	to.reserve(perimeter);
+
+	foreach_tile_pos([&](const int3 & pos)
+		{
+			if(ts->fogOfWarMap[pos.z][pos.x][pos.y])
+			{
+				bool hasInvisibleNeighbor = false;
+
+				foreach_neighbour(cbp, pos, [&](CCallback * cbp, int3 neighbour)
+					{
+						if(!ts->fogOfWarMap[neighbour.z][neighbour.x][neighbour.y])
+						{
+							hasInvisibleNeighbor = true;
+						}
+					});
+
+				if(hasInvisibleNeighbor)
+					from.push_back(pos);
+			}
+		});
+
+	logAi->debug("Exploration scan visible area perimeter for hero %s", hero->getNameTranslated());
+
+	for(const int3 & tile : from)
+	{
+		scanTile(tile);
+	}
+
+	if(!bestGoal->invalid())
+	{
+		return false;
+	}
+
+	allowDeadEndCancellation = false;
+
+	for(int i = 0; i < sightRadius; i++)
+	{
+		getVisibleNeighbours(from, to);
+		vstd::concatenate(from, to);
+		vstd::removeDuplicates(from);
+	}
+
+	logAi->debug("Exploration scan all possible tiles for hero %s", hero->getNameTranslated());
+
+	for(const int3 & tile : from)
+	{
+		scanTile(tile);
+	}
+
+	return !bestGoal->invalid();
+}
+
+void ExplorationHelper::scanTile(const int3 & tile)
+{
+	if(tile == ourPos
+		|| !ai->cb->getTile(tile, false)
+		|| !ai->pathfinder->isTileAccessible(hero, tile)) //shouldn't happen, but it does
+		return;
+
+	int tilesDiscovered = howManyTilesWillBeDiscovered(tile);
+	if(!tilesDiscovered)
+		return;
+	
+	auto paths = ai->pathfinder->getPathInfo(tile);
+	auto waysToVisit = CaptureObjectsBehavior::getVisitGoals(paths, ai, ai->cb->getTopObj(tile));
+
+	for(int i = 0; i != paths.size(); i++)
+	{
+		auto & path = paths[i];
+		auto goal = waysToVisit[i];
+
+		if(path.exchangeCount > 1 || path.targetHero != hero || path.movementCost() <= 0.0 || goal->invalid())
+			continue;
+
+		float ourValue = (float)tilesDiscovered * tilesDiscovered / path.movementCost();
+
+		if(ourValue > bestValue) //avoid costly checks of tiles that don't reveal much
+		{
+			auto obj = cb->getTopObj(tile);
+
+			// picking up resources does not yield any exploration at all.
+			// if it blocks the way to some explorable tile AIPathfinder will take care of it
+			if(obj && obj->isBlockedVisitable())
+			{
+				continue;
+			}
+
+			if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger()))
+			{
+				bestGoal = goal;
+				bestValue = ourValue;
+				bestTile = tile;
+				bestTilesDiscovered = tilesDiscovered;
+			}
+		}
+	}
+}
+
+void ExplorationHelper::getVisibleNeighbours(const std::vector<int3> & tiles, std::vector<int3> & out) const
+{
+	for(const int3 & tile : tiles)
+	{
+		foreach_neighbour(cbp, tile, [&](CCallback * cbp, int3 neighbour)
+			{
+				if(ts->fogOfWarMap[neighbour.z][neighbour.x][neighbour.y])
+				{
+					out.push_back(neighbour);
+				}
+			});
+	}
+}
+
+int ExplorationHelper::howManyTilesWillBeDiscovered(const int3 & pos) const
+{
+	int ret = 0;
+	int3 npos = int3(0, 0, pos.z);
+
+	const auto & slice = ts->fogOfWarMap[pos.z];
+
+	for(npos.x = pos.x - sightRadius; npos.x <= pos.x + sightRadius; npos.x++)
+	{
+		for(npos.y = pos.y - sightRadius; npos.y <= pos.y + sightRadius; npos.y++)
+		{
+			if(cbp->isInTheMap(npos)
+				&& pos.dist2d(npos) - 0.5 < sightRadius
+				&& !slice[npos.x][npos.y])
+			{
+				if(allowDeadEndCancellation
+					&& !hasReachableNeighbor(npos))
+				{
+					continue;
+				}
+
+				ret++;
+			}
+		}
+	}
+
+	return ret;
+}
+
+bool ExplorationHelper::hasReachableNeighbor(const int3 & pos) const
+{
+	for(const int3 & dir : int3::getDirs())
+	{
+		int3 tile = pos + dir;
+		if(cbp->isInTheMap(tile))
+		{
+			auto isAccessible = ai->pathfinder->isTileAccessible(hero, tile);
+
+			if(isAccessible)
+				return true;
+		}
+	}
+
+	return false;
+}
+
+}

+ 51 - 0
AI/Nullkiller/Helpers/ExplorationHelper.h

@@ -0,0 +1,51 @@
+/*
+* ExplorationHelper.h, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+#pragma once
+
+#include "../AIUtility.h"
+
+#include "../../../lib/GameConstants.h"
+#include "../../../lib/VCMI_Lib.h"
+#include "../../../lib/CTownHandler.h"
+#include "../../../lib/CBuildingHandler.h"
+#include "../Goals/AbstractGoal.h"
+
+namespace NKAI
+{
+
+class ExplorationHelper
+{
+private:
+	const CGHeroInstance * hero;
+	int sightRadius;
+	float bestValue;
+	Goals::TSubgoal bestGoal;
+	int3 bestTile;
+	int bestTilesDiscovered;
+	const Nullkiller * ai;
+	CCallback * cbp;
+	const TeamState * ts;
+	int3 ourPos;
+	bool allowDeadEndCancellation;
+
+public:
+	ExplorationHelper(const CGHeroInstance * hero, const Nullkiller * ai);
+	Goals::TSubgoal makeComposition() const;
+	bool scanSector(int scanRadius);
+	bool scanMap();
+	int howManyTilesWillBeDiscovered(const int3 & pos) const;
+
+private:
+	void scanTile(const int3 & tile);
+	bool hasReachableNeighbor(const int3 & pos) const;
+	void getVisibleNeighbours(const std::vector<int3> & tiles, std::vector<int3> & out) const;
+};
+
+}

+ 4 - 3
AI/VCAI/Goals/CGoal.h

@@ -18,10 +18,11 @@ class VCAI;
 
 namespace Goals
 {
-	template<typename T> class DLL_EXPORT CGoal : public AbstractGoal
+	template<typename T>
+	class DLL_EXPORT CGoal : public AbstractGoal
 	{
 	public:
-		CGoal<T>(EGoals goal = INVALID) : AbstractGoal(goal)
+		CGoal(EGoals goal = INVALID) : AbstractGoal(goal)
 		{
 			priority = 0;
 			isElementar = false;
@@ -56,7 +57,7 @@ namespace Goals
 			return f->evaluate(static_cast<T &>(*this)); //casting enforces template instantiation
 		}
 
-		CGoal<T> * clone() const override
+		CGoal * clone() const override
 		{
 			return new T(static_cast<T const &>(*this)); //casting enforces template instantiation
 		}

+ 0 - 1
AI/VCAI/VCAI.h

@@ -23,7 +23,6 @@
 #include "../../lib/CTownHandler.h"
 #include "../../lib/mapObjects/MiscObjects.h"
 #include "../../lib/spells/CSpellHandler.h"
-#include "../../lib/CondSh.h"
 #include "Pathfinding/AIPathfinder.h"
 
 VCMI_LIB_NAMESPACE_BEGIN

+ 1 - 0
CI/linux-qt6/before_install.sh

@@ -1,5 +1,6 @@
 #!/bin/sh
 
+sudo apt remove needrestart
 sudo apt-get update
 
 # Dependencies

+ 1 - 0
CI/linux/before_install.sh

@@ -1,5 +1,6 @@
 #!/bin/sh
 
+sudo apt remove needrestart
 sudo apt-get update
 
 # Dependencies

+ 93 - 0
ChangeLog.md

@@ -1,3 +1,96 @@
+# 1.5.1 -> 1.5.2
+
+### Stability
+* Fixed crash on closing game while combat or map animations are playing
+* Fixed crash on closing game while network thread is waiting for dialog to be closed
+* Fixed random crash on starting random map with 'random' number of players
+* Fixed crash caused by thread races on loading map list
+* Failure to read data from network connection will show up as 'disconnection' and not as a crash
+* Fixed a possible crash when replaying a manually played battle with the 'unlimited battle replay' option set
+* Fixed crash when loading save made on a 64-bit system or connecting to multiplayer game with a 64-bit host on a 32-bit system (and vice versa)
+* Fixed crash when ending a battle in a draw when a hero has the Necromancy skill
+* Fixed crash when having SPELL_LIKE_ATTACK bonus with invalid spell ID
+* Fixed transfer of non-first artefacts in backpack if hero does not transfer as well
+* Game will now abort loading if a corrupt mod is detected instead of crashing without explanation later
+
+### Multiplayer
+* Contact between allied players will no longer break simturns
+* Having hero in range of object owned by another player will now be registered as contact
+* Multiplayer saves are now visible when starting a single player game
+* Added chat command '!vote' to initiate a vote to change the duration of simultaneous turns or to change turn timers
+* Added chat command '!help' to list all available chat commands
+* All multiplayer chat commands now use a leading exclamation mark
+
+### Campaigns
+* If the hero attacks an enemy player and is defeated, he will be correctly registered as defeated by the defending player.
+* Allow standard victory condition on 'To kill a hero' campaign mission in line with H3
+* Fixes Adrienne starting without Inferno spell in campaign
+
+### Interface
+* For artefacts that are part of a combined artefact, the game will now show which component of that artefact your hero has.
+* Fixed broken in 1.5.1 shortcut for artifact sets saving
+* Fixed full screen toggle (F4) not applying changes immediately
+* Retaliation preview now accounts for creatures that don't receive retaliations (Sprites, Archdevils, etc)
+* Fixed not visible retaliation preview if damage estimation string is longer than battle log line due to long creature name
+* Game will now select last save on loading screen
+* High Scores screen and Campaign Epilogue screen are now displayed with background on resolutions higher than 800x600
+* Fixed non-functioning shortcut 'P' to access Puzzle Map from adventure map
+* Added keyboard shortcuts to markets and altars. 'Space' to confirm deal and 'M' to trade maximum possible amount
+* Pressing 'Escape' in main menu will now trigger 'Back' and 'Quit' buttons
+* Added keyboard shortcuts to hero exchange window:
+* * 'F10' will now swap armies
+* * 'F11' will now swap artifacts. Additionally, 'Ctrl+F11' will swap equipped artifacts, and 'Shift+F11' will swap backpacks
+* * Added unassigned shortcuts to move armies or artifacts to left or right side
+* Added keyboard shortcuts to access buildings from town interface:
+* * 'F' will now open Fort window
+* * 'B' will now open Town Hall window
+* * 'G' will now open Mage Guild window
+* * 'M' will now open Marketplace
+* * 'R' will now open recruitment interface
+* * 'T' will now open Tavern window
+* * 'G' will now open Thieves Guild
+* * 'E' will now open hero exchange screen, if both heroes are present in town
+* * 'H' will now open hero screen. Additionally, 'Shift+H' will open garrisoned hero screen, and 'Ctrl+H' will open visiting hero screen
+* * 'Space' will now swap visiting and garrisoned heroes
+* Added keyboard shortcuts to switch between tabs in Scenario Selection window:
+* * 'E' will open Extra Options tab
+* * 'T' will open Turn Options tab
+* * 'I' will open Invite Players window (only for lobby games)
+* * 'R' will now replay video in campaigns
+* Added keyboard shortcuts to Adventure map:
+* * 'Ctrl+L' will now prompt to open Load Game screen
+* * 'Ctrl+M' will now prompt to go to main menu
+* * 'Ctrl+N' will now prompt to go to New Game screen
+* * 'Ctrl+Q' will now prompt to quit game
+* * Page Up, Page Down, Home and End keys will now move hero on adventure map similar to numpad equivalents
+* * Fixed non-functioning shortcuts '+' and '-' on numpad to zoom adventure map
+* Added keyboard shortcuts to Battle interface:
+* * 'V' now allows to view information of hovered unit
+* * 'I' now allows to view information of active unit
+
+### Mechanics
+* Game will no longer pick creatures exclusive to AB campaigns for random creatures or for Refugee Camp, in line with H3
+* If original movement rules are on, it is not possible to attack guards from visitable object directly, only from free tile
+* Fixed bug leading that allowed picking up objects while flying on top of water
+* Hero can now land when flying from guarded tile to accessible guarded tile irregardless of original movement rules switch
+* Interface will now use same arrow for U-turns in path as H3
+
+### AI
+* Nullkiller AI can now explore the map
+* Nullkiller AI will no longer use the map reveal cheat when allied with a human or when playing on low difficulty
+* Nullkiller AI is now used by default for allied players
+
+### Launcher
+* When extracting data from gog.com offline installer game will extract files directly into used data directory instead of temporary directory
+
+### Map Editor
+* Fixed victory / loss conditions widget initialization
+
+### Modding
+* Hero specialties with multiple bonuses that have TIMES_HERO_LEVEL updater now work as expected
+* Spells that apply multiple bonuses with same type and subtype but different value type now work as expected
+* Added option to toggle layout of guards in creature banks
+
 # 1.5.0 -> 1.5.1
 
 ### Stability

+ 11 - 4
Mods/vcmi/config/vcmi/chinese.json

@@ -242,16 +242,23 @@
 	"vcmi.adventureMap.revisitObject.help" : "{重新访问}\n\n让当前英雄重新访问地图建筑或城镇。",
 
 	"vcmi.battleWindow.pressKeyToSkipIntro" : "按下任意键立即开始战斗",
-	"vcmi.battleWindow.damageEstimation.melee" : "近战攻击 %CREATURE (%DAMAGE).",
-	"vcmi.battleWindow.damageEstimation.meleeKills" : "近战攻击 %CREATURE (%DAMAGE, %KILLS).",
-	"vcmi.battleWindow.damageEstimation.ranged" : "射击 %CREATURE (%SHOTS, %DAMAGE).",
-	"vcmi.battleWindow.damageEstimation.rangedKills" : "射击 %CREATURE (%SHOTS, %DAMAGE, %KILLS).",
+	"vcmi.battleWindow.damageEstimation.melee" : "近战攻击 %CREATURE (%DAMAGE)",
+	"vcmi.battleWindow.damageEstimation.meleeKills" : "近战攻击 %CREATURE (%DAMAGE, %KILLS)",
+	"vcmi.battleWindow.damageEstimation.ranged" : "射击 %CREATURE (%SHOTS, %DAMAGE)",
+	"vcmi.battleWindow.damageEstimation.rangedKills" : "射击 %CREATURE (%SHOTS, %DAMAGE, %KILLS)",
 	"vcmi.battleWindow.damageEstimation.shots" : "%d 弹药剩余",
 	"vcmi.battleWindow.damageEstimation.shots.1" : "%d 弹药剩余",
 	"vcmi.battleWindow.damageEstimation.damage" : "%d 伤害",
 	"vcmi.battleWindow.damageEstimation.damage.1" : "%d 伤害",
 	"vcmi.battleWindow.damageEstimation.kills" : "%d 将被消灭",
 	"vcmi.battleWindow.damageEstimation.kills.1" : "%d 将被消灭",
+
+	"vcmi.battleWindow.damageRetaliation.will" : "将会反击",
+	"vcmi.battleWindow.damageRetaliation.may" : "可能反击",
+	"vcmi.battleWindow.damageRetaliation.never" : "不会反击。",
+	"vcmi.battleWindow.damageRetaliation.damage" : "(%DAMAGE)。",
+	"vcmi.battleWindow.damageRetaliation.damageKills" : "(%DAMAGE, %KILLS)。",
+
 	"vcmi.battleWindow.killed" : "已消灭",
 	"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s 死于精准射击",
 	"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s 死于精准射击",

+ 7 - 0
Mods/vcmi/config/vcmi/polish.json

@@ -252,6 +252,13 @@
 	"vcmi.battleWindow.damageEstimation.damage.1" : "obrażenia: %d",
 	"vcmi.battleWindow.damageEstimation.kills" : "zginie: %d",
 	"vcmi.battleWindow.damageEstimation.kills.1" : "zginie: %d",
+
+	"vcmi.battleWindow.damageRetaliation.will" : "Nastąpi odwet ",
+	"vcmi.battleWindow.damageRetaliation.may" : "Możliwy odwet ",
+	"vcmi.battleWindow.damageRetaliation.never" : "Nie będzie odwetu.",
+	"vcmi.battleWindow.damageRetaliation.damage" : "(%DAMAGE).",
+	"vcmi.battleWindow.damageRetaliation.damageKills" : "(%DAMAGE, %KILLS).",
+
 	"vcmi.battleWindow.killed" : "Zabici",
 	"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s zostało zabitych poprzez celne strzały!",
 	"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s został zabity poprzez celny strzał!",

+ 4 - 1
client/CMT.cpp

@@ -435,6 +435,8 @@ static void mainLoop()
 
 [[noreturn]] static void quitApplication()
 {
+	CSH->endNetwork();
+
 	if(!settings["session"]["headless"].Bool())
 	{
 		if(CSH->client)
@@ -443,6 +445,8 @@ static void mainLoop()
 		GH.windows().clear();
 	}
 
+	vstd::clear_pointer(CSH);
+
 	CMM.reset();
 
 	if(!settings["session"]["headless"].Bool())
@@ -463,7 +467,6 @@ static void mainLoop()
 		vstd::clear_pointer(graphics);
 	}
 
-	vstd::clear_pointer(CSH);
 	vstd::clear_pointer(VLC);
 
 	// sometimes leads to a hang. TODO: investigate

+ 3 - 0
client/CMakeLists.txt

@@ -144,6 +144,7 @@ set(client_SRCS
 
 	windows/CCastleInterface.cpp
 	windows/CCreatureWindow.cpp
+	windows/CExchangeWindow.cpp
 	windows/CHeroOverview.cpp
 	windows/CHeroWindow.cpp
 	windows/CKingdomInterface.cpp
@@ -346,6 +347,7 @@ set(client_HEADERS
 
 	windows/CCastleInterface.h
 	windows/CCreatureWindow.h
+	windows/CExchangeWindow.h
 	windows/CHeroOverview.h
 	windows/CHeroWindow.h
 	windows/CKingdomInterface.h
@@ -377,6 +379,7 @@ set(client_HEADERS
 	Client.h
 	ClientCommandManager.h
 	ClientNetPackVisitors.h
+	ConditionalWait.h
 	HeroMovementController.h
 	GameChatHandler.h
 	LobbyClientNetPackVisitors.h

+ 15 - 12
client/CPlayerInterface.cpp

@@ -53,6 +53,7 @@
 
 #include "windows/CCastleInterface.h"
 #include "windows/CCreatureWindow.h"
+#include "windows/CExchangeWindow.h"
 #include "windows/CHeroWindow.h"
 #include "windows/CKingdomInterface.h"
 #include "windows/CMarketWindow.h"
@@ -74,7 +75,6 @@
 #include "../lib/CStopWatch.h"
 #include "../lib/CThreadHelper.h"
 #include "../lib/CTownHandler.h"
-#include "../lib/CondSh.h"
 #include "../lib/GameConstants.h"
 #include "../lib/RoadHandler.h"
 #include "../lib/StartInfo.h"
@@ -139,10 +139,10 @@ CPlayerInterface::CPlayerInterface(PlayerColor Player):
 	LOCPLINT = this;
 	playerID=Player;
 	human=true;
-	battleInt = nullptr;
+	battleInt.reset();
 	castleInt = nullptr;
 	makingTurn = false;
-	showingDialog = new CondSh<bool>(false);
+	showingDialog = new ConditionalWait();
 	cingconsole = new CInGameConsole();
 	autosaveCount = 0;
 	isAutoFightOn = false;
@@ -1006,7 +1006,7 @@ void CPlayerInterface::showInfoDialog(const std::string &text, const std::vector
 	if (makingTurn && GH.windows().count() > 0 && LOCPLINT == this)
 	{
 		CCS->soundh->playSound(static_cast<soundBase::soundID>(soundID));
-		showingDialog->set(true);
+		showingDialog->setBusy();
 		movementController->requestMovementAbort(); // interrupt movement to show dialog
 		GH.windows().pushWindow(temp);
 	}
@@ -1029,7 +1029,7 @@ void CPlayerInterface::showInfoDialogAndWait(std::vector<Component> & components
 void CPlayerInterface::showYesNoDialog(const std::string &text, CFunctionList<void()> onYes, CFunctionList<void()> onNo, const std::vector<std::shared_ptr<CComponent>> & components)
 {
 	movementController->requestMovementAbort();
-	LOCPLINT->showingDialog->setn(true);
+	LOCPLINT->showingDialog->setBusy();
 	CInfoWindow::showYesNoDialog(text, components, onYes, onNo, playerID);
 }
 
@@ -1205,7 +1205,7 @@ void CPlayerInterface::heroBonusChanged( const CGHeroInstance *hero, const Bonus
 void CPlayerInterface::moveHero( const CGHeroInstance *h, const CGPath& path )
 {
 	assert(h);
-	assert(!showingDialog->get());
+	assert(!showingDialog->isBusy());
 	assert(dialogs.empty());
 
 	LOG_TRACE(logGlobal);
@@ -1215,7 +1215,7 @@ void CPlayerInterface::moveHero( const CGHeroInstance *h, const CGPath& path )
 		return; //can't find hero
 
 	//It shouldn't be possible to move hero with open dialog (or dialog waiting in bg)
-	if (showingDialog->get() || !dialogs.empty())
+	if (showingDialog->isBusy() || !dialogs.empty())
 		return;
 
 	if (localState->isHeroSleeping(h))
@@ -1383,9 +1383,7 @@ void CPlayerInterface::waitWhileDialog()
 	}
 
 	auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex);
-	boost::unique_lock<boost::mutex> un(showingDialog->mx);
-	while(showingDialog->data)
-		showingDialog->cond.wait(un);
+	showingDialog->waitWhileBusy();
 }
 
 void CPlayerInterface::showShipyardDialog(const IShipyard *obj)
@@ -1490,9 +1488,9 @@ void CPlayerInterface::update()
 		return;
 
 	//if there are any waiting dialogs, show them
-	if ((CSH->howManyPlayerInterfaces() <= 1 || makingTurn) && !dialogs.empty() && !showingDialog->get())
+	if ((CSH->howManyPlayerInterfaces() <= 1 || makingTurn) && !dialogs.empty() && !showingDialog->isBusy())
 	{
-		showingDialog->set(true);
+		showingDialog->setBusy();
 		GH.windows().pushWindow(dialogs.front());
 		dialogs.pop_front();
 	}
@@ -1504,6 +1502,11 @@ void CPlayerInterface::update()
 	GH.windows().simpleRedraw();
 }
 
+void CPlayerInterface::endNetwork()
+{
+	showingDialog->requestTermination();
+}
+
 int CPlayerInterface::getLastIndex( std::string namePrefix)
 {
 	using namespace boost::filesystem;

+ 3 - 2
client/CPlayerInterface.h

@@ -25,7 +25,7 @@ struct CGPath;
 class CCreatureSet;
 class CGObjectInstance;
 struct UpgradeInfo;
-template <typename T> struct CondSh;
+class ConditionalWait;
 struct CPathsInfo;
 
 VCMI_LIB_NAMESPACE_END
@@ -72,7 +72,7 @@ public: // TODO: make private
 	std::unique_ptr<PlayerLocalState> localState;
 
 	//minor interfaces
-	CondSh<bool> *showingDialog; //indicates if dialog box is displayed
+	ConditionalWait * showingDialog; //indicates if dialog box is displayed
 
 	bool makingTurn; //if player is already making his turn
 
@@ -198,6 +198,7 @@ public: // public interface for use by client via LOCPLINT access
 	void proposeLoadingGame();
 	void performAutosave();
 	void gamePause(bool pause);
+	void endNetwork();
 
 	///returns true if all events are processed internally
 	bool capturedAllEvents();

+ 20 - 1
client/CServerHandler.cpp

@@ -29,6 +29,7 @@
 
 #include "../lib/CConfigHandler.h"
 #include "../lib/CGeneralTextHandler.h"
+#include "ConditionalWait.h"
 #include "../lib/CThreadHelper.h"
 #include "../lib/StartInfo.h"
 #include "../lib/TurnTimerInfo.h"
@@ -131,6 +132,17 @@ CServerHandler::~CServerHandler()
 	}
 }
 
+void CServerHandler::endNetwork()
+{
+	if (client)
+		client->endNetwork();
+	networkHandler->stop();
+	{
+		auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex);
+		threadNetwork.join();
+	}
+}
+
 CServerHandler::CServerHandler()
 	: networkHandler(INetworkHandler::createHandler())
 	, lobbyClient(std::make_unique<GlobalLobbyClient>())
@@ -158,7 +170,14 @@ void CServerHandler::threadRunNetwork()
 {
 	logGlobal->info("Starting network thread");
 	setThreadName("runNetwork");
-	networkHandler->run();
+	try {
+		networkHandler->run();
+	}
+	catch (const TerminationRequestedException & e)
+	{
+		logGlobal->info("Terminating network thread");
+		return;
+	}
 	logGlobal->info("Ending network thread");
 }
 

+ 1 - 1
client/CServerHandler.h

@@ -13,7 +13,6 @@
 
 #include "../lib/network/NetworkInterface.h"
 #include "../lib/StartInfo.h"
-#include "../lib/CondSh.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -208,6 +207,7 @@ public:
 
 	void startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameState = nullptr);
 	void showHighScoresAndEndGameplay(PlayerColor player, bool victory);
+	void endNetwork();
 	void endGameplay();
 	void restartGameplay();
 	void startCampaignScenario(HighScoreParameter param, std::shared_ptr<CampaignState> cs = {});

+ 16 - 0
client/Client.cpp

@@ -218,6 +218,22 @@ void CClient::save(const std::string & fname)
 	sendRequest(&save_game, PlayerColor::NEUTRAL);
 }
 
+void CClient::endNetwork()
+{
+	if (CGI->mh)
+		CGI->mh->endNetwork();
+
+	if (CPlayerInterface::battleInt)
+		CPlayerInterface::battleInt->endNetwork();
+
+	for(auto & i : playerint)
+	{
+		auto interface = std::dynamic_pointer_cast<CPlayerInterface>(i.second);
+		if (interface)
+			interface->endNetwork();
+	}
+}
+
 void CClient::endGame()
 {
 #if SCRIPTING_ENABLED

+ 1 - 0
client/Client.h

@@ -133,6 +133,7 @@ public:
 	void loadGame(CGameState * gameState);
 
 	void save(const std::string & fname);
+	void endNetwork();
 	void endGame();
 
 	void initMapHandler();

+ 0 - 8
client/ClientCommandManager.cpp

@@ -179,11 +179,6 @@ void ClientCommandManager::handleRedrawCommand()
 	GH.windows().totalRedraw();
 }
 
-void ClientCommandManager::handleNotDialogCommand()
-{
-	LOCPLINT->showingDialog->setn(false);
-}
-
 void ClientCommandManager::handleTranslateGameCommand()
 {
 	std::map<std::string, std::map<std::string, std::string>> textsByMod;
@@ -584,9 +579,6 @@ void ClientCommandManager::processCommand(const std::string & message, bool call
 	else if(commandName == "redraw")
 		handleRedrawCommand();
 
-	else if(commandName == "not dialog")
-		handleNotDialogCommand();
-
 	else if(message=="translate" || message=="translate game")
 		handleTranslateGameCommand();
 

+ 0 - 3
client/ClientCommandManager.h

@@ -45,9 +45,6 @@ class ClientCommandManager //take mantis #2292 issue about account if thinking a
 	// Redraw the current screen
 	void handleRedrawCommand();
 
-	// Set the state indicating if dialog box is active to "no"
-	void handleNotDialogCommand();
-
 	// Extracts all translateable game texts into Translation directory, separating files on per-mod basis
 	void handleTranslateGameCommand();
 

+ 77 - 0
client/ConditionalWait.h

@@ -0,0 +1,77 @@
+/*
+ * ConditionalWait.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include <condition_variable>
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class TerminationRequestedException : public std::exception
+{
+public:
+	using exception::exception;
+
+	const char* what() const noexcept override
+	{
+		return "Thread termination requested";
+	}
+};
+
+class ConditionalWait
+{
+	bool isBusyValue = false;
+	bool isTerminating = false;
+	std::condition_variable cond;
+	std::mutex mx;
+
+	void set(bool value)
+	{
+		boost::unique_lock<std::mutex> lock(mx);
+		isBusyValue = value;
+	}
+
+public:
+	ConditionalWait() = default;
+
+	void setBusy()
+	{
+		set(true);
+	}
+
+	void setFree()
+	{
+		set(false);
+		cond.notify_all();
+	}
+
+	void requestTermination()
+	{
+		isTerminating = true;
+		setFree();
+	}
+
+	bool isBusy()
+	{
+		std::unique_lock<std::mutex> lock(mx);
+		return isBusyValue;
+	}
+
+	void waitWhileBusy()
+	{
+		std::unique_lock<std::mutex> un(mx);
+		while(isBusyValue)
+			cond.wait(un);
+
+		if (isTerminating)
+			throw TerminationRequestedException();
+	}
+};
+
+VCMI_LIB_NAMESPACE_END

+ 2 - 2
client/HeroMovementController.cpp

@@ -22,7 +22,7 @@
 
 #include "../CCallback.h"
 
-#include "../lib/CondSh.h"
+#include "ConditionalWait.h"
 #include "../lib/CConfigHandler.h"
 #include "../lib/pathfinder/CGPathNode.h"
 #include "../lib/mapObjects/CGHeroInstance.h"
@@ -238,7 +238,7 @@ void HeroMovementController::onMoveHeroApplied()
 	assert(currentlyMovingHero);
 	const auto * hero = currentlyMovingHero;
 
-	bool canMove = LOCPLINT->localState->hasPath(hero) && LOCPLINT->localState->getPath(hero).nextNode().turns == 0 && !LOCPLINT->showingDialog->get();
+	bool canMove = LOCPLINT->localState->hasPath(hero) && LOCPLINT->localState->getPath(hero).nextNode().turns == 0 && !LOCPLINT->showingDialog->isBusy();
 	bool wantStop = stoppingMovement;
 	bool canStop = !canMove || canHeroStopAtNode(LOCPLINT->localState->getPath(hero).currNode());
 

+ 1 - 0
client/NetPacksLobbyClient.cpp

@@ -43,6 +43,7 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientConnected(LobbyClientCon
 	// Check if it's LobbyClientConnected for our client
 	if(pack.uuid == handler.logicConnection->uuid)
 	{
+		handler.logicConnection->setSerializationVersion(pack.version);
 		handler.logicConnection->connectionID = pack.clientId;
 		if(handler.mapToStart)
 		{

+ 2 - 2
client/adventureMap/AdventureMapInterface.cpp

@@ -886,9 +886,9 @@ void AdventureMapInterface::hotkeySwitchMapLevel()
 	widget->getMapView()->onMapLevelSwitched();
 }
 
-void AdventureMapInterface::hotkeyZoom(int delta)
+void AdventureMapInterface::hotkeyZoom(int delta, bool useDeadZone)
 {
-	widget->getMapView()->onMapZoomLevelChanged(delta);
+	widget->getMapView()->onMapZoomLevelChanged(delta, useDeadZone);
 }
 
 void AdventureMapInterface::onScreenResize()

+ 1 - 1
client/adventureMap/AdventureMapInterface.h

@@ -120,7 +120,7 @@ public:
 	void hotkeyEndingTurn();
 	void hotkeyNextTown();
 	void hotkeySwitchMapLevel();
-	void hotkeyZoom(int delta);
+	void hotkeyZoom(int delta, bool useDeadZone);
 
 	/// Called by PlayerInterface when specified player is ready to start his turn
 	void onHotseatWaitStarted(PlayerColor playerID);

+ 78 - 9
client/adventureMap/AdventureMapShortcuts.cpp

@@ -12,6 +12,7 @@
 #include "AdventureMapShortcuts.h"
 
 #include "../CGameInfo.h"
+#include "../CMT.h"
 #include "../CPlayerInterface.h"
 #include "../CServerHandler.h"
 #include "../PlayerLocalState.h"
@@ -77,21 +78,26 @@ std::vector<AdventureMapShortcutState> AdventureMapShortcuts::getShortcuts()
 		{ EShortcut::ADVENTURE_CAST_SPELL,       optionHeroSelected(),   [this]() { this->showSpellbook(); } },
 		{ EShortcut::ADVENTURE_GAME_OPTIONS,     optionInMapView(),      [this]() { this->adventureOptions(); } },
 		{ EShortcut::GLOBAL_OPTIONS,             optionInMapView(),      [this]() { this->systemOptions(); } },
+		{ EShortcut::ADVENTURE_FIRST_HERO,       optionInMapView(),      [this]() { this->firstHero(); } },
 		{ EShortcut::ADVENTURE_NEXT_HERO,        optionHasNextHero(),    [this]() { this->nextHero(); } },
-		{ EShortcut::GAME_END_TURN,              optionCanEndTurn(),     [this]() { this->endTurn(); } },
+		{ EShortcut::ADVENTURE_END_TURN,         optionCanEndTurn(),     [this]() { this->endTurn(); } },
 		{ EShortcut::ADVENTURE_THIEVES_GUILD,    optionInMapView(),      [this]() { this->showThievesGuild(); } },
 		{ EShortcut::ADVENTURE_VIEW_SCENARIO,    optionInMapView(),      [this]() { this->showScenarioInfo(); } },
-		{ EShortcut::GAME_SAVE_GAME,             optionInMapView(),      [this]() { this->saveGame(); } },
-		{ EShortcut::GAME_LOAD_GAME,             optionInMapView(),      [this]() { this->loadGame(); } },
+		{ EShortcut::ADVENTURE_QUIT_GAME,        optionInMapView(),      [this]() { this->quitGame(); } },
+		{ EShortcut::ADVENTURE_TO_MAIN_MENU,     optionInMapView(),      [this]() { this->toMainMenu(); } },
+		{ EShortcut::ADVENTURE_SAVE_GAME,        optionInMapView(),      [this]() { this->saveGame(); } },
+		{ EShortcut::ADVENTURE_NEW_GAME,         optionInMapView(),      [this]() { this->newGame(); } },
+		{ EShortcut::ADVENTURE_LOAD_GAME,        optionInMapView(),      [this]() { this->loadGame(); } },
+		{ EShortcut::ADVENTURE_RESTART_GAME,     optionInMapView(),      [this]() { this->restartGame(); } },
 		{ EShortcut::ADVENTURE_DIG_GRAIL,        optionHeroSelected(),   [this]() { this->digGrail(); } },
 		{ EShortcut::ADVENTURE_VIEW_PUZZLE,      optionSidePanelActive(),[this]() { this->viewPuzzleMap(); } },
-		{ EShortcut::GAME_RESTART_GAME,          optionInMapView(),      [this]() { this->restartGame(); } },
 		{ EShortcut::ADVENTURE_VISIT_OBJECT,     optionCanVisitObject(), [this]() { this->visitObject(); } },
 		{ EShortcut::ADVENTURE_VIEW_SELECTED,    optionInMapView(),      [this]() { this->openObject(); } },
-		{ EShortcut::GAME_OPEN_MARKETPLACE,      optionInMapView(),      [this]() { this->showMarketplace(); } },
-		{ EShortcut::ADVENTURE_ZOOM_IN,          optionSidePanelActive(),[this]() { this->zoom(+1); } },
-		{ EShortcut::ADVENTURE_ZOOM_OUT,         optionSidePanelActive(),[this]() { this->zoom(-1); } },
+		{ EShortcut::ADVENTURE_MARKETPLACE,      optionInMapView(),      [this]() { this->showMarketplace(); } },
+		{ EShortcut::ADVENTURE_ZOOM_IN,          optionSidePanelActive(),[this]() { this->zoom(+10); } },
+		{ EShortcut::ADVENTURE_ZOOM_OUT,         optionSidePanelActive(),[this]() { this->zoom(-10); } },
 		{ EShortcut::ADVENTURE_ZOOM_RESET,       optionSidePanelActive(),[this]() { this->zoom( 0); } },
+		{ EShortcut::ADVENTURE_FIRST_TOWN,       optionInMapView(),      [this]() { this->firstTown(); } },
 		{ EShortcut::ADVENTURE_NEXT_TOWN,        optionInMapView(),      [this]() { this->nextTown(); } },
 		{ EShortcut::ADVENTURE_NEXT_OBJECT,      optionInMapView(),      [this]() { this->nextObject(); } },
 		{ EShortcut::ADVENTURE_MOVE_HERO_SW,     optionHeroSelected(),   [this]() { this->moveHeroDirectional({-1, +1}); } },
@@ -223,6 +229,16 @@ void AdventureMapShortcuts::systemOptions()
 	GH.windows().createAndPushWindow<SettingsMainWindow>();
 }
 
+void AdventureMapShortcuts::firstHero()
+{
+	if (!LOCPLINT->localState->getWanderingHeroes().empty())
+	{
+		const auto * hero = LOCPLINT->localState->getWanderingHero(0);
+		LOCPLINT->localState->setSelection(hero);
+		owner.centerOnObject(hero);
+	}
+}
+
 void AdventureMapShortcuts::nextHero()
 {
 	const auto * currHero = LOCPLINT->localState->getCurrentHero();
@@ -288,6 +304,49 @@ void AdventureMapShortcuts::showScenarioInfo()
 	AdventureOptions::showScenarioInfo();
 }
 
+void AdventureMapShortcuts::toMainMenu()
+{
+	LOCPLINT->showYesNoDialog(
+		CGI->generaltexth->allTexts[578],
+		[]()
+		{
+			CSH->endGameplay();
+			GH.defActionsDef = 63;
+			CMM->menu->switchToTab("main");
+		},
+		0
+		);
+}
+
+void AdventureMapShortcuts::newGame()
+{
+	LOCPLINT->showYesNoDialog(
+		CGI->generaltexth->allTexts[578],
+		[]()
+		{
+			CSH->endGameplay();
+			GH.defActionsDef = 63;
+			CMM->menu->switchToTab("new");
+		},
+		nullptr
+		);
+}
+
+void AdventureMapShortcuts::quitGame()
+{
+	LOCPLINT->showYesNoDialog(
+		CGI->generaltexth->allTexts[578],
+		[]()
+		{
+			GH.dispatchMainThread( []()
+			{
+				handleQuit(false);
+			});
+		},
+		0
+		);
+}
+
 void AdventureMapShortcuts::saveGame()
 {
 	GH.windows().createAndPushWindow<CSavingScreen>();
@@ -366,6 +425,16 @@ void AdventureMapShortcuts::showMarketplace()
 		LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket"));
 }
 
+void AdventureMapShortcuts::firstTown()
+{
+	if (!LOCPLINT->localState->getOwnedTowns().empty())
+	{
+		const auto * town = LOCPLINT->localState->getOwnedTown(0);
+		LOCPLINT->localState->setSelection(town);
+		owner.centerOnObject(town);
+	}
+}
+
 void AdventureMapShortcuts::nextTown()
 {
 	owner.hotkeyNextTown();
@@ -373,7 +442,7 @@ void AdventureMapShortcuts::nextTown()
 
 void AdventureMapShortcuts::zoom( int distance)
 {
-	owner.hotkeyZoom(distance);
+	owner.hotkeyZoom(distance, false);
 }
 
 void AdventureMapShortcuts::nextObject()
@@ -494,7 +563,7 @@ bool AdventureMapShortcuts::optionInWorldView()
 
 bool AdventureMapShortcuts::optionSidePanelActive()
 {
-	return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::OTHER_HUMAN_PLAYER_TURN;
+return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::OTHER_HUMAN_PLAYER_TURN;
 }
 
 bool AdventureMapShortcuts::optionMapScrollingActive()

+ 5 - 0
client/adventureMap/AdventureMapShortcuts.h

@@ -49,10 +49,14 @@ class AdventureMapShortcuts
 	void showSpellbook();
 	void adventureOptions();
 	void systemOptions();
+	void firstHero();
 	void nextHero();
 	void endTurn();
 	void showThievesGuild();
 	void showScenarioInfo();
+	void toMainMenu();
+	void newGame();
+	void quitGame();
 	void saveGame();
 	void loadGame();
 	void digGrail();
@@ -61,6 +65,7 @@ class AdventureMapShortcuts
 	void visitObject();
 	void openObject();
 	void showMarketplace();
+	void firstTown();
 	void nextTown();
 	void nextObject();
 	void zoom( int distance);

+ 1 - 1
client/adventureMap/AdventureOptions.cpp

@@ -45,7 +45,7 @@ AdventureOptions::AdventureOptions()
 	scenInfo = std::make_shared<CButton>(Point(24, 198), AnimationPath::builtin("ADVINFO.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_SCENARIO);
 	scenInfo->addCallback(AdventureOptions::showScenarioInfo);
 	
-	replay = std::make_shared<CButton>(Point(24, 257), AnimationPath::builtin("ADVTURN.DEF"), CButton::tooltip(), [&](){ close(); });
+	replay = std::make_shared<CButton>(Point(24, 257), AnimationPath::builtin("ADVTURN.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_REPLAY_TURN);
 	replay->addCallback([]{ LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.replayOpponentTurnNotImplemented")); });
 
 	exit = std::make_shared<CButton>(Point(203, 313), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(), std::bind(&AdventureOptions::close, this), EShortcut::GLOBAL_RETURN);

+ 0 - 3
client/adventureMap/CInGameConsole.cpp

@@ -158,9 +158,6 @@ void CInGameConsole::keyPressed (EShortcut key)
 		break;
 
 	case EShortcut::GAME_ACTIVATE_CONSOLE:
-		if(GH.isKeyboardAltDown())
-			return; //QoL for alt-tab operating system shortcut
-
 		if(!enteredText.empty())
 			endEnteringText(false);
 		else

+ 11 - 7
client/battle/BattleInterface.cpp

@@ -39,7 +39,6 @@
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/CHeroHandler.h"
-#include "../../lib/CondSh.h"
 #include "../../lib/gameState/InfoAboutArmy.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/networkPacks/PacksForClientBattle.h"
@@ -96,7 +95,7 @@ BattleInterface::BattleInterface(const BattleID & battleID, const CCreatureSet *
 	obstacleController.reset(new BattleObstacleController(*this));
 
 	adventureInt->onAudioPaused();
-	ongoingAnimationsState.set(true);
+	ongoingAnimationsState.setBusy();
 
 	GH.windows().pushWindow(windowObject);
 	windowObject->blockUI(true);
@@ -341,7 +340,7 @@ void BattleInterface::battleFinished(const BattleResult& br, QueryID queryID)
 	GH.windows().pushWindow(wnd);
 
 	curInt->waitWhileDialog(); // Avoid freeze when AI end turn after battle. Check bug #1897
-	CPlayerInterface::battleInt = nullptr;
+	CPlayerInterface::battleInt.reset();
 }
 
 void BattleInterface::spellCast(const BattleSpellCast * sc)
@@ -752,6 +751,11 @@ void BattleInterface::castThisSpell(SpellID spellID)
 	actionsController->castThisSpell(spellID);
 }
 
+void BattleInterface::endNetwork()
+{
+	ongoingAnimationsState.requestTermination();
+}
+
 void BattleInterface::executeStagedAnimations()
 {
 	EAnimationEvents earliestStage = EAnimationEvents::COUNT;
@@ -783,19 +787,19 @@ void BattleInterface::executeAnimationStage(EAnimationEvents event)
 
 void BattleInterface::onAnimationsStarted()
 {
-	ongoingAnimationsState.setn(true);
+	ongoingAnimationsState.setBusy();
 }
 
 void BattleInterface::onAnimationsFinished()
 {
-	ongoingAnimationsState.setn(false);
+	ongoingAnimationsState.setFree();
 }
 
 void BattleInterface::waitForAnimations()
 {
 	{
 		auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex);
-		ongoingAnimationsState.waitUntil(false);
+		ongoingAnimationsState.waitWhileBusy();
 	}
 
 	assert(!hasAnimations());
@@ -810,7 +814,7 @@ void BattleInterface::waitForAnimations()
 
 bool BattleInterface::hasAnimations()
 {
-	return ongoingAnimationsState.get();
+	return ongoingAnimationsState.isBusy();
 }
 
 void BattleInterface::checkForAnimations()

+ 3 - 2
client/battle/BattleInterface.h

@@ -12,7 +12,7 @@
 #include "BattleConstants.h"
 #include "../gui/CIntObject.h"
 #include "../../lib/spells/CSpellHandler.h" //CSpell::TAnimation
-#include "../../lib/CondSh.h"
+#include "../ConditionalWait.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -99,7 +99,7 @@ class BattleInterface
 	};
 
 	/// Conditional variables that are set depending on ongoing animations on the battlefield
-	CondSh<bool> ongoingAnimationsState;
+	ConditionalWait ongoingAnimationsState;
 
 	/// List of events that are waiting to be triggered
 	std::vector<AwaitingAnimationEvents> awaitingEvents;
@@ -186,6 +186,7 @@ public:
 	void setBattleQueueVisibility(bool visible);
 	void setStickyHeroWindowsVisibility(bool visible);
 
+	void endNetwork();
 	void executeStagedAnimations();
 	void executeAnimationStage( EAnimationEvents event);
 	void onAnimationsStarted();

+ 9 - 3
client/battle/BattleInterfaceClasses.cpp

@@ -52,7 +52,6 @@
 #include "../../lib/CTownHandler.h"
 #include "../../lib/CHeroHandler.h"
 #include "../../lib/StartInfo.h"
-#include "../../lib/CondSh.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/networkPacks/PacksForClientBattle.h"
 #include "../../lib/TextOperations.h"
@@ -83,6 +82,13 @@ std::vector<std::string> BattleConsole::getVisibleText()
 
 		auto result = CMessage::breakText(text, pos.w, FONT_SMALL);
 
+		if(result.size() > 2 && text.find('\n') != std::string::npos)
+		{
+			// Text has too many lines to fit into console, but has line breaks. Try ignore them and fit text that way
+			std::string cleanText = boost::algorithm::replace_all_copy(text, "\n", " ");
+			result = CMessage::breakText(cleanText, pos.w, FONT_SMALL);
+		}
+
 		if(result.size() > 2)
 			result.resize(2);
 		return result;
@@ -801,7 +807,7 @@ BattleResultResources BattleResultWindow::getResources(const BattleResult & br)
 
 void BattleResultWindow::activate()
 {
-	owner.showingDialog->set(true);
+	owner.showingDialog->setBusy();
 	CIntObject::activate();
 }
 
@@ -819,7 +825,7 @@ void BattleResultWindow::buttonPressed(int button)
 
 	//Result window and battle interface are gone. We requested all dialogs to be closed before opening the battle,
 	//so we can be sure that there is no dialogs left on GUI stack.
-	intTmp.showingDialog->setn(false);
+	intTmp.showingDialog->setFree();
 }
 
 void BattleResultWindow::bExitf()

+ 0 - 1
client/battle/BattleStacksController.cpp

@@ -39,7 +39,6 @@
 #include "../../lib/battle/BattleHex.h"
 #include "../../lib/CRandomGenerator.h"
 #include "../../lib/CStack.h"
-#include "../../lib/CondSh.h"
 #include "../../lib/TextOperations.h"
 
 static void onAnimationFinished(const CStack *stack, std::weak_ptr<CreatureAnimation> anim)

+ 25 - 5
client/battle/BattleWindow.cpp

@@ -26,6 +26,7 @@
 #include "../widgets/Buttons.h"
 #include "../widgets/Images.h"
 #include "../windows/CMessage.h"
+#include "../windows/CCreatureWindow.h"
 #include "../render/CAnimation.h"
 #include "../render/Canvas.h"
 #include "../render/IRenderHandler.h"
@@ -76,6 +77,8 @@ BattleWindow::BattleWindow(BattleInterface & owner):
 	addShortcut(EShortcut::BATTLE_TACTICS_NEXT, std::bind(&BattleWindow::bTacticNextStack, this));
 	addShortcut(EShortcut::BATTLE_TACTICS_END, std::bind(&BattleWindow::bTacticPhaseEnd, this));
 	addShortcut(EShortcut::BATTLE_SELECT_ACTION, std::bind(&BattleWindow::bSwitchActionf, this));
+	addShortcut(EShortcut::BATTLE_OPEN_ACTIVE_UNIT, std::bind(&BattleWindow::bOpenActiveUnit, this));
+	addShortcut(EShortcut::BATTLE_OPEN_HOVERED_UNIT, std::bind(&BattleWindow::bOpenHoveredUnit, this));
 
 	addShortcut(EShortcut::BATTLE_TOGGLE_QUEUE, [this](){ this->toggleQueueVisibility();});
 	addShortcut(EShortcut::BATTLE_TOGGLE_HEROES_STATS, [this](){ this->toggleStickyHeroWindowsVisibility();});
@@ -188,11 +191,6 @@ void BattleWindow::createTimerInfoWindows()
 	}
 }
 
-BattleWindow::~BattleWindow()
-{
-	CPlayerInterface::battleInt = nullptr;
-}
-
 std::shared_ptr<BattleConsole> BattleWindow::buildBattleConsole(const JsonNode & config) const
 {
 	auto rect = readRect(config["rect"]);
@@ -754,6 +752,8 @@ void BattleWindow::blockUI(bool on)
 	bool canWait = owner.stacksController->getActiveStack() ? !owner.stacksController->getActiveStack()->waitedThisTurn : false;
 
 	setShortcutBlocked(EShortcut::GLOBAL_OPTIONS, on);
+	setShortcutBlocked(EShortcut::BATTLE_OPEN_ACTIVE_UNIT, on);
+	setShortcutBlocked(EShortcut::BATTLE_OPEN_HOVERED_UNIT, on);
 	setShortcutBlocked(EShortcut::BATTLE_RETREAT, on || !owner.getBattle()->battleCanFlee());
 	setShortcutBlocked(EShortcut::BATTLE_SURRENDER, on || owner.getBattle()->battleGetSurrenderCost() < 0);
 	setShortcutBlocked(EShortcut::BATTLE_CAST_SPELL, on || owner.tacticsMode || !canCastSpells);
@@ -768,6 +768,26 @@ void BattleWindow::blockUI(bool on)
 	setShortcutBlocked(EShortcut::BATTLE_CONSOLE_UP, on && !owner.tacticsMode);
 }
 
+void BattleWindow::bOpenActiveUnit()
+{
+	const auto * unit = owner.stacksController->getActiveStack();
+
+	if (unit)
+		GH.windows().createAndPushWindow<CStackWindow>(unit, false);;
+}
+
+void BattleWindow::bOpenHoveredUnit()
+{
+	const auto units = owner.stacksController->getHoveredStacksUnitIds();
+
+	if (!units.empty())
+	{
+		const auto * unit = owner.getBattle()->battleGetStackByID(units[0]);
+		if (unit)
+			GH.windows().createAndPushWindow<CStackWindow>(unit, false);
+	}
+}
+
 std::optional<uint32_t> BattleWindow::getQueueHoveredUnitId()
 {
 	return queue->getHoveredUnitIdIfAny();

+ 2 - 1
client/battle/BattleWindow.h

@@ -56,6 +56,8 @@ class BattleWindow : public InterfaceObjectConfigurable
 	void bConsoleDownf();
 	void bTacticNextStack();
 	void bTacticPhaseEnd();
+	void bOpenActiveUnit();
+	void bOpenHoveredUnit();
 
 	/// functions for handling actions after they were confirmed by popup window
 	void reallyFlee();
@@ -80,7 +82,6 @@ class BattleWindow : public InterfaceObjectConfigurable
 
 public:
 	BattleWindow(BattleInterface & owner );
-	~BattleWindow();
 
 	/// Closes window once battle finished
 	void close();

+ 1 - 10
client/eventsSDL/InputHandler.cpp

@@ -165,6 +165,7 @@ void InputHandler::preprocessEvent(const SDL_Event & ev)
 	{
 		if(ev.key.keysym.sym == SDLK_F4 && (ev.key.keysym.mod & KMOD_ALT))
 		{
+			// FIXME: dead code? Looks like intercepted by OS/SDL and delivered as SDL_Quit instead?
 			boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
 			handleQuit(true);
 			return;
@@ -176,16 +177,6 @@ void InputHandler::preprocessEvent(const SDL_Event & ev)
 			handleQuit(true);
 			return;
 		}
-
-		if(ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_F4)
-		{
-			boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
-			Settings full = settings.write["video"]["fullscreen"];
-			full->Bool() = !full->Bool();
-
-			GH.onScreenResize(false);
-			return;
-		}
 	}
 	else if(ev.type == SDL_USEREVENT)
 	{

+ 8 - 1
client/eventsSDL/InputSourceKeyboard.cpp

@@ -75,9 +75,16 @@ void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key)
 
 	auto shortcutsVector = GH.shortcuts().translateKeycode(keyName);
 
-	if (vstd::contains(shortcutsVector, EShortcut::LOBBY_ACTIVATE_INTERFACE))
+	if (vstd::contains(shortcutsVector, EShortcut::MAIN_MENU_LOBBY))
 		CSH->getGlobalLobby().activateInterface();
 
+	if (vstd::contains(shortcutsVector, EShortcut::GLOBAL_FULLSCREEN))
+	{
+		Settings full = settings.write["video"]["fullscreen"];
+		full->Bool() = !full->Bool();
+		GH.onScreenResize(true);
+	}
+
 	if (vstd::contains(shortcutsVector, EShortcut::SPECTATE_TRACK_HERO))
 	{
 		Settings s = settings.write["session"];

+ 2 - 1
client/globalLobby/GlobalLobbyInviteWindow.cpp

@@ -15,6 +15,7 @@
 
 #include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/GraphicalPrimitiveCanvas.h"
 #include "../widgets/Images.h"
@@ -95,7 +96,7 @@ GlobalLobbyInviteWindow::GlobalLobbyInviteWindow()
 	listBackground = std::make_shared<TransparentFilledRectangle>(Rect(8, 48, 220, 324), ColorRGBA(0, 0, 0, 64), ColorRGBA(64, 80, 128, 255), 1);
 	accountList = std::make_shared<CListBox>(createAccountCardCallback, Point(10, 50), Point(0, 40), 8, 0, 0, 1 | 4, Rect(200, 0, 320, 320));
 
-	buttonClose = std::make_shared<CButton>(Point(86, 384), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this]() { close(); } );
+	buttonClose = std::make_shared<CButton>(Point(86, 384), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this]() { close(); }, EShortcut::GLOBAL_RETURN );
 
 	center();
 }

+ 3 - 4
client/globalLobby/GlobalLobbyLoginWindow.cpp

@@ -12,12 +12,11 @@
 #include "GlobalLobbyLoginWindow.h"
 
 #include "GlobalLobbyClient.h"
-#include "GlobalLobbyWindow.h"
 
 #include "../CGameInfo.h"
 #include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
-#include "../gui/WindowHandler.h"
+#include "../gui/Shortcut.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/CTextInput.h"
 #include "../widgets/Images.h"
@@ -47,8 +46,8 @@ GlobalLobbyLoginWindow::GlobalLobbyLoginWindow()
 	labelUsername = std::make_shared<CLabel>( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, loginAs.toString());
 	backgroundUsername = std::make_shared<TransparentFilledRectangle>(Rect(10, 90, 264, 20), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64));
 	inputUsername = std::make_shared<CTextInput>(Rect(15, 93, 260, 16), FONT_SMALL, ETextAlignment::CENTERLEFT, true);
-	buttonLogin = std::make_shared<CButton>(Point(10, 180), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onLogin(); });
-	buttonClose = std::make_shared<CButton>(Point(210, 180), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); });
+	buttonLogin = std::make_shared<CButton>(Point(10, 180), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onLogin(); }, EShortcut::GLOBAL_ACCEPT);
+	buttonClose = std::make_shared<CButton>(Point(210, 180), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); }, EShortcut::GLOBAL_CANCEL);
 	labelStatus = std::make_shared<CTextBox>( "", Rect(15, 115, 255, 60), 1, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
 
 	auto buttonRegister = std::make_shared<CToggleButton>(Point(10, 40),  AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0);

+ 3 - 4
client/globalLobby/GlobalLobbyRoomWindow.cpp

@@ -18,6 +18,7 @@
 #include "../CGameInfo.h"
 #include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../mainmenu/CMainMenu.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/Images.h"
@@ -25,10 +26,8 @@
 #include "../widgets/GraphicalPrimitiveCanvas.h"
 #include "../widgets/ObjectLists.h"
 
-#include "../../lib/CConfigHandler.h"
 #include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/MetaString.h"
-#include "../../lib/VCMI_Lib.h"
 #include "../../lib/modding/CModHandler.h"
 #include "../../lib/modding/CModInfo.h"
 
@@ -148,8 +147,8 @@ GlobalLobbyRoomWindow::GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const s
 	labelVersionTitle = std::make_shared<CLabel>( 10, 60, FONT_MEDIUM, ETextAlignment::CENTERLEFT, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.preview.version").toString());
 	labelVersionValue = std::make_shared<CLabel>( 10, 80, FONT_MEDIUM, ETextAlignment::CENTERLEFT, Colors::WHITE, roomDescription.gameVersion);
 
-	buttonJoin = std::make_shared<CButton>(Point(10, 360), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onJoin(); });
-	buttonClose = std::make_shared<CButton>(Point(100, 360), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); });
+	buttonJoin = std::make_shared<CButton>(Point(10, 360), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onJoin(); }, EShortcut::GLOBAL_ACCEPT);
+	buttonClose = std::make_shared<CButton>(Point(100, 360), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); }, EShortcut::GLOBAL_CANCEL);
 
 	MetaString joinStatusText;
 	std::string errorMessage = getJoinRoomErrorMessage(roomDescription, modVerificationList);

+ 3 - 2
client/globalLobby/GlobalLobbyServerSetup.cpp

@@ -16,6 +16,7 @@
 #include "../CGameInfo.h"
 #include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../mainmenu/CMainMenu.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/Images.h"
@@ -74,8 +75,8 @@ GlobalLobbyServerSetup::GlobalLobbyServerSetup()
 
 	labelDescription = std::make_shared<CTextBox>("", Rect(10, 195, pos.w - 20, 80), 1, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
 
-	buttonCreate = std::make_shared<CButton>(Point(10, 300), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onCreate(); });
-	buttonClose = std::make_shared<CButton>(Point(210, 300), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); });
+	buttonCreate = std::make_shared<CButton>(Point(10, 300), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onCreate(); }, EShortcut::GLOBAL_ACCEPT);
+	buttonClose = std::make_shared<CButton>(Point(210, 300), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); }, EShortcut::GLOBAL_CANCEL);
 
 	filledBackground->playerColored(PlayerColor(1));
 

+ 0 - 1
client/gui/CGuiHandler.cpp

@@ -9,7 +9,6 @@
  */
 #include "StdInc.h"
 #include "CGuiHandler.h"
-#include "../lib/CondSh.h"
 
 #include "CIntObject.h"
 #include "CursorHandler.h"

+ 0 - 1
client/gui/CGuiHandler.h

@@ -10,7 +10,6 @@
 #pragma once
 
 VCMI_LIB_NAMESPACE_BEGIN
-template <typename T> struct CondSh;
 class Point;
 class Rect;
 VCMI_LIB_NAMESPACE_END

+ 4 - 3
client/gui/InterfaceObjectConfigurable.cpp

@@ -492,7 +492,7 @@ void InterfaceObjectConfigurable::loadButtonHotkey(std::shared_ptr<CButton> butt
 		return;
 
 	button->addCallback(target->second.callback);
-	target->second.assignedToButton = true;
+	target->second.assignedButtons.push_back(button);
 }
 
 std::shared_ptr<CLabelGroup> InterfaceObjectConfigurable::buildLabelGroup(const JsonNode & config) const
@@ -821,8 +821,9 @@ void InterfaceObjectConfigurable::keyPressed(EShortcut key)
 	if (target == shortcuts.end())
 		return;
 
-	if (target->second.assignedToButton)
-		return; // will be handled by button instance
+	for (auto const & button :target->second.assignedButtons)
+		if (button->isActive())
+			return; // will be handled by button instance
 
 	if (target->second.blocked)
 		return;

+ 1 - 1
client/gui/InterfaceObjectConfigurable.h

@@ -118,7 +118,7 @@ private:
 	struct ShortcutState
 	{
 		std::function<void()> callback;
-		mutable bool assignedToButton = false;
+		mutable std::vector<std::shared_ptr<CButton>> assignedButtons;
 		bool blocked = false;
 	};
 	

+ 112 - 23
client/gui/Shortcut.h

@@ -66,25 +66,55 @@ enum class EShortcut
 	MAIN_MENU_CAMPAIGN_AB,
 	MAIN_MENU_CAMPAIGN_CUSTOM,
 
+	MAIN_MENU_HOTSEAT,
+	MAIN_MENU_LOBBY,
+	MAIN_MENU_HOST_GAME,
+	MAIN_MENU_JOIN_GAME,
+
+	HIGH_SCORES_CAMPAIGNS,
+	HIGH_SCORES_SCENARIOS,
+	HIGH_SCORES_RESET,
+
 	// Game lobby / scenario selection
 	LOBBY_BEGIN_STANDARD_GAME, // b
 	LOBBY_BEGIN_CAMPAIGN, // Return
 	LOBBY_LOAD_GAME,  // l, Return
 	LOBBY_SAVE_GAME,  // s, Return
 	LOBBY_RANDOM_MAP, // Open random map tab
-	LOBBY_HIDE_CHAT,
+	LOBBY_TOGGLE_CHAT,
 	LOBBY_ADDITIONAL_OPTIONS, // Open additional options tab
 	LOBBY_SELECT_SCENARIO,    // Open map list tab
+	LOBBY_REPLAY_VIDEO,
+	LOBBY_EXTRA_OPTIONS,
+	LOBBY_TURN_OPTIONS,
+	LOBBY_INVITE_PLAYERS,
+	LOBBY_FLIP_COIN,
+	LOBBY_RANDOM_TOWN,
+	LOBBY_RANDOM_TOWN_VS,
+
+	MAPS_SIZE_S,
+	MAPS_SIZE_M,
+	MAPS_SIZE_L,
+	MAPS_SIZE_XL,
+	MAPS_SIZE_ALL,
+
+	MAPS_SORT_PLAYERS,
+	MAPS_SORT_SIZE,
+	MAPS_SORT_FORMAT,
+	MAPS_SORT_NAME,
+	MAPS_SORT_VICTORY,
+	MAPS_SORT_DEFEAT,
+	MAPS_SORT_MAPS,
+	MAPS_SORT_CHANGEDATE,
+
+	SETTINGS_LOAD_GAME,
+	SETTINGS_SAVE_GAME,
+	SETTINGS_NEW_GAME,
+	SETTINGS_RESTART_GAME,
+	SETTINGS_TO_MAIN_MENU,
+	SETTINGS_QUIT_GAME,
 
 	// In-game hotkeys, require game state but may be available in windows other than adventure map
-	GAME_END_TURN,
-	GAME_LOAD_GAME,
-	GAME_SAVE_GAME,
-	GAME_RESTART_GAME,
-	GAME_TO_MAIN_MENU,
-	GAME_QUIT_GAME,
-	GAME_OPEN_MARKETPLACE,
-	GAME_OPEN_THIEVES_GUILD,
 	GAME_ACTIVATE_CONSOLE, // Tab, activates in-game console
 
 	// Adventure map screen
@@ -98,10 +128,11 @@ enum class EShortcut
 	ADVENTURE_VIEW_SELECTED,// Open window with currently selected hero/town
 	ADVENTURE_NEXT_TOWN,
 	ADVENTURE_NEXT_HERO,
-	ADVENTURE_NEXT_OBJECT,  // TODO: context-sensitive next object - select next hero/town, depending on current selection
-	ADVENTURE_FIRST_TOWN,   // TODO: select first available town in the list
-	ADVENTURE_FIRST_HERO,   // TODO: select first available hero in the list
+	ADVENTURE_NEXT_OBJECT,  // context-sensitive next object - select next hero/town, depending on current selection
+	ADVENTURE_FIRST_TOWN,   // select first available town in the list
+	ADVENTURE_FIRST_HERO,   // select first available hero in the list
 	ADVENTURE_VIEW_SCENARIO,// View Scenario Information window
+	ADVENTURE_REPLAY_TURN,
 	ADVENTURE_DIG_GRAIL,
 	ADVENTURE_VIEW_PUZZLE,
 	ADVENTURE_VIEW_WORLD,
@@ -113,11 +144,19 @@ enum class EShortcut
 	ADVENTURE_KINGDOM_OVERVIEW,
 	ADVENTURE_QUEST_LOG,
 	ADVENTURE_CAST_SPELL,
+	ADVENTURE_MARKETPLACE,
 	ADVENTURE_THIEVES_GUILD,
 	ADVENTURE_EXIT_WORLD_VIEW,
 	ADVENTURE_ZOOM_IN,
 	ADVENTURE_ZOOM_OUT,
 	ADVENTURE_ZOOM_RESET,
+	ADVENTURE_END_TURN,
+	ADVENTURE_LOAD_GAME,
+	ADVENTURE_SAVE_GAME,
+	ADVENTURE_NEW_GAME,
+	ADVENTURE_RESTART_GAME,
+	ADVENTURE_TO_MAIN_MENU,
+	ADVENTURE_QUIT_GAME,
 
 	// Move hero one tile in specified direction. Bound to cursors & numpad buttons
 	ADVENTURE_MOVE_HERO_SW,
@@ -145,8 +184,20 @@ enum class EShortcut
 	BATTLE_TACTICS_END,
 	BATTLE_SELECT_ACTION, // Alternative actions toggle
 	BATTLE_TOGGLE_HEROES_STATS,
+	BATTLE_OPEN_ACTIVE_UNIT,
+	BATTLE_OPEN_HOVERED_UNIT,
 
-	LOBBY_ACTIVATE_INTERFACE,
+	MARKET_DEAL,
+	MARKET_MAX_AMOUNT,
+	MARKET_SACRIFICE_ALL,
+	MARKET_SACRIFICE_BACKPACK,
+	MARKET_RESOURCE_PLAYER,
+	MARKET_ARTIFACT_RESOURCE,
+	MARKET_RESOURCE_ARTIFACT,
+	MARKET_CREATURE_RESOURCE,
+	MARKET_RESOURCE_RESOURCE,
+	MARKET_CREATURE_EXPERIENCE,
+	MARKET_ARTIFACT_EXPERIENCE,
 
 	SPECTATE_TRACK_HERO,
 	SPECTATE_SKIP_BATTLE,
@@ -154,11 +205,22 @@ enum class EShortcut
 
 	// Town screen
 	TOWN_OPEN_TAVERN,
+	TOWN_OPEN_HALL,
+	TOWN_OPEN_FORT,
+	TOWN_OPEN_MARKET,
+	TOWN_OPEN_MAGE_GUILD,
+	TOWN_OPEN_THIEVES_GUILD,
+	TOWN_OPEN_RECRUITMENT,
+	TOWN_OPEN_HERO_EXCHANGE,
+	TOWN_OPEN_HERO,
+	TOWN_OPEN_VISITING_HERO,
+	TOWN_OPEN_GARRISONED_HERO,
 	TOWN_SWAP_ARMIES, // Swap garrisoned and visiting armies
 
 	// Creature & creature recruitment screen
 	RECRUITMENT_MAX, // Set number of creatures to recruit to max
 	RECRUITMENT_MIN, // Set number of creatures to recruit to min (1)
+	RECRUITMENT_SWITCH_LEVEL,
 	RECRUITMENT_UPGRADE, // Upgrade current creature
 	RECRUITMENT_UPGRADE_ALL, // Upgrade all creatures (Hill Fort / Skeleton Transformer)
 
@@ -172,17 +234,44 @@ enum class EShortcut
 	HERO_LOOSE_FORMATION,
 	HERO_TIGHT_FORMATION,
 	HERO_TOGGLE_TACTICS, // b
+	HERO_ARMY_SPLIT,
 	HERO_BACKPACK,
-	HERO_COSTUME_0,
-	HERO_COSTUME_1,
-	HERO_COSTUME_2,
-	HERO_COSTUME_3,
-	HERO_COSTUME_4,
-	HERO_COSTUME_5,
-	HERO_COSTUME_6,
-	HERO_COSTUME_7,
-	HERO_COSTUME_8,
-	HERO_COSTUME_9,
+	HERO_COSTUME_SAVE_0,
+	HERO_COSTUME_SAVE_1,
+	HERO_COSTUME_SAVE_2,
+	HERO_COSTUME_SAVE_3,
+	HERO_COSTUME_SAVE_4,
+	HERO_COSTUME_SAVE_5,
+	HERO_COSTUME_SAVE_6,
+	HERO_COSTUME_SAVE_7,
+	HERO_COSTUME_SAVE_8,
+	HERO_COSTUME_SAVE_9,
+
+	HERO_COSTUME_LOAD_0,
+	HERO_COSTUME_LOAD_1,
+	HERO_COSTUME_LOAD_2,
+	HERO_COSTUME_LOAD_3,
+	HERO_COSTUME_LOAD_4,
+	HERO_COSTUME_LOAD_5,
+	HERO_COSTUME_LOAD_6,
+	HERO_COSTUME_LOAD_7,
+	HERO_COSTUME_LOAD_8,
+	HERO_COSTUME_LOAD_9,
+
+	EXCHANGE_ARMY_TO_LEFT,
+	EXCHANGE_ARMY_TO_RIGHT,
+	EXCHANGE_ARMY_SWAP,
+	EXCHANGE_ARTIFACTS_TO_LEFT,
+	EXCHANGE_ARTIFACTS_TO_RIGHT,
+	EXCHANGE_ARTIFACTS_SWAP,
+	EXCHANGE_EQUIPPED_TO_LEFT,
+	EXCHANGE_EQUIPPED_TO_RIGHT,
+	EXCHANGE_EQUIPPED_SWAP,
+	EXCHANGE_BACKPACK_TO_LEFT,
+	EXCHANGE_BACKPACK_TO_RIGHT,
+	EXCHANGE_BACKPACK_SWAP,
+	EXCHANGE_BACKPACK_LEFT,
+	EXCHANGE_BACKPACK_RIGHT,
 
 	// Spellbook screen
 	SPELLBOOK_TAB_ADVENTURE,

+ 139 - 21
client/gui/ShortcutHandler.cpp

@@ -22,6 +22,25 @@ ShortcutHandler::ShortcutHandler()
 	mappedKeyboardShortcuts = loadShortcuts(config["keyboard"]);
 	mappedJoystickShortcuts = loadShortcuts(config["joystickButtons"]);
 	mappedJoystickAxes = loadShortcuts(config["joystickAxes"]);
+
+#ifndef ENABLE_GOLDMASTER
+	std::vector<EShortcut> assignedShortcuts;
+	std::vector<EShortcut> missingShortcuts;
+
+	for (auto const & entry : config["keyboard"].Struct())
+	{
+		EShortcut shortcutID = findShortcut(entry.first);
+		assert(!vstd::contains(assignedShortcuts, shortcutID));
+		assignedShortcuts.push_back(shortcutID);
+	}
+
+	for (EShortcut id = vstd::next(EShortcut::NONE, 1); id < EShortcut::AFTER_LAST; id = vstd::next(id, 1))
+		if (!vstd::contains(assignedShortcuts, id))
+			missingShortcuts.push_back(id);
+
+	if (!missingShortcuts.empty())
+		logGlobal->error("Found %d shortcuts without config entry!", missingShortcuts.size());
+#endif
 }
 
 std::multimap<std::string, EShortcut> ShortcutHandler::loadShortcuts(const JsonNode & data) const
@@ -131,22 +150,24 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 		{"mainMenuCampaignRoe",      EShortcut::MAIN_MENU_CAMPAIGN_ROE    },
 		{"mainMenuCampaignAb",       EShortcut::MAIN_MENU_CAMPAIGN_AB     },
 		{"mainMenuCampaignCustom",   EShortcut::MAIN_MENU_CAMPAIGN_CUSTOM },
+		{"mainMenuLobby",            EShortcut::MAIN_MENU_LOBBY           },
 		{"lobbyBeginStandardGame",   EShortcut::LOBBY_BEGIN_STANDARD_GAME },
 		{"lobbyBeginCampaign",       EShortcut::LOBBY_BEGIN_CAMPAIGN      },
 		{"lobbyLoadGame",            EShortcut::LOBBY_LOAD_GAME           },
 		{"lobbySaveGame",            EShortcut::LOBBY_SAVE_GAME           },
 		{"lobbyRandomMap",           EShortcut::LOBBY_RANDOM_MAP          },
-		{"lobbyHideChat",            EShortcut::LOBBY_HIDE_CHAT           },
+		{"lobbyToggleChat",          EShortcut::LOBBY_TOGGLE_CHAT         },
 		{"lobbyAdditionalOptions",   EShortcut::LOBBY_ADDITIONAL_OPTIONS  },
 		{"lobbySelectScenario",      EShortcut::LOBBY_SELECT_SCENARIO     },
-		{"gameEndTurn",              EShortcut::GAME_END_TURN             },
-		{"gameLoadGame",             EShortcut::GAME_LOAD_GAME            },
-		{"gameSaveGame",             EShortcut::GAME_SAVE_GAME            },
-		{"gameRestartGame",          EShortcut::GAME_RESTART_GAME         },
-		{"gameMainMenu",             EShortcut::GAME_TO_MAIN_MENU         },
-		{"gameQuitGame",             EShortcut::GAME_QUIT_GAME            },
-		{"gameOpenMarketplace",      EShortcut::GAME_OPEN_MARKETPLACE     },
-		{"gameOpenThievesGuild",     EShortcut::GAME_OPEN_THIEVES_GUILD   },
+		{"gameEndTurn",              EShortcut::ADVENTURE_END_TURN        }, // compatibility ID - extra's use this string
+		{"adventureEndTurn",         EShortcut::ADVENTURE_END_TURN        },
+		{"adventureLoadGame",        EShortcut::ADVENTURE_LOAD_GAME       },
+		{"adventureSaveGame",        EShortcut::ADVENTURE_SAVE_GAME       },
+		{"adventureRestartGame",     EShortcut::ADVENTURE_RESTART_GAME    },
+		{"adventureMainMenu",        EShortcut::ADVENTURE_TO_MAIN_MENU    },
+		{"adventureQuitGame",        EShortcut::ADVENTURE_QUIT_GAME       },
+		{"adventureMarketplace",     EShortcut::ADVENTURE_MARKETPLACE     },
+		{"adventureThievesGuild",    EShortcut::ADVENTURE_THIEVES_GUILD   },
 		{"gameActivateConsole",      EShortcut::GAME_ACTIVATE_CONSOLE     },
 		{"adventureGameOptions",     EShortcut::ADVENTURE_GAME_OPTIONS    },
 		{"adventureToggleGrid",      EShortcut::ADVENTURE_TOGGLE_GRID     },
@@ -201,7 +222,6 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 		{"battleTacticsNext",        EShortcut::BATTLE_TACTICS_NEXT       },
 		{"battleTacticsEnd",         EShortcut::BATTLE_TACTICS_END        },
 		{"battleSelectAction",       EShortcut::BATTLE_SELECT_ACTION      },
-		{"lobbyActivateInterface",   EShortcut::LOBBY_ACTIVATE_INTERFACE  },
 		{"spectateTrackHero",        EShortcut::SPECTATE_TRACK_HERO       },
 		{"spectateSkipBattle",       EShortcut::SPECTATE_SKIP_BATTLE      },
 		{"spectateSkipBattleResult", EShortcut::SPECTATE_SKIP_BATTLE_RESULT },
@@ -218,20 +238,118 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 		{"heroLooseFormation",       EShortcut::HERO_LOOSE_FORMATION      },
 		{"heroTightFormation",       EShortcut::HERO_TIGHT_FORMATION      },
 		{"heroToggleTactics",        EShortcut::HERO_TOGGLE_TACTICS       },
-		{"heroCostume0",             EShortcut::HERO_COSTUME_0            },
-		{"heroCostume1",             EShortcut::HERO_COSTUME_1            },
-		{"heroCostume2",             EShortcut::HERO_COSTUME_2            },
-		{"heroCostume3",             EShortcut::HERO_COSTUME_3            },
-		{"heroCostume4",             EShortcut::HERO_COSTUME_4            },
-		{"heroCostume5",             EShortcut::HERO_COSTUME_5            },
-		{"heroCostume6",             EShortcut::HERO_COSTUME_6            },
-		{"heroCostume7",             EShortcut::HERO_COSTUME_7            },
-		{"heroCostume8",             EShortcut::HERO_COSTUME_8            },
-		{"heroCostume9",             EShortcut::HERO_COSTUME_9            },
+		{"heroCostumeSave0",         EShortcut::HERO_COSTUME_SAVE_0       },
+		{"heroCostumeSave1",         EShortcut::HERO_COSTUME_SAVE_1       },
+		{"heroCostumeSave2",         EShortcut::HERO_COSTUME_SAVE_2       },
+		{"heroCostumeSave3",         EShortcut::HERO_COSTUME_SAVE_3       },
+		{"heroCostumeSave4",         EShortcut::HERO_COSTUME_SAVE_4       },
+		{"heroCostumeSave5",         EShortcut::HERO_COSTUME_SAVE_5       },
+		{"heroCostumeSave6",         EShortcut::HERO_COSTUME_SAVE_6       },
+		{"heroCostumeSave7",         EShortcut::HERO_COSTUME_SAVE_7       },
+		{"heroCostumeSave8",         EShortcut::HERO_COSTUME_SAVE_8       },
+		{"heroCostumeSave9",         EShortcut::HERO_COSTUME_SAVE_9       },
+		{"heroCostumeLoad0",         EShortcut::HERO_COSTUME_LOAD_0       },
+		{"heroCostumeLoad1",         EShortcut::HERO_COSTUME_LOAD_1       },
+		{"heroCostumeLoad2",         EShortcut::HERO_COSTUME_LOAD_2       },
+		{"heroCostumeLoad3",         EShortcut::HERO_COSTUME_LOAD_3       },
+		{"heroCostumeLoad4",         EShortcut::HERO_COSTUME_LOAD_4       },
+		{"heroCostumeLoad5",         EShortcut::HERO_COSTUME_LOAD_5       },
+		{"heroCostumeLoad6",         EShortcut::HERO_COSTUME_LOAD_6       },
+		{"heroCostumeLoad7",         EShortcut::HERO_COSTUME_LOAD_7       },
+		{"heroCostumeLoad8",         EShortcut::HERO_COSTUME_LOAD_8       },
+		{"heroCostumeLoad9",         EShortcut::HERO_COSTUME_LOAD_9       },
 		{"spellbookTabAdventure",    EShortcut::SPELLBOOK_TAB_ADVENTURE   },
-		{"spellbookTabCombat",       EShortcut::SPELLBOOK_TAB_COMBAT      }
+		{"spellbookTabCombat",       EShortcut::SPELLBOOK_TAB_COMBAT      },
+		{"mainMenuHotseat",          EShortcut::MAIN_MENU_HOTSEAT         },
+		{"mainMenuHostGame",         EShortcut::MAIN_MENU_HOST_GAME       },
+		{"mainMenuJoinGame",         EShortcut::MAIN_MENU_JOIN_GAME       },
+		{"highScoresCampaigns",      EShortcut::HIGH_SCORES_CAMPAIGNS     },
+		{"highScoresScenarios",      EShortcut::HIGH_SCORES_SCENARIOS     },
+		{"highScoresReset",          EShortcut::HIGH_SCORES_RESET         },
+		{"lobbyReplayVideo",         EShortcut::LOBBY_REPLAY_VIDEO        },
+		{"lobbyExtraOptions",        EShortcut::LOBBY_EXTRA_OPTIONS       },
+		{"lobbyTurnOptions",         EShortcut::LOBBY_TURN_OPTIONS        },
+		{"lobbyInvitePlayers",       EShortcut::LOBBY_INVITE_PLAYERS      },
+		{"lobbyFlipCoin",            EShortcut::LOBBY_FLIP_COIN           },
+		{"lobbyRandomTown",          EShortcut::LOBBY_RANDOM_TOWN         },
+		{"lobbyRandomTownVs",        EShortcut::LOBBY_RANDOM_TOWN_VS      },
+		{"mapsSizeS",                EShortcut::MAPS_SIZE_S               },
+		{"mapsSizeM",                EShortcut::MAPS_SIZE_M               },
+		{"mapsSizeL",                EShortcut::MAPS_SIZE_L               },
+		{"mapsSizeXl",               EShortcut::MAPS_SIZE_XL              },
+		{"mapsSizeAll",              EShortcut::MAPS_SIZE_ALL             },
+		{"mapsSortPlayers",          EShortcut::MAPS_SORT_PLAYERS         },
+		{"mapsSortSize",             EShortcut::MAPS_SORT_SIZE            },
+		{"mapsSortFormat",           EShortcut::MAPS_SORT_FORMAT          },
+		{"mapsSortName",             EShortcut::MAPS_SORT_NAME            },
+		{"mapsSortVictory",          EShortcut::MAPS_SORT_VICTORY         },
+		{"mapsSortDefeat",           EShortcut::MAPS_SORT_DEFEAT          },
+		{"mapsSortMaps",             EShortcut::MAPS_SORT_MAPS            },
+		{"mapsSortChangedate",       EShortcut::MAPS_SORT_CHANGEDATE      },
+		{"settingsLoadGame",         EShortcut::SETTINGS_LOAD_GAME        },
+		{"settingsSaveGame",         EShortcut::SETTINGS_SAVE_GAME        },
+		{"settingsNewGame",          EShortcut::SETTINGS_NEW_GAME         },
+		{"settingsRestartGame",      EShortcut::SETTINGS_RESTART_GAME     },
+		{"settingsToMainMenu",       EShortcut::SETTINGS_TO_MAIN_MENU     },
+		{"settingsQuitGame",         EShortcut::SETTINGS_QUIT_GAME        },
+		{"adventureReplayTurn",      EShortcut::ADVENTURE_REPLAY_TURN     },
+		{"adventureNewGame",         EShortcut::ADVENTURE_NEW_GAME        },
+		{"battleOpenActiveUnit",     EShortcut::BATTLE_OPEN_ACTIVE_UNIT   },
+		{"battleOpenHoveredUnit",    EShortcut::BATTLE_OPEN_HOVERED_UNIT  },
+		{"marketDeal",               EShortcut::MARKET_DEAL               },
+		{"marketMaxAmount",          EShortcut::MARKET_MAX_AMOUNT         },
+		{"marketSacrificeAll",       EShortcut::MARKET_SACRIFICE_ALL      },
+		{"marketSacrificeBackpack",  EShortcut::MARKET_SACRIFICE_BACKPACK },
+		{"marketResourcePlayer",     EShortcut::MARKET_RESOURCE_PLAYER    },
+		{"marketArtifactResource",   EShortcut::MARKET_ARTIFACT_RESOURCE  },
+		{"marketResourceArtifact",   EShortcut::MARKET_RESOURCE_ARTIFACT  },
+		{"marketCreatureResource",   EShortcut::MARKET_CREATURE_RESOURCE  },
+		{"marketResourceResource",   EShortcut::MARKET_RESOURCE_RESOURCE  },
+		{"marketCreatureExperience", EShortcut::MARKET_CREATURE_EXPERIENCE },
+		{"marketArtifactExperience", EShortcut::MARKET_ARTIFACT_EXPERIENCE },
+		{"townOpenHall",             EShortcut::TOWN_OPEN_HALL            },
+		{"townOpenFort",             EShortcut::TOWN_OPEN_FORT            },
+		{"townOpenMarket",           EShortcut::TOWN_OPEN_MARKET          },
+		{"townOpenMageGuild",        EShortcut::TOWN_OPEN_MAGE_GUILD      },
+		{"townOpenThievesGuild",     EShortcut::TOWN_OPEN_THIEVES_GUILD   },
+		{"townOpenRecruitment",      EShortcut::TOWN_OPEN_RECRUITMENT     },
+		{"townOpenHeroExchange",     EShortcut::TOWN_OPEN_HERO_EXCHANGE   },
+		{"townOpenHero",             EShortcut::TOWN_OPEN_HERO            },
+		{"townOpenVisitingHero",     EShortcut::TOWN_OPEN_VISITING_HERO   },
+		{"townOpenGarrisonedHero",   EShortcut::TOWN_OPEN_GARRISONED_HERO },
+		{"recruitmentSwitchLevel",   EShortcut::RECRUITMENT_SWITCH_LEVEL  },
+		{"heroArmySplit",            EShortcut::HERO_ARMY_SPLIT           },
+		{"heroBackpack",             EShortcut::HERO_BACKPACK             },
+		{"exchangeArmyToLeft",       EShortcut::EXCHANGE_ARMY_TO_LEFT     },
+		{"exchangeArmyToRight",      EShortcut::EXCHANGE_ARMY_TO_RIGHT    },
+		{"exchangeArmySwap",         EShortcut::EXCHANGE_ARMY_SWAP        },
+		{"exchangeArtifactsToLeft",  EShortcut::EXCHANGE_ARTIFACTS_TO_LEFT },
+		{"exchangeArtifactsToRight", EShortcut::EXCHANGE_ARTIFACTS_TO_RIGHT },
+		{"exchangeArtifactsSwap",    EShortcut::EXCHANGE_ARTIFACTS_SWAP   },
+		{"exchangeBackpackLeft",     EShortcut::EXCHANGE_BACKPACK_LEFT    },
+		{"exchangeBackpackRight",    EShortcut::EXCHANGE_BACKPACK_RIGHT   },
+		{"exchangeEquippedToLeft",   EShortcut::EXCHANGE_EQUIPPED_TO_LEFT },
+		{"exchangeEquippedToRight",  EShortcut::EXCHANGE_EQUIPPED_TO_RIGHT},
+		{"exchangeEquippedSwap",     EShortcut::EXCHANGE_EQUIPPED_SWAP    },
+		{"exchangeBackpackToLeft",   EShortcut::EXCHANGE_BACKPACK_TO_LEFT },
+		{"exchangeBackpackToRight",  EShortcut::EXCHANGE_BACKPACK_TO_RIGHT},
+		{"exchangeBackpackSwap",     EShortcut::EXCHANGE_BACKPACK_SWAP    },
 	};
 
+#ifndef ENABLE_GOLDMASTER
+	std::vector<EShortcut> assignedShortcuts;
+	std::vector<EShortcut> missingShortcuts;
+	for (auto const & entry : shortcutNames)
+		assignedShortcuts.push_back(entry.second);
+
+	for (EShortcut id = vstd::next(EShortcut::NONE, 1); id < EShortcut::AFTER_LAST; id = vstd::next(id, 1))
+		if (!vstd::contains(assignedShortcuts, id))
+			missingShortcuts.push_back(id);
+
+	if (!missingShortcuts.empty())
+		logGlobal->error("Found %d shortcuts without assigned string name!", missingShortcuts.size());
+#endif
+
 	if (shortcutNames.count(identifier))
 		return shortcutNames.at(identifier);
 	return EShortcut::NONE;

+ 17 - 5
client/lobby/CBonusSelection.cpp

@@ -70,9 +70,18 @@ CBonusSelection::CBonusSelection()
 
 	panelBackground = std::make_shared<CPicture>(ImagePath::builtin("CAMPBRF.BMP"), 456, 6);
 
-	buttonStart = std::make_shared<CButton>(Point(475, 536), AnimationPath::builtin("CBBEGIB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::startMap, this), EShortcut::GLOBAL_ACCEPT);
+	const auto & playVideo = [this]()
+	{
+		GH.windows().createAndPushWindow<CPrologEpilogVideo>(
+			getCampaign()->scenario(CSH->campaignMap).prolog,
+			[this]() { redraw(); } );
+	};
+
+	buttonStart = std::make_shared<CButton>(
+		Point(475, 536), AnimationPath::builtin("CBBEGIB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::startMap, this), EShortcut::GLOBAL_ACCEPT
+		);
 	buttonRestart = std::make_shared<CButton>(Point(475, 536), AnimationPath::builtin("CBRESTB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::restartMap, this), EShortcut::GLOBAL_ACCEPT);
-	buttonVideo = std::make_shared<CButton>(Point(705, 214), AnimationPath::builtin("CBVIDEB.DEF"), CButton::tooltip(), [this](){ GH.windows().createAndPushWindow<CPrologEpilogVideo>(getCampaign()->scenario(CSH->campaignMap).prolog, [this](){ redraw(); }); });
+	buttonVideo = std::make_shared<CButton>(Point(705, 214), AnimationPath::builtin("CBVIDEB.DEF"), CButton::tooltip(), playVideo, EShortcut::LOBBY_REPLAY_VIDEO);
 	buttonBack = std::make_shared<CButton>(Point(624, 536), AnimationPath::builtin("CBCANCB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::goBack, this), EShortcut::GLOBAL_CANCEL);
 
 	campaignName = std::make_shared<CLabel>(481, 28, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->si->getCampaignName());
@@ -103,8 +112,11 @@ CBonusSelection::CBonusSelection()
 
 	if(getCampaign()->playerSelectedDifficulty())
 	{
-		buttonDifficultyLeft = std::make_shared<CButton>(settings["general"]["enableUiEnhancements"].Bool() ? Point(693, 495) : Point(694, 508), AnimationPath::builtin("SCNRBLF.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::decreaseDifficulty, this));
-		buttonDifficultyRight = std::make_shared<CButton>(settings["general"]["enableUiEnhancements"].Bool() ? Point(739, 495) : Point(738, 508), AnimationPath::builtin("SCNRBRT.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::increaseDifficulty, this));
+		Point posLeft = settings["general"]["enableUiEnhancements"].Bool() ? Point(693, 495) : Point(694, 508);
+		Point posRight = settings["general"]["enableUiEnhancements"].Bool() ? Point(739, 495) : Point(738, 508);
+
+		buttonDifficultyLeft = std::make_shared<CButton>(posLeft, AnimationPath::builtin("SCNRBLF.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::decreaseDifficulty, this), EShortcut::MOVE_LEFT);
+		buttonDifficultyRight = std::make_shared<CButton>(posRight, AnimationPath::builtin("SCNRBRT.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::increaseDifficulty, this), EShortcut::MOVE_RIGHT);
 	}
 
 	for(auto scenarioID : getCampaign()->allScenarios())
@@ -124,7 +136,7 @@ CBonusSelection::CBonusSelection()
 		tabExtraOptions->recActions = UPDATE | SHOWALL | LCLICK | RCLICK_POPUP;
 		tabExtraOptions->recreate(true);
 		tabExtraOptions->setEnabled(false);
-		buttonExtraOptions = std::make_shared<CButton>(Point(643, 431), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], [this]{ tabExtraOptions->setEnabled(!tabExtraOptions->isActive()); GH.windows().totalRedraw(); }, EShortcut::NONE);
+		buttonExtraOptions = std::make_shared<CButton>(Point(643, 431), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], [this]{ tabExtraOptions->setEnabled(!tabExtraOptions->isActive()); GH.windows().totalRedraw(); }, EShortcut::LOBBY_EXTRA_OPTIONS);
 		buttonExtraOptions->setTextOverlay(CGI->generaltexth->translate("vcmi.optionsTab.extraOptions.hover"), FONT_SMALL, Colors::WHITE);
 	}
 }

+ 3 - 3
client/lobby/CLobbyScreen.cpp

@@ -57,12 +57,12 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType)
 		buttonOptions = std::make_shared<CButton>(Point(411, 510), AnimationPath::builtin("GSPBUTT.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabOpt), EShortcut::LOBBY_ADDITIONAL_OPTIONS);
 		if(settings["general"]["enableUiEnhancements"].Bool())
 		{
-			buttonTurnOptions = std::make_shared<CButton>(Point(619, 105), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabTurnOptions), EShortcut::NONE);
-			buttonExtraOptions = std::make_shared<CButton>(Point(619, 510), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabExtraOptions), EShortcut::NONE);
+			buttonTurnOptions = std::make_shared<CButton>(Point(619, 105), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabTurnOptions), EShortcut::LOBBY_TURN_OPTIONS);
+			buttonExtraOptions = std::make_shared<CButton>(Point(619, 510), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabExtraOptions), EShortcut::LOBBY_EXTRA_OPTIONS);
 		}
 	};
 
-	buttonChat = std::make_shared<CButton>(Point(619, 80), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[48], std::bind(&CLobbyScreen::toggleChat, this), EShortcut::LOBBY_HIDE_CHAT);
+	buttonChat = std::make_shared<CButton>(Point(619, 80), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[48], std::bind(&CLobbyScreen::toggleChat, this), EShortcut::LOBBY_TOGGLE_CHAT);
 	buttonChat->setTextOverlay(CGI->generaltexth->allTexts[532], FONT_SMALL, Colors::WHITE);
 
 	switch(screenType)

+ 7 - 6
client/lobby/CSelectionBase.cpp

@@ -32,6 +32,7 @@
 #include "../widgets/CTextInput.h"
 #include "../widgets/GraphicalPrimitiveCanvas.h"
 #include "../widgets/Images.h"
+#include "../widgets/MiscWidgets.h"
 #include "../widgets/ObjectLists.h"
 #include "../widgets/Slider.h"
 #include "../widgets/TextControls.h"
@@ -142,8 +143,8 @@ InfoCard::InfoCard()
 	chat = std::make_shared<CChatBox>(Rect(18, 126, 335, 143));
 	pvpBox = std::make_shared<PvPBox>(Rect(17, 396, 338, 105));
 
-	buttonInvitePlayers = std::make_shared<CButton>(Point(20, 365), AnimationPath::builtin("pregameInvitePlayers"), CGI->generaltexth->zelp[105], [](){ CSH->getGlobalLobby().activateRoomInviteInterface(); } );
-	buttonOpenGlobalLobby = std::make_shared<CButton>(Point(188, 365), AnimationPath::builtin("pregameReturnToLobby"), CGI->generaltexth->zelp[105], [](){ CSH->getGlobalLobby().activateInterface(); });
+	buttonInvitePlayers = std::make_shared<CButton>(Point(20, 365), AnimationPath::builtin("pregameInvitePlayers"), CGI->generaltexth->zelp[105], [](){ CSH->getGlobalLobby().activateRoomInviteInterface(); }, EShortcut::LOBBY_INVITE_PLAYERS );
+	buttonOpenGlobalLobby = std::make_shared<CButton>(Point(188, 365), AnimationPath::builtin("pregameReturnToLobby"), CGI->generaltexth->zelp[105], [](){ CSH->getGlobalLobby().activateInterface(); }, EShortcut::MAIN_MENU_LOBBY );
 
 	buttonInvitePlayers->setTextOverlay  (MetaString::createFromTextID("vcmi.lobby.invite.header").toString(), EFonts::FONT_SMALL, Colors::WHITE);
 	buttonOpenGlobalLobby->setTextOverlay(MetaString::createFromTextID("vcmi.lobby.backToLobby").toString(), EFonts::FONT_SMALL, Colors::WHITE);
@@ -417,7 +418,7 @@ PvPBox::PvPBox(const Rect & rect)
 		LobbyPvPAction lpa;
 		lpa.action = LobbyPvPAction::COIN;
 		CSH->sendLobbyPack(lpa);
-	}, EShortcut::NONE);
+	}, EShortcut::LOBBY_FLIP_COIN);
 	buttonFlipCoin->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.pvp.coin.hover"), EFonts::FONT_SMALL, Colors::WHITE);
 
 	buttonRandomTown = std::make_shared<CButton>(Point(190, 31), AnimationPath::builtin("GSPBUT2.DEF"), CButton::tooltip("", CGI->generaltexth->translate("vcmi.lobby.pvp.randomTown.help")), [getBannedTowns](){
@@ -425,7 +426,7 @@ PvPBox::PvPBox(const Rect & rect)
 		lpa.action = LobbyPvPAction::RANDOM_TOWN;
 		lpa.bannedTowns = getBannedTowns();
 		CSH->sendLobbyPack(lpa);
-	}, EShortcut::NONE);
+	}, EShortcut::LOBBY_RANDOM_TOWN);
 	buttonRandomTown->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.pvp.randomTown.hover"), EFonts::FONT_SMALL, Colors::WHITE);
 
 	buttonRandomTownVs = std::make_shared<CButton>(Point(190, 56), AnimationPath::builtin("GSPBUT2.DEF"), CButton::tooltip("", CGI->generaltexth->translate("vcmi.lobby.pvp.randomTownVs.help")), [getBannedTowns](){
@@ -433,7 +434,7 @@ PvPBox::PvPBox(const Rect & rect)
 		lpa.action = LobbyPvPAction::RANDOM_TOWN_VS;
 		lpa.bannedTowns = getBannedTowns();
 		CSH->sendLobbyPack(lpa);
-	}, EShortcut::NONE);
+	}, EShortcut::LOBBY_RANDOM_TOWN_VS);
 	buttonRandomTownVs->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.pvp.randomTownVs.hover"), EFonts::FONT_SMALL, Colors::WHITE);
 }
 
@@ -444,7 +445,7 @@ TownSelector::TownSelector(const Point & loc)
 	setRedrawParent(true);
 
 	int count = 0;
-	for(auto const & factionID : VLC->townh->getDefaultAllowed())
+	for(auto const & factionID : CGI->townh->getDefaultAllowed())
 	{
 		townsEnabled[factionID] = true;
 		count++;

+ 35 - 16
client/lobby/SelectionTab.cpp

@@ -169,24 +169,23 @@ SelectionTab::SelectionTab(ESelectionScreen Type)
 		labelMapSizes = std::make_shared<CLabel>(87, 62, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[510]);
 
 		// TODO: Global constants?
-		int sizes[] = {CMapHeader::MAP_SIZE_SMALL,
-						CMapHeader::MAP_SIZE_MIDDLE,
-						CMapHeader::MAP_SIZE_LARGE,
-						CMapHeader::MAP_SIZE_XLARGE,
-						0};
-		const char * filterIconNmes[] = {"SCSMBUT.DEF", "SCMDBUT.DEF", "SCLGBUT.DEF", "SCXLBUT.DEF", "SCALBUT.DEF"};
+		constexpr std::array sizes = {CMapHeader::MAP_SIZE_SMALL, CMapHeader::MAP_SIZE_MIDDLE, CMapHeader::MAP_SIZE_LARGE, CMapHeader::MAP_SIZE_XLARGE, 0};
+		constexpr std::array filterIconNmes = {"SCSMBUT.DEF", "SCMDBUT.DEF", "SCLGBUT.DEF", "SCXLBUT.DEF", "SCALBUT.DEF"};
+		constexpr std::array filterShortcuts = { EShortcut::MAPS_SIZE_S, EShortcut::MAPS_SIZE_M, EShortcut::MAPS_SIZE_L, EShortcut::MAPS_SIZE_XL, EShortcut::MAPS_SIZE_ALL };
+
 		for(int i = 0; i < 5; i++)
-			buttonsSortBy.push_back(std::make_shared<CButton>(Point(158 + 47 * i, 46), AnimationPath::builtin(filterIconNmes[i]), CGI->generaltexth->zelp[54 + i], std::bind(&SelectionTab::filter, this, sizes[i], true)));
+			buttonsSortBy.push_back(std::make_shared<CButton>(Point(158 + 47 * i, 46), AnimationPath::builtin(filterIconNmes[i]), CGI->generaltexth->zelp[54 + i], std::bind(&SelectionTab::filter, this, sizes[i], true), filterShortcuts[i]));
 
-		int xpos[] = {23, 55, 88, 121, 306, 339};
-		const char * sortIconNames[] = {"SCBUTT1.DEF", "SCBUTT2.DEF", "SCBUTCP.DEF", "SCBUTT3.DEF", "SCBUTT4.DEF", "SCBUTT5.DEF"};
+		constexpr std::array xpos = {23, 55, 88, 121, 306, 339};
+		constexpr std::array sortIconNames = {"SCBUTT1.DEF", "SCBUTT2.DEF", "SCBUTCP.DEF", "SCBUTT3.DEF", "SCBUTT4.DEF", "SCBUTT5.DEF"};
+		constexpr std::array sortShortcuts = { EShortcut::MAPS_SORT_PLAYERS, EShortcut::MAPS_SORT_SIZE, EShortcut::MAPS_SORT_FORMAT, EShortcut::MAPS_SORT_NAME, EShortcut::MAPS_SORT_VICTORY, EShortcut::MAPS_SORT_DEFEAT };
 		for(int i = 0; i < 6; i++)
 		{
 			ESortBy criteria = (ESortBy)i;
 			if(criteria == _name)
 				criteria = generalSortingBy;
 
-			buttonsSortBy.push_back(std::make_shared<CButton>(Point(xpos[i], 86), AnimationPath::builtin(sortIconNames[i]), CGI->generaltexth->zelp[107 + i], std::bind(&SelectionTab::sortBy, this, criteria)));
+			buttonsSortBy.push_back(std::make_shared<CButton>(Point(xpos[i], 86), AnimationPath::builtin(sortIconNames[i]), CGI->generaltexth->zelp[107 + i], std::bind(&SelectionTab::sortBy, this, criteria), sortShortcuts[i]));
 		}
 	}
 
@@ -212,8 +211,8 @@ SelectionTab::SelectionTab(ESelectionScreen Type)
 		pos.x += 3;
 		pos.y += 6;
 
-		buttonsSortBy.push_back(std::make_shared<CButton>(Point(23, 86), AnimationPath::builtin("CamCusM.DEF"), CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _numOfMaps)));
-		buttonsSortBy.push_back(std::make_shared<CButton>(Point(55, 86), AnimationPath::builtin("CamCusL.DEF"), CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _name)));
+		buttonsSortBy.push_back(std::make_shared<CButton>(Point(23, 86), AnimationPath::builtin("CamCusM.DEF"), CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _numOfMaps), EShortcut::MAPS_SORT_MAPS));
+		buttonsSortBy.push_back(std::make_shared<CButton>(Point(55, 86), AnimationPath::builtin("CamCusL.DEF"), CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _name), EShortcut::MAPS_SORT_NAME));
 		break;
 	default:
 		assert(0);
@@ -222,7 +221,7 @@ SelectionTab::SelectionTab(ESelectionScreen Type)
 
 	if(enableUiEnhancements)
 	{
-		auto sortByDate = std::make_shared<CButton>(Point(371, 85), AnimationPath::builtin("selectionTabSortDate"), CButton::tooltip("", CGI->generaltexth->translate("vcmi.lobby.sortDate")), std::bind(&SelectionTab::sortBy, this, ESortBy::_changeDate));
+		auto sortByDate = std::make_shared<CButton>(Point(371, 85), AnimationPath::builtin("selectionTabSortDate"), CButton::tooltip("", CGI->generaltexth->translate("vcmi.lobby.sortDate")), std::bind(&SelectionTab::sortBy, this, ESortBy::_changeDate), EShortcut::MAPS_SORT_CHANGEDATE);
 		sortByDate->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("lobby/selectionTabSortDate")));
 		buttonsSortBy.push_back(sortByDate);
 	}
@@ -677,6 +676,8 @@ void SelectionTab::selectFileName(std::string fname)
 		}
 	}
 
+	filter(-1);
+
 	for(int i = (int)curItems.size() - 1; i >= 0; i--)
 	{
 		if(curItems[i]->fileURI == fname)
@@ -687,13 +688,25 @@ void SelectionTab::selectFileName(std::string fname)
 		}
 	}
 
-	filter(-1);
 	selectAbs(-1);
 
 	if(tabType == ESelectionScreen::saveGame && inputName->getText().empty())
 		inputName->setText("NEWGAME");
 }
 
+void SelectionTab::selectNewestFile()
+{
+	time_t newestTime = 0;
+	std::string newestFile = "";
+	for(int i = (int)allItems.size() - 1; i >= 0; i--)
+		if(allItems[i]->lastWrite > newestTime)
+		{
+			newestTime = allItems[i]->lastWrite;
+			newestFile = allItems[i]->fileURI;
+		}
+	selectFileName(newestFile);
+}
+
 std::shared_ptr<ElementInfo> SelectionTab::getSelectedMapInfo() const
 {
 	return curItems.empty() || curItems[selectionPos]->isFolder ? nullptr : curItems[selectionPos];
@@ -733,6 +746,8 @@ void SelectionTab::restoreLastSelection()
 		selectFileName(settings["general"]["lastCampaign"].String());
 		break;
 	case ESelectionScreen::loadGame:
+		selectNewestFile();
+		break;
 	case ESelectionScreen::saveGame:
 		selectFileName(settings["general"]["lastSave"].String());
 	}
@@ -795,7 +810,7 @@ void SelectionTab::parseSaves(const std::unordered_set<ResourcePath> & files)
 			switch(CSH->getLoadMode())
 			{
 			case ELoadMode::SINGLE:
-				if(isMultiplayer || isCampaign || isTutorial)
+				if(isCampaign || isTutorial)
 					mapInfo->mapHeader.reset();
 				break;
 			case ELoadMode::CAMPAIGN:
@@ -806,10 +821,14 @@ void SelectionTab::parseSaves(const std::unordered_set<ResourcePath> & files)
 				if(!isTutorial)
 					mapInfo->mapHeader.reset();
 				break;
-			default:
+			case ELoadMode::MULTI:
 				if(!isMultiplayer)
 					mapInfo->mapHeader.reset();
 				break;
+			default:
+				assert(0);
+				mapInfo->mapHeader.reset();
+				break;
 			}
 
 			allItems.push_back(mapInfo);

+ 1 - 0
client/lobby/SelectionTab.h

@@ -101,6 +101,7 @@ public:
 	int getLine() const;
 	int getLine(const Point & position) const;
 	void selectFileName(std::string fname);
+	void selectNewestFile();
 	std::shared_ptr<ElementInfo> getSelectedMapInfo() const;
 	void rememberCurrentSelection();
 	void restoreLastSelection();

+ 10 - 7
client/mainmenu/CHighScoreScreen.cpp

@@ -24,6 +24,7 @@
 #include "../windows/InfoWindows.h"
 #include "../widgets/TextControls.h"
 #include "../render/Canvas.h"
+#include "../render/IRenderHandler.h"
 
 #include "../CGameInfo.h"
 #include "../../lib/CGeneralTextHandler.h"
@@ -82,7 +83,8 @@ CHighScoreScreen::CHighScoreScreen(HighScorePage highscorepage, int highlighted)
 
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
 	pos = center(Rect(0, 0, 800, 600));
-	updateShadow();
+
+	backgroundAroundMenu = std::make_shared<CFilledTexture>(ImagePath::builtin("DIBOXBCK"), Rect(-pos.x, -pos.y, GH.screenDimensions().x, GH.screenDimensions().y));
 
 	addHighScores();
 	addButtons();
@@ -110,10 +112,10 @@ void CHighScoreScreen::addButtons()
 	
 	buttons.clear();
 
-	buttons.push_back(std::make_shared<CButton>(Point(31, 113), AnimationPath::builtin("HISCCAM.DEF"), CButton::tooltip(), [&](){ buttonCampaignClick(); }));
-	buttons.push_back(std::make_shared<CButton>(Point(31, 345), AnimationPath::builtin("HISCSTA.DEF"), CButton::tooltip(), [&](){ buttonScenarioClick(); }));
-	buttons.push_back(std::make_shared<CButton>(Point(726, 113), AnimationPath::builtin("HISCRES.DEF"), CButton::tooltip(), [&](){ buttonResetClick(); }));
-	buttons.push_back(std::make_shared<CButton>(Point(726, 345), AnimationPath::builtin("HISCEXT.DEF"), CButton::tooltip(), [&](){ buttonExitClick(); }));
+	buttons.push_back(std::make_shared<CButton>(Point(31, 113), AnimationPath::builtin("HISCCAM.DEF"), CButton::tooltip(), [&](){ buttonCampaignClick(); }, EShortcut::HIGH_SCORES_CAMPAIGNS));
+	buttons.push_back(std::make_shared<CButton>(Point(31, 345), AnimationPath::builtin("HISCSTA.DEF"), CButton::tooltip(), [&](){ buttonScenarioClick(); }, EShortcut::HIGH_SCORES_SCENARIOS));
+	buttons.push_back(std::make_shared<CButton>(Point(726, 113), AnimationPath::builtin("HISCRES.DEF"), CButton::tooltip(), [&](){ buttonResetClick(); }, EShortcut::HIGH_SCORES_RESET));
+	buttons.push_back(std::make_shared<CButton>(Point(726, 345), AnimationPath::builtin("HISCEXT.DEF"), CButton::tooltip(), [&](){ buttonExitClick(); }, EShortcut::GLOBAL_RETURN));
 }
 
 void CHighScoreScreen::addHighScores()
@@ -223,8 +225,8 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc
 
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
 	pos = center(Rect(0, 0, 800, 600));
-	updateShadow();
 
+	backgroundAroundMenu = std::make_shared<CFilledTexture>(ImagePath::builtin("DIBOXBCK"), Rect(-pos.x, -pos.y, GH.screenDimensions().x, GH.screenDimensions().y));
 	background = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), Colors::BLACK);
 
 	if(won)
@@ -341,7 +343,8 @@ CHighScoreInput::CHighScoreInput(std::string playerName, std::function<void(std:
 
 	buttonOk = std::make_shared<CButton>(Point(26, 142), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CHighScoreInput::okay, this), EShortcut::GLOBAL_ACCEPT);
 	buttonCancel = std::make_shared<CButton>(Point(142, 142), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[561], std::bind(&CHighScoreInput::abort, this), EShortcut::GLOBAL_CANCEL);
-	statusBar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(7, 186, 218, 18), 7, 186));
+// FIXME: broken. Never activates?
+//	statusBar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(7, 186, 218, 18), 7, 186));
 	textInput = std::make_shared<CTextInput>(Rect(18, 104, 200, 25), FONT_SMALL, ETextAlignment::CENTER, true);
 	textInput->setText(playerName);
 }

+ 3 - 0
client/mainmenu/CHighScoreScreen.h

@@ -16,6 +16,7 @@ class CMultiLineLabel;
 class CAnimImage;
 class CTextInput;
 class VideoWidgetBase;
+class CFilledTexture;
 
 class TransparentFilledRectangle;
 
@@ -62,6 +63,7 @@ private:
 	HighScorePage highscorepage;
 
 	std::shared_ptr<CPicture> background;
+	std::shared_ptr<CFilledTexture> backgroundAroundMenu;
 	std::vector<std::shared_ptr<CButton>> buttons;
 	std::vector<std::shared_ptr<CLabel>> texts;
 	std::vector<std::shared_ptr<CAnimImage>> images;
@@ -95,6 +97,7 @@ class CHighScoreInputScreen : public CWindowObject
 	std::shared_ptr<CHighScoreInput> input;
 	std::shared_ptr<TransparentFilledRectangle> background;
 	std::shared_ptr<VideoWidgetBase> videoPlayer;
+	std::shared_ptr<CFilledTexture> backgroundAroundMenu;
 
 	bool won;
 	HighScoreCalculation calc;

+ 6 - 6
client/mainmenu/CMainMenu.cpp

@@ -58,7 +58,6 @@
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/GameConstants.h"
 #include "../../lib/CRandomGenerator.h"
-#include "../../lib/CondSh.h"
 
 std::shared_ptr<CMainMenu> CMM;
 ISelectionScreenInfo * SEL;
@@ -265,7 +264,8 @@ CMainMenuConfig::CMainMenuConfig()
 	: campaignSets(JsonPath::builtin("config/campaignSets.json"))
 	, config(JsonPath::builtin("config/mainmenu.json"))
 {
-
+	if (config["game-select"].Vector().empty())
+		handleFatalError("Main menu config is invalid or corrupted. Please disable any mods or reinstall VCMI", false);
 }
 
 const CMainMenuConfig & CMainMenuConfig::get()
@@ -447,11 +447,11 @@ CMultiMode::CMultiMode(ESelectionScreen ScreenType)
 	playerName->setText(getPlayerName());
 	playerName->setCallback(std::bind(&CMultiMode::onNameChange, this, _1));
 
-	buttonHotseat = std::make_shared<CButton>(Point(373, 78 + 57 * 0), AnimationPath::builtin("MUBHOT.DEF"), CGI->generaltexth->zelp[266], std::bind(&CMultiMode::hostTCP, this));
-	buttonLobby = std::make_shared<CButton>(Point(373, 78 + 57 * 1), AnimationPath::builtin("MUBONL.DEF"), CGI->generaltexth->zelp[265], std::bind(&CMultiMode::openLobby, this));
+	buttonHotseat = std::make_shared<CButton>(Point(373, 78 + 57 * 0), AnimationPath::builtin("MUBHOT.DEF"), CGI->generaltexth->zelp[266], std::bind(&CMultiMode::hostTCP, this), EShortcut::MAIN_MENU_HOTSEAT);
+	buttonLobby = std::make_shared<CButton>(Point(373, 78 + 57 * 1), AnimationPath::builtin("MUBONL.DEF"), CGI->generaltexth->zelp[265], std::bind(&CMultiMode::openLobby, this), EShortcut::MAIN_MENU_LOBBY);
 
-	buttonHost = std::make_shared<CButton>(Point(373, 78 + 57 * 3), AnimationPath::builtin("MUBHOST.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.hostTCP"), ""), std::bind(&CMultiMode::hostTCP, this));
-	buttonJoin = std::make_shared<CButton>(Point(373, 78 + 57 * 4), AnimationPath::builtin("MUBJOIN.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.joinTCP"), ""), std::bind(&CMultiMode::joinTCP, this));
+	buttonHost = std::make_shared<CButton>(Point(373, 78 + 57 * 3), AnimationPath::builtin("MUBHOST.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.hostTCP"), ""), std::bind(&CMultiMode::hostTCP, this), EShortcut::MAIN_MENU_HOST_GAME);
+	buttonJoin = std::make_shared<CButton>(Point(373, 78 + 57 * 4), AnimationPath::builtin("MUBJOIN.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.joinTCP"), ""), std::bind(&CMultiMode::joinTCP, this), EShortcut::MAIN_MENU_JOIN_GAME);
 
 	buttonCancel = std::make_shared<CButton>(Point(373, 424), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[288], [=](){ close();}, EShortcut::GLOBAL_CANCEL);
 }

+ 3 - 1
client/mainmenu/CPrologEpilogVideo.cpp

@@ -19,6 +19,7 @@
 //#include "../gui/FramerateManager.h"
 #include "../widgets/TextControls.h"
 #include "../widgets/VideoWidget.h"
+#include "../widgets/Images.h"
 #include "../render/Canvas.h"
 
 CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::function<void()> callback)
@@ -27,7 +28,8 @@ CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::f
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
 	addUsedEvents(LCLICK | TIME);
 	pos = center(Rect(0, 0, 800, 600));
-	updateShadow();
+
+	backgroundAroundMenu = std::make_shared<CFilledTexture>(ImagePath::builtin("DIBOXBCK"), Rect(-pos.x, -pos.y, GH.screenDimensions().x, GH.screenDimensions().y));
 
 	//TODO: remove hardcoded paths. Some of campaigns video actually consist from 2 parts
 	// however, currently our campaigns format expects only	a single video file

+ 2 - 0
client/mainmenu/CPrologEpilogVideo.h

@@ -14,6 +14,7 @@
 
 class CMultiLineLabel;
 class VideoWidget;
+class CFilledTexture;
 
 class CPrologEpilogVideo : public CWindowObject
 {
@@ -27,6 +28,7 @@ class CPrologEpilogVideo : public CWindowObject
 
 	std::shared_ptr<CMultiLineLabel> text;
 	std::shared_ptr<VideoWidget> videoPlayer;
+	std::shared_ptr<CFilledTexture> backgroundAroundMenu;
 
 	bool voiceStopped = false;
 

+ 2 - 0
client/mapView/IMapRendererObserver.h

@@ -25,6 +25,8 @@ public:
 	virtual ~IMapObjectObserver();
 
 	virtual bool hasOngoingAnimations() = 0;
+	virtual void waitForOngoingAnimations(){};
+	virtual void endNetwork(){};
 
 	/// Plays fade-in animation and adds object to map
 	virtual void onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) = 0;

+ 8 - 8
client/mapView/MapRenderer.cpp

@@ -664,15 +664,15 @@ size_t MapRendererPath::selectImageArrow(bool reachableToday, const int3 & curr,
 	// is (directionToArrowIndex[7][5])
 	//
 	const static size_t directionToArrowIndex[9][9] = {
-		{16, 17, 18, 7,  0, 19, 6,  5,  0 },
-		{8,  9,  18, 7,  0, 19, 6,  0,  20},
-		{8,  1,  10, 7,  0, 19, 0,  21, 20},
-		{24, 17, 18, 15, 0, 0,  6,  5,  4 },
+		{16, 17, 18, 7,  0, 19, 6,  5,  12},
+		{8,  9,  18, 7,  0, 19, 6,  13, 20},
+		{8,  1,  10, 7,  0, 19, 14, 21, 20},
+		{24, 17, 18, 15, 0, 11, 6,  5,  4 },
 		{0,  0,  0,  0,  0, 0,  0,  0,  0 },
-		{8,  1,  2,  0,  0, 11, 22, 21, 20},
-		{24, 17, 0,  23, 0, 3,  14, 5,  4 },
-		{24, 0,  2,  23, 0, 3,  22, 13, 4 },
-		{0,  1,  2,  23, 0, 3,  22, 21, 12}
+		{8,  1,  2,  15, 0, 11, 22, 21, 20},
+		{24, 17, 10, 23, 0, 3,  14, 5,  4 },
+		{24, 9,  2,  23, 0, 3,  22, 13, 4 },
+		{16, 1,  2,  23, 0, 3,  22, 21, 12}
 	};
 
 	size_t enterDirection = (curr.x - next.x + 1) + 3 * (curr.y - next.y + 1);

+ 2 - 2
client/mapView/MapView.cpp

@@ -239,9 +239,9 @@ void MapView::onViewWorldActivated(uint32_t tileSize)
 	controller->setTileSize(Point(tileSize, tileSize));
 }
 
-void MapView::onMapZoomLevelChanged(int stepsChange)
+void MapView::onMapZoomLevelChanged(int stepsChange, bool useDeadZone)
 {
-	controller->modifyTileSize(stepsChange);
+	controller->modifyTileSize(stepsChange, useDeadZone);
 }
 
 void MapView::onViewMapActivated()

+ 1 - 1
client/mapView/MapView.h

@@ -87,7 +87,7 @@ public:
 	void onViewWorldActivated(uint32_t tileSize);
 
 	/// Changes zoom level / tile size of current view by specified factor
-	void onMapZoomLevelChanged(int stepsChange);
+	void onMapZoomLevelChanged(int stepsChange, bool useDeadZone);
 
 	/// Switches view from View World mode back to standard view
 	void onViewMapActivated();

+ 2 - 2
client/mapView/MapViewActions.cpp

@@ -87,7 +87,7 @@ void MapViewActions::mouseMoved(const Point & cursorPosition, const Point & last
 
 void MapViewActions::wheelScrolled(int distance)
 {
-	adventureInt->hotkeyZoom(distance * 4);
+	adventureInt->hotkeyZoom(distance * 4, true);
 }
 
 void MapViewActions::mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance)
@@ -114,7 +114,7 @@ void MapViewActions::gesturePinch(const Point & centerPosition, double lastUpdat
 	int oldZoomSteps = std::round(std::log(pinchZoomFactor) / std::log(1.01));
 
 	if (newZoomSteps != oldZoomSteps)
-		adventureInt->hotkeyZoom(newZoomSteps - oldZoomSteps);
+		adventureInt->hotkeyZoom(newZoomSteps - oldZoomSteps, true);
 
 	pinchZoomFactor = newZoom;
 }

+ 25 - 5
client/mapView/MapViewController.cpp

@@ -25,6 +25,7 @@
 
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/StartInfo.h"
+#include "../../lib/UnlockGuard.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/mapObjects/MiscObjects.h"
 #include "../../lib/pathfinder/CGPathNode.h"
@@ -88,7 +89,7 @@ void MapViewController::setTileSize(const Point & tileSize)
 	setViewCenter(newViewCenter, model->getLevel());
 }
 
-void MapViewController::modifyTileSize(int stepsChange)
+void MapViewController::modifyTileSize(int stepsChange, bool useDeadZone)
 {
 	// we want to zoom in/out in fixed 10% steps, to allow player to return back to exactly 100% zoom just by scrolling
 	// so, zooming in for 5 steps will put game at 1.1^5 = 1.61 scale
@@ -117,10 +118,13 @@ void MapViewController::modifyTileSize(int stepsChange)
 	if (actualZoom != currentZoom)
 	{
 		targetTileSize = actualZoom;
-		if(actualZoom.x >= defaultTileSize - zoomTileDeadArea && actualZoom.x <= defaultTileSize + zoomTileDeadArea)
-			actualZoom.x = defaultTileSize;
-		if(actualZoom.y >= defaultTileSize - zoomTileDeadArea && actualZoom.y <= defaultTileSize + zoomTileDeadArea)
-			actualZoom.y = defaultTileSize;
+		if (useDeadZone)
+		{
+			if(actualZoom.x >= defaultTileSize - zoomTileDeadArea && actualZoom.x <= defaultTileSize + zoomTileDeadArea)
+				actualZoom.x = defaultTileSize;
+			if(actualZoom.y >= defaultTileSize - zoomTileDeadArea && actualZoom.y <= defaultTileSize + zoomTileDeadArea)
+				actualZoom.y = defaultTileSize;
+		}
 		
 		bool isInDeadZone = targetTileSize != actualZoom || actualZoom == Point(defaultTileSize, defaultTileSize);
 
@@ -346,6 +350,7 @@ bool MapViewController::isEventVisible(const CGHeroInstance * obj, const int3 &
 
 void MapViewController::fadeOutObject(const CGObjectInstance * obj)
 {
+	animationWait.setBusy();
 	logGlobal->debug("Starting fade out animation");
 	fadingOutContext = std::make_shared<MapRendererAdventureFadingContext>(*state);
 	fadingOutContext->animationTime = adventureContext->animationTime;
@@ -366,6 +371,7 @@ void MapViewController::fadeOutObject(const CGObjectInstance * obj)
 
 void MapViewController::fadeInObject(const CGObjectInstance * obj)
 {
+	animationWait.setBusy();
 	logGlobal->debug("Starting fade in animation");
 	fadingInContext = std::make_shared<MapRendererAdventureFadingContext>(*state);
 	fadingInContext->animationTime = adventureContext->animationTime;
@@ -505,6 +511,7 @@ void MapViewController::onAfterHeroTeleported(const CGHeroInstance * obj, const
 
 	if(isEventVisible(obj, from, dest))
 	{
+		animationWait.setBusy();
 		logGlobal->debug("Starting teleport animation");
 		teleportContext = std::make_shared<MapRendererAdventureTransitionContext>(*state);
 		teleportContext->animationTime = adventureContext->animationTime;
@@ -540,6 +547,7 @@ void MapViewController::onHeroMoved(const CGHeroInstance * obj, const int3 & fro
 
 	if(movementTime > 1)
 	{
+		animationWait.setBusy();
 		logGlobal->debug("Starting movement animation");
 		movementContext = std::make_shared<MapRendererAdventureMovingContext>(*state);
 		movementContext->animationTime = adventureContext->animationTime;
@@ -577,6 +585,17 @@ bool MapViewController::hasOngoingAnimations()
 	return false;
 }
 
+void MapViewController::waitForOngoingAnimations()
+{
+	auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex);
+	animationWait.waitWhileBusy();
+}
+
+void MapViewController::endNetwork()
+{
+	animationWait.requestTermination();
+}
+
 void MapViewController::activateAdventureContext(uint32_t animationTime)
 {
 	resetContext();
@@ -642,6 +661,7 @@ void MapViewController::resetContext()
 	worldViewContext.reset();
 	spellViewContext.reset();
 	puzzleMapContext.reset();
+	animationWait.setFree();
 }
 
 void MapViewController::setTerrainVisibility(bool showAllTerrain)

+ 8 - 2
client/mapView/MapViewController.h

@@ -10,6 +10,7 @@
 #pragma once
 
 #include "IMapRendererObserver.h"
+#include "../ConditionalWait.h"
 #include "../../lib/Point.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -34,6 +35,8 @@ class MapRendererPuzzleMapContext;
 /// such as its position and any animations
 class MapViewController : public IMapObjectObserver
 {
+	ConditionalWait animationWait;
+
 	std::shared_ptr<IMapRendererContext> context;
 	std::shared_ptr<MapRendererContextState> state;
 	std::shared_ptr<MapViewModel> model;
@@ -52,7 +55,7 @@ class MapViewController : public IMapObjectObserver
 
 private:
 	const int defaultTileSize = 32;
-	const int zoomTileDeadArea = 5;
+	const int zoomTileDeadArea = 4;
 	Point targetTileSize = Point(32, 32);
 	bool wasInDeadZone = true;
 
@@ -68,6 +71,9 @@ private:
 
 	// IMapObjectObserver impl
 	bool hasOngoingAnimations() override;
+	void waitForOngoingAnimations() override;
+	void endNetwork() override;
+
 	void onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) override;
 	void onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) override;
 	void onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) override;
@@ -91,7 +97,7 @@ public:
 	void setViewCenter(const int3 & position);
 	void setViewCenter(const Point & position, int level);
 	void setTileSize(const Point & tileSize);
-	void modifyTileSize(int stepsChange);
+	void modifyTileSize(int stepsChange, bool useDeadZone);
 	void tick(uint32_t timePassed);
 	void afterRender();
 

+ 9 - 4
client/mapView/mapHandler.cpp

@@ -19,7 +19,6 @@
 
 #include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/TerrainHandler.h"
-#include "../../lib/UnlockGuard.h"
 #include "../../lib/mapObjectConstructors/CObjectClassesHandler.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/mapObjects/ObjectTemplate.h"
@@ -36,13 +35,19 @@ bool CMapHandler::hasOngoingAnimations()
 
 void CMapHandler::waitForOngoingAnimations()
 {
-	while(CGI->mh->hasOngoingAnimations())
+	for(auto * observer : observers)
 	{
-		auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex);
-		boost::this_thread::sleep_for(boost::chrono::milliseconds(1));
+		if (observer->hasOngoingAnimations())
+			observer->waitForOngoingAnimations();
 	}
 }
 
+void CMapHandler::endNetwork()
+{
+	for(auto * observer : observers)
+		observer->endNetwork();
+}
+
 std::string CMapHandler::getTerrainDescr(const int3 & pos, bool rightClick) const
 {
 	const TerrainTile & t = map->getTile(pos);

+ 1 - 0
client/mapView/mapHandler.h

@@ -71,6 +71,7 @@ public:
 
 	/// blocking wait until all ongoing animatins are over
 	void waitForOngoingAnimations();
+	void endNetwork();
 
 	static bool compareObjectBlitOrder(const CGObjectInstance * a, const CGObjectInstance * b);
 };

+ 25 - 15
client/widgets/CArtPlace.cpp

@@ -140,11 +140,6 @@ void CCommanderArtPlace::showPopupWindow(const Point & cursorPosition)
 		CArtPlace::showPopupWindow(cursorPosition);
 }
 
-CHeroArtPlace::CHeroArtPlace(Point position, const CArtifactInstance * art)
-	: CArtPlace(position, art)
-{
-}
-
 void CArtPlace::lockSlot(bool on)
 {
 	if(locked == on)
@@ -219,20 +214,35 @@ void CArtPlace::setGestureCallback(const ClickFunctor & callback)
 	gestureCallback = callback;
 }
 
-void CHeroArtPlace::addCombinedArtInfo(const std::map<const CArtifact*, int> & arts)
+void CArtPlace::addCombinedArtInfo(const std::map<const ArtifactID, std::vector<ArtifactID>> & arts)
 {
-	for(const auto & combinedArt : arts)
+	for(const auto & availableArts : arts)
 	{
-		std::string artList;
-		text += "\n\n";
-		text += "{" + combinedArt.first->getNameTranslated() + "}";
-		if(arts.size() == 1)
+		const auto combinedArt = availableArts.first.toArtifact();
+		MetaString info;
+		info.appendEOL();
+		info.appendEOL();
+		info.appendRawString("{");
+		info.appendName(combinedArt->getId());
+		info.appendRawString("}");
+		info.appendRawString(" (%d/%d)");
+		info.replaceNumber(availableArts.second.size());
+		info.replaceNumber(combinedArt->getConstituents().size());
+		for(const auto part : combinedArt->getConstituents())
 		{
-			for(const auto part : combinedArt.first->getConstituents())
-				artList += "\n" + part->getNameTranslated();
+			info.appendEOL();
+			if(vstd::contains(availableArts.second, part->getId()))
+			{
+				info.appendName(part->getId());
+			}
+			else
+			{
+				info.appendRawString("{#A9A9A9|");
+				info.appendName(part->getId());
+				info.appendRawString("}");
+			}
 		}
-		text += " (" + boost::str(boost::format("%d") % combinedArt.second) + " / " +
-			boost::str(boost::format("%d") % combinedArt.first->getConstituents().size()) + ")" + artList;
+		text += info.toString();
 	}
 }
 

+ 1 - 7
client/widgets/CArtPlace.h

@@ -31,6 +31,7 @@ public:
 	void clickPressed(const Point & cursorPosition) override;
 	void showPopupWindow(const Point & cursorPosition) override;
 	void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override;
+	void addCombinedArtInfo(const std::map<const ArtifactID, std::vector<ArtifactID>> & arts);
 
 private:
 	const CArtifactInstance * ourArt;
@@ -59,13 +60,6 @@ public:
 	void showPopupWindow(const Point & cursorPosition) override;
 };
 
-class CHeroArtPlace: public CArtPlace
-{
-public:
-	CHeroArtPlace(Point position, const CArtifactInstance * art = nullptr);
-	void addCombinedArtInfo(const std::map<const CArtifact*, int> & arts);
-};
-
 namespace ArtifactUtilsClient
 {
 	bool askToAssemble(const CGHeroInstance * hero, const ArtifactPosition & slot);

+ 1 - 1
client/widgets/CArtifactsOfHeroBackpack.cpp

@@ -82,7 +82,7 @@ void CArtifactsOfHeroBackpack::initAOHbackpack(size_t slots, bool slider)
 		const auto pos = Point(slotSizeWithMargin * (artPlaceIdx % slotsColumnsMax),
 			slotSizeWithMargin * (artPlaceIdx / slotsColumnsMax));
 		backpackSlotsBackgrounds.emplace_back(std::make_shared<CPicture>(ImagePath::builtin("heroWindow/artifactSlotEmpty"), pos));
-		artPlace = std::make_shared<CHeroArtPlace>(pos);
+		artPlace = std::make_shared<CArtPlace>(pos);
 		artPlace->setArtifact(nullptr);
 		artPlace->setClickPressedCallback(std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2));
 		artPlace->setShowPopupCallback(std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2));

+ 13 - 13
client/widgets/CArtifactsOfHeroBase.cpp

@@ -55,12 +55,12 @@ void CArtifactsOfHeroBase::init(
 	pos += position;
 	for(int g = 0; g < ArtifactPosition::BACKPACK_START; g++)
 	{
-		artWorn[ArtifactPosition(g)] = std::make_shared<CHeroArtPlace>(slotPos[g]);
+		artWorn[ArtifactPosition(g)] = std::make_shared<CArtPlace>(slotPos[g]);
 	}
 	backpack.clear();
 	for(int s = 0; s < 5; s++)
 	{
-		auto artPlace = std::make_shared<CHeroArtPlace>(Point(403 + 46 * s, 365));
+		auto artPlace = std::make_shared<CArtPlace>(Point(403 + 46 * s, 365));
 		backpack.push_back(artPlace);
 	}
 	for(auto artPlace : artWorn)
@@ -256,21 +256,21 @@ void CArtifactsOfHeroBase::setSlotData(ArtPlacePtr artPlace, const ArtifactPosit
 	{
 		artPlace->lockSlot(slotInfo->locked);
 		artPlace->setArtifact(slotInfo->artifact);
-		if(!slotInfo->artifact->isCombined())
+		if(slotInfo->locked || slotInfo->artifact->isCombined())
+			return;
+
+		// If the artifact is part of at least one combined artifact, add additional information
+		std::map<const ArtifactID, std::vector<ArtifactID>> arts;
+		for(const auto combinedArt : slotInfo->artifact->artType->getPartOf())
 		{
-			// If the artifact is part of at least one combined artifact, add additional information
-			std::map<const CArtifact*, int> arts;
-			for(const auto combinedArt : slotInfo->artifact->artType->getPartOf())
+			arts.try_emplace(combinedArt->getId(), std::vector<ArtifactID>{});
+			for(const auto part : combinedArt->getConstituents())
 			{
-				arts.insert(std::pair(combinedArt, 0));
-				for(const auto part : combinedArt->getConstituents())
-				{
-					if(curHero->hasArt(part->getId(), false))
-						arts.at(combinedArt)++;
-				}
+				if(curHero->hasArt(part->getId(), false, false, false))
+					arts.at(combinedArt->getId()).emplace_back(part->getId());
 			}
-			artPlace->addCombinedArtInfo(arts);
 		}
+		artPlace->addCombinedArtInfo(arts);
 	}
 	else
 	{

+ 1 - 1
client/widgets/CArtifactsOfHeroBase.h

@@ -18,7 +18,7 @@ class CButton;
 class CArtifactsOfHeroBase : virtual public CIntObject, public CKeyShortcut
 {
 protected:
-	using ArtPlacePtr = std::shared_ptr<CHeroArtPlace>;
+	using ArtPlacePtr = std::shared_ptr<CArtPlace>;
 	using BpackScrollFunctor = std::function<void(int)>;
 
 public:

+ 14 - 36
client/widgets/CArtifactsOfHeroMain.cpp

@@ -33,49 +33,27 @@ void CArtifactsOfHeroMain::keyPressed(EShortcut key)
 {
 	if(!shortcutPressed)
 	{
-		uint32_t costumeIdx;
-		switch(key)
+		int saveIdx = vstd::find_pos(costumeSaveShortcuts, key);
+		int loadIdx = vstd::find_pos(costumeLoadShortcuts, key);
+
+		if (saveIdx != -1)
+		{
+			shortcutPressed = true;
+			LOCPLINT->cb->manageHeroCostume(getHero()->id, saveIdx, true);
+			return;
+		}
+
+		if (loadIdx != -1)
 		{
-		case EShortcut::HERO_COSTUME_0:
-			costumeIdx = 0;
-			break;
-		case EShortcut::HERO_COSTUME_1:
-			costumeIdx = 1;
-			break;
-		case EShortcut::HERO_COSTUME_2:
-			costumeIdx = 2;
-			break;
-		case EShortcut::HERO_COSTUME_3:
-			costumeIdx = 3;
-			break;
-		case EShortcut::HERO_COSTUME_4:
-			costumeIdx = 4;
-			break;
-		case EShortcut::HERO_COSTUME_5:
-			costumeIdx = 5;
-			break;
-		case EShortcut::HERO_COSTUME_6:
-			costumeIdx = 6;
-			break;
-		case EShortcut::HERO_COSTUME_7:
-			costumeIdx = 7;
-			break;
-		case EShortcut::HERO_COSTUME_8:
-			costumeIdx = 8;
-			break;
-		case EShortcut::HERO_COSTUME_9:
-			costumeIdx = 9;
-			break;
-		default:
+			shortcutPressed = true;
+			LOCPLINT->cb->manageHeroCostume(getHero()->id, loadIdx, false);
 			return;
 		}
-		shortcutPressed = true;
-		LOCPLINT->cb->manageHeroCostume(getHero()->id, costumeIdx, GH.isKeyboardCtrlDown());
 	}
 }
 
 void CArtifactsOfHeroMain::keyReleased(EShortcut key)
 {
-	if(vstd::contains(costumesSwitcherHotkeys, key))
+	if(vstd::contains(costumeSaveShortcuts, key) || vstd::contains(costumeLoadShortcuts, key))
 		shortcutPressed = false;
 }

+ 24 - 12
client/widgets/CArtifactsOfHeroMain.h

@@ -20,17 +20,29 @@ public:
 	void keyReleased(EShortcut key) override;
 
 private:
-	const std::vector<EShortcut> costumesSwitcherHotkeys =
-	{
-		EShortcut::HERO_COSTUME_0,
-		EShortcut::HERO_COSTUME_1,
-		EShortcut::HERO_COSTUME_2,
-		EShortcut::HERO_COSTUME_3,
-		EShortcut::HERO_COSTUME_4,
-		EShortcut::HERO_COSTUME_5,
-		EShortcut::HERO_COSTUME_6,
-		EShortcut::HERO_COSTUME_7,
-		EShortcut::HERO_COSTUME_8,
-		EShortcut::HERO_COSTUME_9
+	static constexpr std::array costumeSaveShortcuts = {
+		EShortcut::HERO_COSTUME_SAVE_0,
+		EShortcut::HERO_COSTUME_SAVE_1,
+		EShortcut::HERO_COSTUME_SAVE_2,
+		EShortcut::HERO_COSTUME_SAVE_3,
+		EShortcut::HERO_COSTUME_SAVE_4,
+		EShortcut::HERO_COSTUME_SAVE_5,
+		EShortcut::HERO_COSTUME_SAVE_6,
+		EShortcut::HERO_COSTUME_SAVE_7,
+		EShortcut::HERO_COSTUME_SAVE_8,
+		EShortcut::HERO_COSTUME_SAVE_9
+	};
+
+	static constexpr std::array costumeLoadShortcuts = {
+		EShortcut::HERO_COSTUME_LOAD_0,
+		EShortcut::HERO_COSTUME_LOAD_1,
+		EShortcut::HERO_COSTUME_LOAD_2,
+		EShortcut::HERO_COSTUME_LOAD_3,
+		EShortcut::HERO_COSTUME_LOAD_4,
+		EShortcut::HERO_COSTUME_LOAD_5,
+		EShortcut::HERO_COSTUME_LOAD_6,
+		EShortcut::HERO_COSTUME_LOAD_7,
+		EShortcut::HERO_COSTUME_LOAD_8,
+		EShortcut::HERO_COSTUME_LOAD_9
 	};
 };

+ 1 - 0
client/widgets/CGarrisonInt.cpp

@@ -19,6 +19,7 @@
 #include "../render/IImage.h"
 #include "../render/Graphics.h"
 #include "../windows/CCreatureWindow.h"
+#include "../windows/CWindowWithArtifacts.h"
 #include "../windows/GUIClasses.h"
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"

+ 4 - 3
client/widgets/markets/CAltarArtifacts.cpp

@@ -12,6 +12,7 @@
 #include "CAltarArtifacts.h"
 
 #include "../../gui/CGuiHandler.h"
+#include "../../gui/Shortcut.h"
 #include "../../widgets/Buttons.h"
 #include "../../widgets/TextControls.h"
 
@@ -35,16 +36,16 @@ CAltarArtifacts::CAltarArtifacts(const IMarket * market, const CGHeroInstance *
 	altarArtifacts = altarObj;
 
 	deal = std::make_shared<CButton>(Point(269, 520), AnimationPath::builtin("ALTSACR.DEF"),
-		CGI->generaltexth->zelp[585], [this]() {CAltarArtifacts::makeDeal(); });
+		CGI->generaltexth->zelp[585], [this]() {CAltarArtifacts::makeDeal(); }, EShortcut::MARKET_DEAL);
 	labels.emplace_back(std::make_shared<CLabel>(450, 32, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[477]));
 	labels.emplace_back(std::make_shared<CLabel>(302, 424, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[478]));
 
 	sacrificeAllButton = std::make_shared<CButton>(Point(393, 520), AnimationPath::builtin("ALTFILL.DEF"),
-		CGI->generaltexth->zelp[571], std::bind(&CExperienceAltar::sacrificeAll, this));
+		CGI->generaltexth->zelp[571], std::bind(&CExperienceAltar::sacrificeAll, this), EShortcut::MARKET_SACRIFICE_ALL);
 	sacrificeAllButton->block(hero->artifactsInBackpack.empty() && hero->artifactsWorn.empty());
 
 	sacrificeBackpackButton = std::make_shared<CButton>(Point(147, 520), AnimationPath::builtin("ALTEMBK.DEF"),
-		CGI->generaltexth->zelp[570], std::bind(&CAltarArtifacts::sacrificeBackpack, this));
+		CGI->generaltexth->zelp[570], std::bind(&CAltarArtifacts::sacrificeBackpack, this), EShortcut::MARKET_SACRIFICE_BACKPACK);
 	sacrificeBackpackButton->block(hero->artifactsInBackpack.empty());
 
 	// Hero's artifacts

+ 3 - 2
client/widgets/markets/CAltarCreatures.cpp

@@ -12,6 +12,7 @@
 #include "CAltarCreatures.h"
 
 #include "../../gui/CGuiHandler.h"
+#include "../../gui/Shortcut.h"
 #include "../../widgets/Buttons.h"
 #include "../../widgets/TextControls.h"
 
@@ -33,7 +34,7 @@ CAltarCreatures::CAltarCreatures(const IMarket * market, const CGHeroInstance *
 	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
 
 	deal = std::make_shared<CButton>(dealButtonPosWithSlider, AnimationPath::builtin("ALTSACR.DEF"),
-		CGI->generaltexth->zelp[584], [this]() {CAltarCreatures::makeDeal();});
+		CGI->generaltexth->zelp[584], [this]() {CAltarCreatures::makeDeal();}, EShortcut::MARKET_DEAL);
 	labels.emplace_back(std::make_shared<CLabel>(155, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW,
 		boost::str(boost::format(CGI->generaltexth->allTexts[272]) % hero->getNameTranslated())));
 	labels.emplace_back(std::make_shared<CLabel>(450, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[479]));
@@ -44,7 +45,7 @@ CAltarCreatures::CAltarCreatures(const IMarket * market, const CGHeroInstance *
 	unitsOnAltar.resize(GameConstants::ARMY_SIZE, 0);
 	expPerUnit.resize(GameConstants::ARMY_SIZE, 0);
 	sacrificeAllButton = std::make_shared<CButton>(
-		Point(393, 520), AnimationPath::builtin("ALTARMY.DEF"), CGI->generaltexth->zelp[579], std::bind(&CExperienceAltar::sacrificeAll, this));
+		Point(393, 520), AnimationPath::builtin("ALTARMY.DEF"), CGI->generaltexth->zelp[579], std::bind(&CExperienceAltar::sacrificeAll, this), EShortcut::MARKET_SACRIFICE_ALL);
 
 	// Hero creatures panel
 	assert(bidTradePanel);

+ 2 - 1
client/widgets/markets/CArtifactsBuying.cpp

@@ -12,6 +12,7 @@
 #include "CArtifactsBuying.h"
 
 #include "../../gui/CGuiHandler.h"
+#include "../../gui/Shortcut.h"
 #include "../../widgets/Buttons.h"
 #include "../../widgets/TextControls.h"
 
@@ -38,7 +39,7 @@ CArtifactsBuying::CArtifactsBuying(const IMarket * market, const CGHeroInstance
 		title = CGI->generaltexth->allTexts[349];
 	labels.emplace_back(std::make_shared<CLabel>(titlePos.x, titlePos.y, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, title));
 	deal = std::make_shared<CButton>(dealButtonPos, AnimationPath::builtin("TPMRKB.DEF"),
-		CGI->generaltexth->zelp[595], [this](){CArtifactsBuying::makeDeal();});
+		CGI->generaltexth->zelp[595], [this](){CArtifactsBuying::makeDeal();}, EShortcut::MARKET_DEAL);
 	labels.emplace_back(std::make_shared<CLabel>(445, 148, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[168]));
 
 	// Player's resources

+ 2 - 1
client/widgets/markets/CArtifactsSelling.cpp

@@ -12,6 +12,7 @@
 #include "CArtifactsSelling.h"
 
 #include "../../gui/CGuiHandler.h"
+#include "../../gui/Shortcut.h"
 #include "../../widgets/Buttons.h"
 #include "../../widgets/TextControls.h"
 
@@ -43,7 +44,7 @@ CArtifactsSelling::CArtifactsSelling(const IMarket * market, const CGHeroInstanc
 	labels.emplace_back(std::make_shared<CLabel>(titlePos.x, titlePos.y, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, title));
 	labels.push_back(std::make_shared<CLabel>(155, 56, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, boost::str(boost::format(CGI->generaltexth->allTexts[271]) % hero->getNameTranslated())));
 	deal = std::make_shared<CButton>(dealButtonPos, AnimationPath::builtin("TPMRKB.DEF"),
-		CGI->generaltexth->zelp[595], [this](){CArtifactsSelling::makeDeal();});
+		CGI->generaltexth->zelp[595], [this](){CArtifactsSelling::makeDeal();}, EShortcut::MARKET_DEAL);
 	bidSelectedSlot = std::make_shared<CTradeableItem>(Rect(Point(123, 470), Point(69, 66)), EType::ARTIFACT_TYPE, 0, 0);
 
 	// Market resources panel

+ 2 - 1
client/widgets/markets/CFreelancerGuild.cpp

@@ -12,6 +12,7 @@
 #include "CFreelancerGuild.h"
 
 #include "../../gui/CGuiHandler.h"
+#include "../../gui/Shortcut.h"
 #include "../../widgets/Buttons.h"
 #include "../../widgets/TextControls.h"
 
@@ -39,7 +40,7 @@ CFreelancerGuild::CFreelancerGuild(const IMarket * market, const CGHeroInstance
 	labels.emplace_back(std::make_shared<CLabel>(155, 103, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE,
 		boost::str(boost::format(CGI->generaltexth->allTexts[272]) % hero->getNameTranslated())));
 	deal = std::make_shared<CButton>(dealButtonPosWithSlider, AnimationPath::builtin("TPMRKB.DEF"),
-		CGI->generaltexth->zelp[595], [this]() {CFreelancerGuild::makeDeal();});
+		CGI->generaltexth->zelp[595], [this]() {CFreelancerGuild::makeDeal();}, EShortcut::MARKET_DEAL);
 	offerSlider->moveTo(pos.topLeft() + Point(232, 489));
 
 	// Hero creatures panel

+ 2 - 1
client/widgets/markets/CMarketBase.cpp

@@ -14,6 +14,7 @@
 
 #include "../Images.h"
 #include "../../gui/CGuiHandler.h"
+#include "../../gui/Shortcut.h"
 #include "../../widgets/Buttons.h"
 #include "../../widgets/TextControls.h"
 
@@ -200,7 +201,7 @@ CMarketSlider::CMarketSlider(const CSlider::SliderMovingFunctor & movingCallback
 		[this]()
 		{
 			offerSlider->scrollToMax();
-		});
+		}, EShortcut::MARKET_MAX_AMOUNT);
 }
 
 void CMarketSlider::deselect()

+ 2 - 1
client/widgets/markets/CMarketResources.cpp

@@ -12,6 +12,7 @@
 #include "CMarketResources.h"
 
 #include "../../gui/CGuiHandler.h"
+#include "../../gui/Shortcut.h"
 #include "../../widgets/Buttons.h"
 #include "../../widgets/TextControls.h"
 
@@ -36,7 +37,7 @@ CMarketResources::CMarketResources(const IMarket * market, const CGHeroInstance
 
 	labels.emplace_back(std::make_shared<CLabel>(titlePos.x, titlePos.y, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[158]));
 	deal = std::make_shared<CButton>(dealButtonPosWithSlider, AnimationPath::builtin("TPMRKB.DEF"),
-		CGI->generaltexth->zelp[595], [this]() {CMarketResources::makeDeal(); });
+		CGI->generaltexth->zelp[595], [this]() {CMarketResources::makeDeal(); }, EShortcut::MARKET_DEAL);
 
 	// Player's resources
 	assert(bidTradePanel);

+ 2 - 1
client/widgets/markets/CTransferResources.cpp

@@ -12,6 +12,7 @@
 #include "CTransferResources.h"
 
 #include "../../gui/CGuiHandler.h"
+#include "../../gui/Shortcut.h"
 #include "../../widgets/Buttons.h"
 #include "../../widgets/TextControls.h"
 
@@ -34,7 +35,7 @@ CTransferResources::CTransferResources(const IMarket * market, const CGHeroInsta
 	labels.emplace_back(std::make_shared<CLabel>(titlePos.x, titlePos.y, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[158]));
 	labels.emplace_back(std::make_shared<CLabel>(445, 56, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[169]));
 	deal = std::make_shared<CButton>(dealButtonPosWithSlider, AnimationPath::builtin("TPMRKB.DEF"),
-		CGI->generaltexth->zelp[595], [this](){CTransferResources::makeDeal();});
+		CGI->generaltexth->zelp[595], [this](){CTransferResources::makeDeal();}, EShortcut::MARKET_DEAL);
 
 	// Player's resources
 	assert(bidTradePanel);

+ 68 - 24
client/windows/CCastleInterface.cpp

@@ -1036,6 +1036,40 @@ void CCastleBuildings::openTownHall()
 	GH.windows().createAndPushWindow<CHallInterface>(town);
 }
 
+void CCastleBuildings::enterAnyThievesGuild()
+{
+	std::vector<const CGTownInstance*> towns = LOCPLINT->cb->getTownsInfo(true);
+	for(auto & town : towns)
+	{
+		if(town->builtBuildings.count(BuildingID::TAVERN))
+		{
+			LOCPLINT->showThievesGuildWindow(town);
+			return;
+		}
+	}
+	LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithTavern"));
+}
+
+void CCastleBuildings::enterAnyMarket()
+{
+	if(town->builtBuildings.count(BuildingID::MARKETPLACE))
+	{
+		GH.windows().createAndPushWindow<CMarketWindow>(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE);
+		return;
+	}
+
+	std::vector<const CGTownInstance*> towns = LOCPLINT->cb->getTownsInfo(true);
+	for(auto & town : towns)
+	{
+		if(town->builtBuildings.count(BuildingID::MARKETPLACE))
+		{
+			GH.windows().createAndPushWindow<CMarketWindow>(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE);
+			return;
+		}
+	}
+	LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket"));
+}
+
 CCreaInfo::CCreaInfo(Point position, const CGTownInstance * Town, int Level, bool compact, bool _showAvailable):
 	town(Town),
 	level(Level),
@@ -1216,7 +1250,7 @@ CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInst
 	exit = std::make_shared<CButton>(Point(744, 544), AnimationPath::builtin("TSBTNS"), CButton::tooltip(CGI->generaltexth->tcommands[8]), [&](){close();}, EShortcut::GLOBAL_RETURN);
 	exit->setImageOrder(4, 5, 6, 7);
 
-	auto split = std::make_shared<CButton>(Point(744, 382), AnimationPath::builtin("TSBTNS"), CButton::tooltip(CGI->generaltexth->tcommands[3]), [this]() { garr->splitClick(); });
+	auto split = std::make_shared<CButton>(Point(744, 382), AnimationPath::builtin("TSBTNS"), CButton::tooltip(CGI->generaltexth->tcommands[3]), [this]() { garr->splitClick(); }, EShortcut::HERO_ARMY_SPLIT);
 	garr->addSplitBtn(split);
 
 	Rect barRect(9, 182, 732, 18);
@@ -1225,8 +1259,8 @@ CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInst
 	resdatabar = std::make_shared<CResDataBar>(ImagePath::builtin("ARESBAR"), 3, 575, 37, 3, 84, 78);
 
 	townlist = std::make_shared<CTownList>(3, Rect(Point(743, 414), Point(48, 128)), Point(1,16), Point(0, 32), LOCPLINT->localState->getOwnedTowns().size() );
-	townlist->setScrollUpButton( std::make_shared<CButton>( Point(744, 414), AnimationPath::builtin("IAM014"), CButton::tooltipLocalized("core.help.306")));
-	townlist->setScrollDownButton( std::make_shared<CButton>( Point(744, 526), AnimationPath::builtin("IAM015"), CButton::tooltipLocalized("core.help.307")));
+	townlist->setScrollUpButton( std::make_shared<CButton>( Point(744, 414), AnimationPath::builtin("IAM014"), CButton::tooltipLocalized("core.help.306"), 0, EShortcut::MOVE_UP));
+	townlist->setScrollDownButton( std::make_shared<CButton>( Point(744, 526), AnimationPath::builtin("IAM015"), CButton::tooltipLocalized("core.help.307"), 0, EShortcut::MOVE_DOWN));
 
 	if(from)
 		townlist->select(from);
@@ -1327,27 +1361,14 @@ void CCastleInterface::recreateIcons()
 	hall = std::make_shared<CTownInfo>(80, 413, town, true);
 	fort = std::make_shared<CTownInfo>(122, 413, town, false);
 
-	fastTownHall = std::make_shared<CButton>(Point(80, 413), AnimationPath::builtin("castleInterfaceQuickAccess"), CButton::tooltip(), [this](){ builds->enterTownHall(); });
+	fastTownHall = std::make_shared<CButton>(Point(80, 413), AnimationPath::builtin("castleInterfaceQuickAccess"), CButton::tooltip(), [this](){ builds->enterTownHall(); }, EShortcut::TOWN_OPEN_HALL);
 	fastTownHall->setOverlay(std::make_shared<CAnimImage>(AnimationPath::builtin("ITMTL"), town->hallLevel()));
 
 	int imageIndex = town->fortLevel() == CGTownInstance::EFortLevel::NONE ? 3 : town->fortLevel() - 1;
-	fastArmyPurchase = std::make_shared<CButton>(Point(122, 413), AnimationPath::builtin("castleInterfaceQuickAccess"), CButton::tooltip(), [this](){ builds->enterToTheQuickRecruitmentWindow(); });
+	fastArmyPurchase = std::make_shared<CButton>(Point(122, 413), AnimationPath::builtin("castleInterfaceQuickAccess"), CButton::tooltip(), [this](){ builds->enterToTheQuickRecruitmentWindow(); }, EShortcut::TOWN_OPEN_RECRUITMENT);
 	fastArmyPurchase->setOverlay(std::make_shared<CAnimImage>(AnimationPath::builtin("itmcl"), imageIndex));
 
-	fastMarket = std::make_shared<LRClickableArea>(Rect(163, 410, 64, 42), [&]()
-	{
-		std::vector<const CGTownInstance*> towns = LOCPLINT->cb->getTownsInfo(true);
-		for(auto & town : towns)
-		{
-			if(town->builtBuildings.count(BuildingID::MARKETPLACE))
-			{
-				GH.windows().createAndPushWindow<CMarketWindow>(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE);
-				return;
-			}
-		}
-		LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket"));
-	});
-	
+	fastMarket = std::make_shared<LRClickableArea>(Rect(163, 410, 64, 42), [this]() { builds->enterAnyMarket(); });
 	fastTavern = std::make_shared<LRClickableArea>(Rect(15, 387, 58, 64), [&]()
 	{
 		if(town->builtBuildings.count(BuildingID::TAVERN))
@@ -1368,18 +1389,41 @@ void CCastleInterface::recreateIcons()
 
 	for(size_t i=0; i<4; i++)
 		creainfo.push_back(std::make_shared<CCreaInfo>(Point(14 + 55 * (int)i, 507), town, (int)i + 4, compactCreatureInfo, useAvailableCreaturesForLabel));
-
 }
 
 void CCastleInterface::keyPressed(EShortcut key)
 {
 	switch(key)
 	{
-	case EShortcut::MOVE_UP:
-		townlist->selectPrev();
+	case EShortcut::TOWN_OPEN_FORT:
+		GH.windows().createAndPushWindow<CFortScreen>(town);
+		break;
+	case EShortcut::TOWN_OPEN_MARKET:
+		builds->enterAnyMarket();
+		break;
+	case EShortcut::TOWN_OPEN_MAGE_GUILD:
+		if(town->hasBuilt(BuildingID::MAGES_GUILD_1))
+			builds->enterMagesGuild();
+		break;
+	case EShortcut::TOWN_OPEN_THIEVES_GUILD:
+		break;
+	case EShortcut::TOWN_OPEN_HERO_EXCHANGE:
+		if (town->visitingHero && town->garrisonHero)
+			LOCPLINT->showHeroExchange(town->visitingHero->id, town->garrisonHero->id);
+		break;
+	case EShortcut::TOWN_OPEN_HERO:
+		if (town->visitingHero)
+			LOCPLINT->openHeroWindow(town->visitingHero);
+		else if (town->garrisonHero)
+			LOCPLINT->openHeroWindow(town->garrisonHero);
+		break;
+	case EShortcut::TOWN_OPEN_VISITING_HERO:
+		if (town->visitingHero)
+			LOCPLINT->openHeroWindow(town->visitingHero);
 		break;
-	case EShortcut::MOVE_DOWN:
-		townlist->selectNext();
+	case EShortcut::TOWN_OPEN_GARRISONED_HERO:
+		if (town->garrisonHero)
+			LOCPLINT->openHeroWindow(town->garrisonHero);
 		break;
 	case EShortcut::TOWN_SWAP_ARMIES:
 		heroes->swapArmies();

+ 3 - 1
client/windows/CCastleInterface.h

@@ -154,7 +154,6 @@ class CCastleBuildings : public CIntObject
 	void enterBuilding(BuildingID building);//for buildings with simple description + pic left-click messages
 	void enterCastleGate();
 	void enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades);//Rampart's fountains
-	void enterMagesGuild();
 	
 	void openMagesGuild();
 	void openTownHall();
@@ -168,6 +167,9 @@ public:
 
 	void enterDwelling(int level);
 	void enterTownHall();
+	void enterMagesGuild();
+	void enterAnyMarket();
+	void enterAnyThievesGuild();
 	void enterToTheQuickRecruitmentWindow();
 
 	void buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID = BuildingSubID::NONE, BuildingID upgrades = BuildingID::NONE);

+ 383 - 0
client/windows/CExchangeWindow.cpp

@@ -0,0 +1,383 @@
+/*
+ * CExchangeWindow.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "CExchangeWindow.h"
+
+#include "CHeroBackpackWindow.h"
+
+#include "../CGameInfo.h"
+#include "../CPlayerInterface.h"
+
+#include "../gui/CGuiHandler.h"
+#include "../gui/CursorHandler.h"
+#include "../gui/Shortcut.h"
+#include "../gui/WindowHandler.h"
+
+#include "../widgets/CGarrisonInt.h"
+#include "../widgets/Images.h"
+#include "../widgets/Buttons.h"
+#include "../widgets/TextControls.h"
+
+#include "../render/IRenderHandler.h"
+#include "../render/CAnimation.h"
+
+#include "../../CCallback.h"
+
+#include "../lib/mapObjects/CGHeroInstance.h"
+#include "../lib/CGeneralTextHandler.h"
+#include "../lib/CHeroHandler.h"
+#include "../lib/filesystem/Filesystem.h"
+#include "../lib/CSkillHandler.h"
+#include "../lib/TextOperations.h"
+
+static const std::string QUICK_EXCHANGE_BG = "quick-exchange/TRADEQE";
+
+static bool isQuickExchangeLayoutAvailable()
+{
+	return CResourceHandler::get()->existsResource(ImagePath::builtin("SPRITES/" + QUICK_EXCHANGE_BG));
+}
+
+CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID)
+	: CWindowObject(PLAYER_COLORED | BORDERED, ImagePath::builtin(isQuickExchangeLayoutAvailable() ? QUICK_EXCHANGE_BG : "TRADE2")),
+	controller(hero1, hero2),
+	moveStackLeftButtons(),
+	moveStackRightButtons()
+{
+	const bool qeLayout = isQuickExchangeLayoutAvailable();
+
+	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
+	addUsedEvents(KEYBOARD);
+
+	heroInst[0] = LOCPLINT->cb->getHero(hero1);
+	heroInst[1] = LOCPLINT->cb->getHero(hero2);
+
+	auto genTitle = [](const CGHeroInstance * h)
+	{
+		boost::format fmt(CGI->generaltexth->allTexts[138]);
+		fmt % h->getNameTranslated() % h->level % h->getClassNameTranslated();
+		return boost::str(fmt);
+	};
+
+	titles[0] = std::make_shared<CLabel>(147, 25, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, genTitle(heroInst[0]));
+	titles[1] = std::make_shared<CLabel>(653, 25, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, genTitle(heroInst[1]));
+
+	auto PSKIL32 = GH.renderHandler().loadAnimation(AnimationPath::builtin("PSKIL32"));
+	PSKIL32->preload();
+
+	auto SECSK32 = GH.renderHandler().loadAnimation(AnimationPath::builtin("SECSK32"));
+
+	for(int g = 0; g < 4; ++g)
+	{
+		if (qeLayout)
+			primSkillImages.push_back(std::make_shared<CAnimImage>(PSKIL32, g, Rect(389, 12 + 26 * g, 22, 22)));
+		else
+			primSkillImages.push_back(std::make_shared<CAnimImage>(PSKIL32, g, 0, 385, 19 + 36 * g));
+	}
+
+	for(int leftRight : {0, 1})
+	{
+		const CGHeroInstance * hero = heroInst.at(leftRight);
+
+		for(int m=0; m<GameConstants::PRIMARY_SKILLS; ++m)
+			primSkillValues[leftRight].push_back(std::make_shared<CLabel>(352 + (qeLayout ? 96 : 93) * leftRight, (qeLayout ? 22 : 35) + (qeLayout ? 26 : 36) * m, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE));
+
+
+		for(int m=0; m < hero->secSkills.size(); ++m)
+			secSkillIcons[leftRight].push_back(std::make_shared<CAnimImage>(SECSK32, 0, 0, 32 + 36 * m + 454 * leftRight, qeLayout ? 83 : 88));
+
+		specImages[leftRight] = std::make_shared<CAnimImage>(AnimationPath::builtin("UN32"), hero->type->imageIndex, 0, 67 + 490 * leftRight, qeLayout ? 41 : 45);
+
+		expImages[leftRight] = std::make_shared<CAnimImage>(PSKIL32, 4, 0, 103 + 490 * leftRight, qeLayout ? 41 : 45);
+		expValues[leftRight] = std::make_shared<CLabel>(119 + 490 * leftRight, qeLayout ? 66 : 71, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
+
+		manaImages[leftRight] = std::make_shared<CAnimImage>(PSKIL32, 5, 0, 139 + 490 * leftRight, qeLayout ? 41 : 45);
+		manaValues[leftRight] = std::make_shared<CLabel>(155 + 490 * leftRight, qeLayout ? 66 : 71, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
+	}
+
+	artifs[0] = std::make_shared<CArtifactsOfHeroMain>(Point(-334, 151));
+	artifs[0]->clickPressedCallback = [this, hero = heroInst[0]](const CArtPlace & artPlace, const Point & cursorPosition){clickPressedOnArtPlace(hero, artPlace.slot, true, false, false);};
+	artifs[0]->showPopupCallback = [this, heroArts = artifs[0]](CArtPlace & artPlace, const Point & cursorPosition){showArtifactAssembling(*heroArts, artPlace, cursorPosition);};
+	artifs[0]->gestureCallback = [this, hero = heroInst[0]](const CArtPlace & artPlace, const Point & cursorPosition){showQuickBackpackWindow(hero, artPlace.slot, cursorPosition);};
+	artifs[0]->setHero(heroInst[0]);
+	artifs[1] = std::make_shared<CArtifactsOfHeroMain>(Point(98, 151));
+	artifs[1]->clickPressedCallback = [this, hero = heroInst[1]](const CArtPlace & artPlace, const Point & cursorPosition){clickPressedOnArtPlace(hero, artPlace.slot, true, false, false);};
+	artifs[1]->showPopupCallback = [this, heroArts = artifs[1]](CArtPlace & artPlace, const Point & cursorPosition){showArtifactAssembling(*heroArts, artPlace, cursorPosition);};
+	artifs[1]->gestureCallback = [this, hero = heroInst[1]](const CArtPlace & artPlace, const Point & cursorPosition){showQuickBackpackWindow(hero, artPlace.slot, cursorPosition);};
+	artifs[1]->setHero(heroInst[1]);
+
+
+	addSet(artifs[0]);
+	addSet(artifs[1]);
+
+	for(int g=0; g<4; ++g)
+	{
+		primSkillAreas.push_back(std::make_shared<LRClickableAreaWTextComp>());
+		if (qeLayout)
+			primSkillAreas[g]->pos = Rect(Point(pos.x + 324, pos.y + 12 + 26 * g), Point(152, 22));
+		else
+			primSkillAreas[g]->pos = Rect(Point(pos.x + 329, pos.y + 19 + 36 * g), Point(140, 32));
+		primSkillAreas[g]->text = CGI->generaltexth->arraytxt[2+g];
+		primSkillAreas[g]->component = Component( ComponentType::PRIM_SKILL, PrimarySkill(g));
+		primSkillAreas[g]->hoverText = CGI->generaltexth->heroscrn[1];
+		boost::replace_first(primSkillAreas[g]->hoverText, "%s", CGI->generaltexth->primarySkillNames[g]);
+	}
+
+	//heroes related thing
+	for(int b=0; b < heroInst.size(); b++)
+	{
+		const CGHeroInstance * hero = heroInst.at(b);
+
+		//secondary skill's clickable areas
+		for(int g=0; g<hero->secSkills.size(); ++g)
+		{
+			SecondarySkill skill = hero->secSkills[g].first;
+			int level = hero->secSkills[g].second; // <1, 3>
+			secSkillAreas[b].push_back(std::make_shared<LRClickableAreaWTextComp>());
+			secSkillAreas[b][g]->pos = Rect(Point(pos.x + 32 + g * 36 + b * 454 , pos.y + (qeLayout ? 83 : 88)), Point(32, 32) );
+			secSkillAreas[b][g]->component = Component(ComponentType::SEC_SKILL, skill, level);
+			secSkillAreas[b][g]->text = CGI->skillh->getByIndex(skill)->getDescriptionTranslated(level);
+
+			secSkillAreas[b][g]->hoverText = CGI->generaltexth->heroscrn[21];
+			boost::algorithm::replace_first(secSkillAreas[b][g]->hoverText, "%s", CGI->generaltexth->levels[level - 1]);
+			boost::algorithm::replace_first(secSkillAreas[b][g]->hoverText, "%s", CGI->skillh->getByIndex(skill)->getNameTranslated());
+		}
+
+		heroAreas[b] = std::make_shared<CHeroArea>(257 + 228 * b, 13, hero);
+		heroAreas[b]->addClickCallback([this, hero]() -> void
+									   {
+										   if(getPickedArtifact() == nullptr)
+											   LOCPLINT->openHeroWindow(hero);
+									   });
+
+		specialtyAreas[b] = std::make_shared<LRClickableAreaWText>();
+		specialtyAreas[b]->pos = Rect(Point(pos.x + 69 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32));
+		specialtyAreas[b]->hoverText = CGI->generaltexth->heroscrn[27];
+		specialtyAreas[b]->text = hero->type->getSpecialtyDescriptionTranslated();
+
+		experienceAreas[b] = std::make_shared<LRClickableAreaWText>();
+		experienceAreas[b]->pos = Rect(Point(pos.x + 105 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32));
+		experienceAreas[b]->hoverText = CGI->generaltexth->heroscrn[9];
+		experienceAreas[b]->text = CGI->generaltexth->allTexts[2];
+		boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(hero->level));
+		boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(CGI->heroh->reqExp(hero->level+1)));
+		boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(hero->exp));
+
+		spellPointsAreas[b] = std::make_shared<LRClickableAreaWText>();
+		spellPointsAreas[b]->pos = Rect(Point(pos.x + 141 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32));
+		spellPointsAreas[b]->hoverText = CGI->generaltexth->heroscrn[22];
+		spellPointsAreas[b]->text = CGI->generaltexth->allTexts[205];
+		boost::algorithm::replace_first(spellPointsAreas[b]->text, "%s", hero->getNameTranslated());
+		boost::algorithm::replace_first(spellPointsAreas[b]->text, "%d", std::to_string(hero->mana));
+		boost::algorithm::replace_first(spellPointsAreas[b]->text, "%d", std::to_string(hero->manaLimit()));
+
+		morale[b] = std::make_shared<MoraleLuckBox>(true, Rect(Point(176 + 490 * b, 39), Point(32, 32)), true);
+		luck[b] = std::make_shared<MoraleLuckBox>(false,  Rect(Point(212 + 490 * b, 39), Point(32, 32)), true);
+	}
+
+	quit = std::make_shared<CButton>(Point(732, 567), AnimationPath::builtin("IOKAY.DEF"), CGI->generaltexth->zelp[600], std::bind(&CExchangeWindow::close, this), EShortcut::GLOBAL_ACCEPT);
+	if(queryID.getNum() > 0)
+		quit->addCallback([=](){ LOCPLINT->cb->selectionMade(0, queryID); });
+
+	questlogButton[0] = std::make_shared<CButton>(Point( 10, qeLayout ? 39 : 44), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(CGI->generaltexth->heroscrn[0]), std::bind(&CExchangeWindow::questLogShortcut, this), EShortcut::ADVENTURE_QUEST_LOG);
+	questlogButton[1] = std::make_shared<CButton>(Point(740, qeLayout ? 39 : 44), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(CGI->generaltexth->heroscrn[0]), std::bind(&CExchangeWindow::questLogShortcut, this), EShortcut::ADVENTURE_QUEST_LOG);
+
+	Rect barRect(5, 578, 725, 18);
+	statusbar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), barRect, 5, 578));
+
+	//garrison interface
+
+	garr = std::make_shared<CGarrisonInt>(Point(69, qeLayout ? 122 : 131), 4, Point(418,0), heroInst[0], heroInst[1], true, true);
+	auto splitButtonCallback = [&](){ garr->splitClick(); };
+	garr->addSplitBtn(std::make_shared<CButton>( Point( 10, qeLayout ? 122 : 132), AnimationPath::builtin("TSBTNS.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback, EShortcut::HERO_ARMY_SPLIT));
+	garr->addSplitBtn(std::make_shared<CButton>( Point(744, qeLayout ? 122 : 132), AnimationPath::builtin("TSBTNS.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback, EShortcut::HERO_ARMY_SPLIT));
+
+	if(qeLayout)
+	{
+		moveAllGarrButtonLeft    = std::make_shared<CButton>(Point(325, 118), AnimationPath::builtin("quick-exchange/armRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]),
+			[this](){ this->moveUnitsShortcut(false); });
+		exchangeGarrButton       = std::make_shared<CButton>(Point(377, 118), AnimationPath::builtin("quick-exchange/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[2]),
+			[this](){ controller.swapArmy(); });
+		moveAllGarrButtonRight   = std::make_shared<CButton>(Point(425, 118), AnimationPath::builtin("quick-exchange/armLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]),
+			[this](){ this->moveUnitsShortcut(true); });
+		moveArtifactsButtonLeft  = std::make_shared<CButton>(Point(325, 154), AnimationPath::builtin("quick-exchange/artRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]),
+			[this](){ this->moveArtifactsCallback(false);});
+		exchangeArtifactsButton  = std::make_shared<CButton>(Point(377, 154), AnimationPath::builtin("quick-exchange/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[4]),
+			[this](){ this->swapArtifactsCallback(); });
+		moveArtifactsButtonRight = std::make_shared<CButton>(Point(425, 154), AnimationPath::builtin("quick-exchange/artLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]),
+			[this](){ this->moveArtifactsCallback(true);});
+
+		backpackButtonLeft       = std::make_shared<CButton>(Point(325, 518), AnimationPath::builtin("heroBackpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"),
+			[this](){ this->backpackShortcut(true); });
+		backpackButtonRight      = std::make_shared<CButton>(Point(419, 518), AnimationPath::builtin("heroBackpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"),
+			[this](){ this->backpackShortcut(false); });
+		backpackButtonLeft->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("heroWindow/backpackButtonIcon")));
+		backpackButtonRight->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("heroWindow/backpackButtonIcon")));
+
+		auto leftHeroBlock = heroInst[0]->tempOwner != LOCPLINT->cb->getPlayerID();
+		auto rightHeroBlock = heroInst[1]->tempOwner != LOCPLINT->cb->getPlayerID();
+		moveAllGarrButtonLeft->block(leftHeroBlock);
+		exchangeGarrButton->block(leftHeroBlock || rightHeroBlock);
+		moveAllGarrButtonRight->block(rightHeroBlock);
+		moveArtifactsButtonLeft->block(leftHeroBlock);
+		exchangeArtifactsButton->block(leftHeroBlock || rightHeroBlock);
+		moveArtifactsButtonRight->block(rightHeroBlock);
+		backpackButtonLeft->block(leftHeroBlock);
+		backpackButtonRight->block(rightHeroBlock);
+
+		for(int i = 0; i < GameConstants::ARMY_SIZE; i++)
+		{
+			moveStackLeftButtons.push_back(
+				std::make_shared<CButton>(
+					Point(484 + 35 * i, 154),
+					AnimationPath::builtin("quick-exchange/unitLeft.DEF"),
+					CButton::tooltip(CGI->generaltexth->qeModCommands[1]),
+					std::bind(&CExchangeController::moveStack, &controller, false, SlotID(i))));
+			moveStackLeftButtons.back()->block(leftHeroBlock);
+
+			moveStackRightButtons.push_back(
+				std::make_shared<CButton>(
+					Point(66 + 35 * i, 154),
+					AnimationPath::builtin("quick-exchange/unitRight.DEF"),
+					CButton::tooltip(CGI->generaltexth->qeModCommands[1]),
+					std::bind(&CExchangeController::moveStack, &controller, true, SlotID(i))));
+			moveStackLeftButtons.back()->block(rightHeroBlock);
+		}
+	}
+
+	CWindowWithArtifacts::update();
+}
+
+void CExchangeWindow::moveArtifactsCallback(bool leftToRight)
+{
+	bool moveEquipped = !GH.isKeyboardShiftDown();
+	bool moveBackpack = !GH.isKeyboardCmdDown();
+	controller.moveArtifacts(leftToRight, moveEquipped, moveBackpack);
+};
+
+void CExchangeWindow::swapArtifactsCallback()
+{
+	bool moveEquipped = !GH.isKeyboardShiftDown();
+	bool moveBackpack = !GH.isKeyboardCmdDown();
+	controller.swapArtifacts(moveEquipped, moveBackpack);
+}
+
+void CExchangeWindow::moveUnitsShortcut(bool leftToRight)
+{
+	std::optional<SlotID> slotId = std::nullopt;
+	if(const auto * slot = getSelectedSlotID())
+		slotId = slot->getSlot();
+	controller.moveArmy(leftToRight, slotId);
+};
+
+void CExchangeWindow::backpackShortcut(bool leftHero)
+{
+	GH.windows().createAndPushWindow<CHeroBackpackWindow>(heroInst[leftHero ? 0 : 1], artSets);
+};
+
+void CExchangeWindow::keyPressed(EShortcut key)
+{
+	switch (key)
+	{
+		case EShortcut::EXCHANGE_ARMY_TO_LEFT:
+			moveUnitsShortcut(false);
+		break;
+		case EShortcut::EXCHANGE_ARMY_TO_RIGHT:
+			moveUnitsShortcut(true);
+		break;
+		case EShortcut::EXCHANGE_ARMY_SWAP:
+			controller.swapArmy();
+		break;
+		case EShortcut::EXCHANGE_ARTIFACTS_TO_LEFT:
+			controller.moveArtifacts(false, true, true);
+		break;
+		case EShortcut::EXCHANGE_ARTIFACTS_TO_RIGHT:
+			controller.moveArtifacts(true, true, true);
+		break;
+		case EShortcut::EXCHANGE_ARTIFACTS_SWAP:
+			controller.swapArtifacts(true, true);
+		break;
+		case EShortcut::EXCHANGE_EQUIPPED_TO_LEFT:
+			controller.moveArtifacts(false, true, false);
+		break;
+		case EShortcut::EXCHANGE_EQUIPPED_TO_RIGHT:
+			controller.moveArtifacts(true, true, false);
+		break;
+		case EShortcut::EXCHANGE_EQUIPPED_SWAP:
+			controller.swapArtifacts(true, false);
+		break;
+		case EShortcut::EXCHANGE_BACKPACK_TO_LEFT:
+			controller.moveArtifacts(false, false, true);
+		break;
+		case EShortcut::EXCHANGE_BACKPACK_TO_RIGHT:
+			controller.moveArtifacts(true, false, true);
+		break;
+		case EShortcut::EXCHANGE_BACKPACK_SWAP:
+			controller.swapArtifacts(false, true);
+		break;
+		case EShortcut::EXCHANGE_BACKPACK_LEFT:
+			backpackShortcut(true);
+		break;
+		case EShortcut::EXCHANGE_BACKPACK_RIGHT:
+			backpackShortcut(false);
+		break;
+	}
+}
+
+const CGarrisonSlot * CExchangeWindow::getSelectedSlotID() const
+{
+	return garr->getSelection();
+}
+
+void CExchangeWindow::updateGarrisons()
+{
+	garr->recreateSlots();
+
+	update();
+}
+
+bool CExchangeWindow::holdsGarrison(const CArmedInstance * army)
+{
+	return garr->upperArmy() == army || garr->lowerArmy() == army;
+}
+
+void CExchangeWindow::questLogShortcut()
+{
+	CCS->curh->dragAndDropCursor(nullptr);
+	LOCPLINT->showQuestLog();
+}
+
+void CExchangeWindow::update()
+{
+	for(size_t leftRight : {0, 1})
+	{
+		const CGHeroInstance * hero = heroInst.at(leftRight);
+
+		for(int m=0; m<GameConstants::PRIMARY_SKILLS; ++m)
+		{
+			auto value = heroInst[leftRight]->getPrimSkillLevel(static_cast<PrimarySkill>(m));
+			primSkillValues[leftRight][m]->setText(std::to_string(value));
+		}
+
+		for(int m=0; m < hero->secSkills.size(); ++m)
+		{
+			int id = hero->secSkills[m].first;
+			int level = hero->secSkills[m].second;
+
+			secSkillIcons[leftRight][m]->setFrame(2 + id * 3 + level);
+		}
+
+		expValues[leftRight]->setText(TextOperations::formatMetric(hero->exp, 3));
+		manaValues[leftRight]->setText(TextOperations::formatMetric(hero->mana, 3));
+
+		morale[leftRight]->set(hero);
+		luck[leftRight]->set(hero);
+	}
+}
+

+ 78 - 0
client/windows/CExchangeWindow.h

@@ -0,0 +1,78 @@
+/*
+ * CExchangeWindow.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "CWindowWithArtifacts.h"
+#include "../widgets/CExchangeController.h"
+
+class CGarrisonSlot;
+
+class CExchangeWindow : public CStatusbarWindow, public IGarrisonHolder, public CWindowWithArtifacts
+{
+	std::array<std::shared_ptr<CLabel>, 2> titles;
+	std::vector<std::shared_ptr<CAnimImage>> primSkillImages;//shared for both heroes
+	std::array<std::vector<std::shared_ptr<CLabel>>, 2> primSkillValues;
+	std::array<std::vector<std::shared_ptr<CAnimImage>>, 2> secSkillIcons;
+	std::array<std::shared_ptr<CAnimImage>, 2> specImages;
+	std::array<std::shared_ptr<CAnimImage>, 2> expImages;
+	std::array<std::shared_ptr<CLabel>, 2> expValues;
+	std::array<std::shared_ptr<CAnimImage>, 2> manaImages;
+	std::array<std::shared_ptr<CLabel>, 2> manaValues;
+
+	std::vector<std::shared_ptr<LRClickableAreaWTextComp>> primSkillAreas;
+	std::array<std::vector<std::shared_ptr<LRClickableAreaWTextComp>>, 2> secSkillAreas;
+
+	std::array<std::shared_ptr<CHeroArea>, 2> heroAreas;
+	std::array<std::shared_ptr<LRClickableAreaWText>, 2> specialtyAreas;
+	std::array<std::shared_ptr<LRClickableAreaWText>, 2> experienceAreas;
+	std::array<std::shared_ptr<LRClickableAreaWText>, 2> spellPointsAreas;
+
+	std::array<std::shared_ptr<MoraleLuckBox>, 2> morale;
+	std::array<std::shared_ptr<MoraleLuckBox>, 2> luck;
+
+	std::shared_ptr<CButton> quit;
+	std::array<std::shared_ptr<CButton>, 2> questlogButton;
+
+	std::shared_ptr<CGarrisonInt> garr;
+	std::shared_ptr<CButton> moveAllGarrButtonLeft;
+	std::shared_ptr<CButton> exchangeGarrButton;
+	std::shared_ptr<CButton> moveAllGarrButtonRight;
+	std::shared_ptr<CButton> moveArtifactsButtonLeft;
+	std::shared_ptr<CButton> exchangeArtifactsButton;
+	std::shared_ptr<CButton> moveArtifactsButtonRight;
+	std::vector<std::shared_ptr<CButton>> moveStackLeftButtons;
+	std::vector<std::shared_ptr<CButton>> moveStackRightButtons;
+	std::shared_ptr<CButton> backpackButtonLeft;
+	std::shared_ptr<CButton> backpackButtonRight;
+	CExchangeController controller;
+
+	void moveArtifactsCallback(bool leftToRight);
+	void swapArtifactsCallback();
+	void moveUnitsShortcut(bool leftToRight);
+	void backpackShortcut(bool leftHero);
+	void questLogShortcut();
+
+	std::array<const CGHeroInstance *, 2> heroInst;
+	std::array<std::shared_ptr<CArtifactsOfHeroMain>, 2> artifs;
+
+	const CGarrisonSlot * getSelectedSlotID() const;
+
+public:
+	CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID);
+
+	void keyPressed(EShortcut key) override;
+
+	void update() override;
+
+	// IGarrisonHolder impl
+	void updateGarrisons() override;
+	bool holdsGarrison(const CArmedInstance * army) override;
+
+};

+ 5 - 5
client/windows/CHeroWindow.cpp

@@ -13,7 +13,7 @@
 #include "CCreatureWindow.h"
 #include "CHeroBackpackWindow.h"
 #include "CKingdomInterface.h"
-#include "GUIClasses.h"
+#include "CExchangeWindow.h"
 
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
@@ -22,6 +22,7 @@
 #include "../gui/TextAlignment.h"
 #include "../gui/Shortcut.h"
 #include "../gui/WindowHandler.h"
+#include "../widgets/Images.h"
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/CGarrisonInt.h"
@@ -207,7 +208,7 @@ void CHeroWindow::update()
 			boost::algorithm::replace_first(helpBox, "%s", CGI->generaltexth->allTexts[43]);
 
 			garr = std::make_shared<CGarrisonInt>(Point(15, 485), 8, Point(), curHero);
-			auto split = std::make_shared<CButton>(Point(539, 519), AnimationPath::builtin("hsbtns9.def"), CButton::tooltip(CGI->generaltexth->allTexts[256], helpBox), [&](){ garr->splitClick(); });
+			auto split = std::make_shared<CButton>(Point(539, 519), AnimationPath::builtin("hsbtns9.def"), CButton::tooltip(CGI->generaltexth->allTexts[256], helpBox), [&](){ garr->splitClick(); }, EShortcut::HERO_ARMY_SPLIT);
 			garr->addSplitBtn(split);
 		}
 		if(!arts)
@@ -278,9 +279,8 @@ void CHeroWindow::update()
 
 	for(auto cew : GH.windows().findWindows<CExchangeWindow>())
 	{
-		for(int g=0; g < cew->heroInst.size(); ++g)
-			if(cew->heroInst[g] == curHero)
-				noDismiss = true;
+		if (cew->holdsGarrison(curHero))
+			noDismiss = true;
 	}
 
 	//if player only have one hero and no towns

+ 4 - 4
client/windows/CKingdomInterface.cpp

@@ -878,7 +878,7 @@ class ArtSlotsTab : public CIntObject
 {
 public:
 	std::shared_ptr<CAnimImage> background;
-	std::vector<std::shared_ptr<CHeroArtPlace>> arts;
+	std::vector<std::shared_ptr<CArtPlace>> arts;
 
 	ArtSlotsTab()
 	{
@@ -886,7 +886,7 @@ public:
 		background = std::make_shared<CAnimImage>(AnimationPath::builtin("OVSLOT"), 4);
 		pos = background->pos;
 		for(int i=0; i<9; i++)
-			arts.push_back(std::make_shared<CHeroArtPlace>(Point(269+i*48, 66)));
+			arts.push_back(std::make_shared<CArtPlace>(Point(269+i*48, 66)));
 	}
 };
 
@@ -894,7 +894,7 @@ class BackpackTab : public CIntObject
 {
 public:
 	std::shared_ptr<CAnimImage> background;
-	std::vector<std::shared_ptr<CHeroArtPlace>> arts;
+	std::vector<std::shared_ptr<CArtPlace>> arts;
 	std::shared_ptr<CButton> btnLeft;
 	std::shared_ptr<CButton> btnRight;
 
@@ -906,7 +906,7 @@ public:
 		btnLeft = std::make_shared<CButton>(Point(269, 66), AnimationPath::builtin("HSBTNS3"), CButton::tooltip(), 0);
 		btnRight = std::make_shared<CButton>(Point(675, 66), AnimationPath::builtin("HSBTNS5"), CButton::tooltip(), 0);
 		for(int i=0; i<8; i++)
-			arts.push_back(std::make_shared<CHeroArtPlace>(Point(294+i*48, 66)));
+			arts.push_back(std::make_shared<CArtPlace>(Point(294+i*48, 66)));
 	}
 };
 

+ 9 - 9
client/windows/CMarketWindow.cpp

@@ -128,35 +128,35 @@ void CMarketWindow::createChangeModeButtons(EMarketMode currentMode, const IMark
 	auto buttonPos = Point(18, 520);
 
 	auto addButton = [this, &buttonPos](const AnimationPath & picPath, const std::pair<std::string, std::string> & buttonHelpContainer,
-		const std::function<void()> & pressButtonFunctor)
+		const std::function<void()> & pressButtonFunctor, EShortcut shortcut)
 	{
-		changeModeButtons.emplace_back(std::make_shared<CButton>(buttonPos, picPath, buttonHelpContainer, pressButtonFunctor));
+		changeModeButtons.emplace_back(std::make_shared<CButton>(buttonPos, picPath, buttonHelpContainer, pressButtonFunctor, shortcut));
 		buttonPos -= Point(0, buttonHeightWithMargin);
 	};
 
 	if(isButtonVisible(EMarketMode::RESOURCE_PLAYER))
-		addButton(AnimationPath::builtin("TPMRKBU1.DEF"), CGI->generaltexth->zelp[612], std::bind(&CMarketWindow::createTransferResources, this, market, hero));
+		addButton(AnimationPath::builtin("TPMRKBU1.DEF"), CGI->generaltexth->zelp[612], std::bind(&CMarketWindow::createTransferResources, this, market, hero), EShortcut::MARKET_RESOURCE_PLAYER);
 	if(isButtonVisible(EMarketMode::ARTIFACT_RESOURCE))
-		addButton(AnimationPath::builtin("TPMRKBU3.DEF"), CGI->generaltexth->zelp[613], std::bind(&CMarketWindow::createArtifactsSelling, this, market, hero));
+		addButton(AnimationPath::builtin("TPMRKBU3.DEF"), CGI->generaltexth->zelp[613], std::bind(&CMarketWindow::createArtifactsSelling, this, market, hero), EShortcut::MARKET_ARTIFACT_RESOURCE);
 	if(isButtonVisible(EMarketMode::RESOURCE_ARTIFACT))
-		addButton(AnimationPath::builtin("TPMRKBU2.DEF"), CGI->generaltexth->zelp[598], std::bind(&CMarketWindow::createArtifactsBuying, this, market, hero));
+		addButton(AnimationPath::builtin("TPMRKBU2.DEF"), CGI->generaltexth->zelp[598], std::bind(&CMarketWindow::createArtifactsBuying, this, market, hero), EShortcut::MARKET_RESOURCE_ARTIFACT);
 
 	buttonPos = Point(516, 520 - buttonHeightWithMargin);
 	if(isButtonVisible(EMarketMode::CREATURE_RESOURCE))
-		addButton(AnimationPath::builtin("TPMRKBU4.DEF"), CGI->generaltexth->zelp[599], std::bind(&CMarketWindow::createFreelancersGuild, this, market, hero));
+		addButton(AnimationPath::builtin("TPMRKBU4.DEF"), CGI->generaltexth->zelp[599], std::bind(&CMarketWindow::createFreelancersGuild, this, market, hero), EShortcut::MARKET_CREATURE_RESOURCE);
 	if(isButtonVisible(EMarketMode::RESOURCE_RESOURCE))
-		addButton(AnimationPath::builtin("TPMRKBU5.DEF"), CGI->generaltexth->zelp[605], std::bind(&CMarketWindow::createMarketResources, this, market, hero));
+		addButton(AnimationPath::builtin("TPMRKBU5.DEF"), CGI->generaltexth->zelp[605], std::bind(&CMarketWindow::createMarketResources, this, market, hero), EShortcut::MARKET_RESOURCE_RESOURCE);
 	
 	buttonPos = Point(516, 421);
 	if(isButtonVisible(EMarketMode::CREATURE_EXP))
 	{
-		addButton(AnimationPath::builtin("ALTSACC.DEF"), CGI->generaltexth->zelp[572], std::bind(&CMarketWindow::createAltarCreatures, this, market, hero));
+		addButton(AnimationPath::builtin("ALTSACC.DEF"), CGI->generaltexth->zelp[572], std::bind(&CMarketWindow::createAltarCreatures, this, market, hero), EShortcut::MARKET_CREATURE_EXPERIENCE);
 		if(marketWidget->hero->getAlignment() == EAlignment::GOOD)
 			changeModeButtons.back()->block(true);
 	}
 	if(isButtonVisible(EMarketMode::ARTIFACT_EXP))
 	{
-		addButton(AnimationPath::builtin("ALTART.DEF"), CGI->generaltexth->zelp[580], std::bind(&CMarketWindow::createAltarArtifacts, this, market, hero));
+		addButton(AnimationPath::builtin("ALTART.DEF"), CGI->generaltexth->zelp[580], std::bind(&CMarketWindow::createAltarArtifacts, this, market, hero), EShortcut::MARKET_ARTIFACT_EXPERIENCE);
 		if(marketWidget->hero->getAlignment() == EAlignment::EVIL)
 			changeModeButtons.back()->block(true);
 	}

+ 1 - 1
client/windows/CQuestLog.cpp

@@ -128,7 +128,7 @@ CQuestLog::CQuestLog (const std::vector<QuestInfo> & Quests)
 	minimap = std::make_shared<CQuestMinimap>(Rect(12, 12, 169, 169));
 	// TextBox have it's own 4 pixel padding from top at least for English. To achieve 10px from both left and top only add 6px margin
 	description = std::make_shared<CTextBox>("", Rect(205, 18, 385, DESCRIPTION_HEIGHT_MAX), CSlider::BROWN, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE);
-	ok = std::make_shared<CButton>(Point(539, 398), AnimationPath::builtin("IOKAY.DEF"), CGI->generaltexth->zelp[445], std::bind(&CQuestLog::close, this), EShortcut::GLOBAL_ACCEPT);
+	ok = std::make_shared<CButton>(Point(539, 398), AnimationPath::builtin("IOKAY.DEF"), CGI->generaltexth->zelp[445], std::bind(&CQuestLog::close, this), EShortcut::GLOBAL_RETURN);
 	// Both button and lable are shifted to -2px by x and y to not make them actually look like they're on same line with quests list and ok button
 	hideCompleteButton = std::make_shared<CToggleButton>(Point(10, 396), AnimationPath::builtin("sysopchk.def"), CButton::tooltipLocalized("vcmi.questLog.hideComplete"), std::bind(&CQuestLog::toggleComplete, this, _1));
 	hideCompleteLabel = std::make_shared<CLabel>(46, 398, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.questLog.hideComplete.hover"));

+ 4 - 4
client/windows/CTutorialWindow.cpp

@@ -12,7 +12,7 @@
 
 #include "../eventsSDL/InputHandler.h"
 #include "../../lib/CConfigHandler.h"
-#include "../../lib/CondSh.h"
+#include "../ConditionalWait.h"
 #include "../../lib/CGeneralTextHandler.h"
 #include "../CPlayerInterface.h"
 #include "../CGameInfo.h"
@@ -45,7 +45,7 @@ CTutorialWindow::CTutorialWindow(const TutorialMode & m)
 
 	labelTitle = std::make_shared<CLabel>(190, 15, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.tutorialWindow.title"));
 	labelInformation = std::make_shared<CMultiLineLabel>(Rect(5, 40, 370, 60), EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, "");
-	buttonOk = std::make_shared<CButton>(Point(159, 367), AnimationPath::builtin("IOKAY"), CButton::tooltip(), std::bind(&CTutorialWindow::exit, this), EShortcut::GLOBAL_ACCEPT); //62x28
+	buttonOk = std::make_shared<CButton>(Point(159, 367), AnimationPath::builtin("IOKAY"), CButton::tooltip(), std::bind(&CTutorialWindow::exit, this), EShortcut::GLOBAL_RETURN); //62x28
 	buttonLeft = std::make_shared<CButton>(Point(5, 217), AnimationPath::builtin("HSBTNS3"), CButton::tooltip(), std::bind(&CTutorialWindow::previous, this), EShortcut::MOVE_LEFT); //22x46
 	buttonRight = std::make_shared<CButton>(Point(352, 217), AnimationPath::builtin("HSBTNS5"), CButton::tooltip(), std::bind(&CTutorialWindow::next, this), EShortcut::MOVE_RIGHT); //22x46
 
@@ -70,7 +70,7 @@ void CTutorialWindow::openWindowFirstTime(const TutorialMode & m)
 	if(GH.input().hasTouchInputDevice() && !persistentStorage["gui"]["tutorialCompleted" + std::to_string(m)].Bool())
 	{
 		if(LOCPLINT)
-			LOCPLINT->showingDialog->set(true);
+			LOCPLINT->showingDialog->setBusy();
 		GH.windows().pushWindow(std::make_shared<CTutorialWindow>(m));
 
 		Settings s = persistentStorage.write["gui"]["tutorialCompleted" + std::to_string(m)];
@@ -81,7 +81,7 @@ void CTutorialWindow::openWindowFirstTime(const TutorialMode & m)
 void CTutorialWindow::exit()
 {
 	if(LOCPLINT)
-		LOCPLINT->showingDialog->setn(false);
+		LOCPLINT->showingDialog->setFree();
 
 	close();
 }

+ 5 - 2
client/windows/CWindowWithArtifacts.cpp

@@ -10,6 +10,11 @@
 #include "StdInc.h"
 #include "CWindowWithArtifacts.h"
 
+#include "CHeroWindow.h"
+#include "CSpellWindow.h"
+#include "CExchangeWindow.h"
+#include "CHeroBackpackWindow.h"
+
 #include "../gui/CGuiHandler.h"
 #include "../gui/CursorHandler.h"
 #include "../gui/WindowHandler.h"
@@ -20,8 +25,6 @@
 
 #include "../widgets/CComponent.h"
 
-#include "../windows/CSpellWindow.h"
-#include "../windows/CHeroBackpackWindow.h"
 #include "../CPlayerInterface.h"
 #include "../CGameInfo.h"
 

+ 1 - 1
client/windows/CreaturePurchaseCard.cpp

@@ -45,7 +45,7 @@ void CreaturePurchaseCard::initMinButton()
 
 void CreaturePurchaseCard::initCreatureSwitcherButton()
 {
-	creatureSwitcher = std::make_shared<CButton>(Point(pos.x + 18, pos.y-37), AnimationPath::builtin("iDv6432.def"), CButton::tooltip(), [&](){ switchCreatureLevel(); });
+	creatureSwitcher = std::make_shared<CButton>(Point(pos.x + 18, pos.y-37), AnimationPath::builtin("iDv6432.def"), CButton::tooltip(), [&](){ switchCreatureLevel(); }, EShortcut::RECRUITMENT_SWITCH_LEVEL);
 }
 
 void CreaturePurchaseCard::switchCreatureLevel()

部分文件因文件數量過多而無法顯示