2
0
Эх сурвалжийг харах

Merge branch 'develop-upstream' into town-buildings

# Conflicts:
#	client/CPlayerInterface.cpp
#	client/windows/CTradeWindow.cpp
#	config/objects/generic.json
#	lib/mapObjects/CGMarket.cpp
#	lib/mapObjects/CGMarket.h
#	lib/mapObjects/CGTownInstance.cpp
#	lib/mapObjects/CommonConstructors.cpp
#	lib/mapObjects/CommonConstructors.h
nordsoft 2 жил өмнө
parent
commit
0250e6fb92
100 өөрчлөгдсөн 2563 нэмэгдсэн , 1918 устгасан
  1. 2 8
      AI/BattleAI/BattleExchangeVariant.h
  2. 4 4
      AI/Nullkiller/AIUtility.h
  3. 1 6
      AI/Nullkiller/Analyzers/ArmyManager.h
  4. 5 2
      AI/Nullkiller/Analyzers/BuildAnalyzer.h
  5. 5 11
      AI/Nullkiller/Analyzers/ObjectClusterizer.h
  6. 1 1
      AI/Nullkiller/Engine/DeepDecomposer.h
  7. 1 1
      AI/Nullkiller/Engine/FuzzyHelper.h
  8. 6 2
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  9. 4 5
      AI/Nullkiller/Goals/AbstractGoal.h
  10. 2 2
      AI/Nullkiller/Pathfinding/AINodeStorage.h
  11. 1 3
      AI/Nullkiller/Pathfinding/Actors.h
  12. 3 3
      AI/VCAI/AIUtility.h
  13. 2 3
      AI/VCAI/Goals/AbstractGoal.h
  14. 9 6
      AI/VCAI/Goals/CollectRes.cpp
  15. 0 2
      AI/VCAI/Pathfinding/AINodeStorage.h
  16. 4 2
      Global.h
  17. 2 3
      Mods/vcmi/config/vcmi/rmg/symmetric/3sm3d.JSON
  18. 15 0
      client/CMakeLists.txt
  19. 1 1
      client/CMusicHandler.h
  20. 10 17
      client/CPlayerInterface.cpp
  21. 2 2
      client/Client.h
  22. 69 88
      client/adventureMap/CAdventureMapInterface.cpp
  23. 4 2
      client/adventureMap/CAdventureMapInterface.h
  24. 6 6
      client/adventureMap/CAdventureOptions.cpp
  25. 19 23
      client/adventureMap/CInGameConsole.cpp
  26. 1 1
      client/adventureMap/CInGameConsole.h
  27. 1 1
      client/adventureMap/CResDataBar.cpp
  28. 1 1
      client/battle/BattleFieldController.cpp
  29. 3 2
      client/battle/BattleInterfaceClasses.cpp
  30. 32 78
      client/battle/BattleWindow.cpp
  31. 1 6
      client/battle/BattleWindow.h
  32. 1 1
      client/battle/CreatureAnimation.h
  33. 28 77
      client/gui/CGuiHandler.cpp
  34. 6 9
      client/gui/CGuiHandler.h
  35. 9 14
      client/gui/CIntObject.cpp
  36. 8 10
      client/gui/CIntObject.h
  37. 67 35
      client/gui/InterfaceObjectConfigurable.cpp
  38. 16 2
      client/gui/InterfaceObjectConfigurable.h
  39. 156 0
      client/gui/Shortcut.h
  40. 362 0
      client/gui/ShortcutHandler.cpp
  41. 24 0
      client/gui/ShortcutHandler.h
  42. 4 3
      client/lobby/CBonusSelection.cpp
  43. 9 10
      client/lobby/CLobbyScreen.cpp
  44. 2 2
      client/lobby/CSavingScreen.cpp
  45. 2 1
      client/lobby/CScenarioInfoScreen.cpp
  46. 4 3
      client/lobby/CSelectionBase.cpp
  47. 1 1
      client/lobby/CSelectionBase.h
  48. 8 7
      client/lobby/SelectionTab.cpp
  49. 1 1
      client/lobby/SelectionTab.h
  50. 2 1
      client/mainmenu/CCampaignScreen.cpp
  51. 10 6
      client/mainmenu/CMainMenu.cpp
  52. 1 1
      client/renderSDL/CTrueTypeFont.h
  53. 21 21
      client/renderSDL/SDL_Extensions.h
  54. 2 2
      client/renderSDL/SDL_PixelAccess.h
  55. 12 11
      client/widgets/Buttons.cpp
  56. 3 3
      client/widgets/Buttons.h
  57. 168 923
      client/widgets/CArtifactHolder.cpp
  58. 30 123
      client/widgets/CArtifactHolder.h
  59. 109 0
      client/widgets/CArtifactsOfHeroAltar.cpp
  60. 32 0
      client/widgets/CArtifactsOfHeroAltar.h
  61. 278 0
      client/widgets/CArtifactsOfHeroBase.cpp
  62. 67 0
      client/widgets/CArtifactsOfHeroBase.h
  63. 54 0
      client/widgets/CArtifactsOfHeroKingdom.cpp
  64. 27 0
      client/widgets/CArtifactsOfHeroKingdom.h
  65. 35 0
      client/widgets/CArtifactsOfHeroMain.cpp
  66. 26 0
      client/widgets/CArtifactsOfHeroMain.h
  67. 41 0
      client/widgets/CArtifactsOfHeroMarket.cpp
  68. 21 0
      client/widgets/CArtifactsOfHeroMarket.h
  69. 4 2
      client/widgets/CComponent.cpp
  70. 5 8
      client/widgets/CGarrisonInt.cpp
  71. 363 0
      client/widgets/CWindowWithArtifacts.cpp
  72. 44 0
      client/widgets/CWindowWithArtifacts.h
  73. 1 1
      client/widgets/MiscWidgets.cpp
  74. 1 1
      client/widgets/ObjectLists.h
  75. 6 7
      client/widgets/TextControls.cpp
  76. 2 2
      client/widgets/TextControls.h
  77. 14 17
      client/windows/CCastleInterface.cpp
  78. 1 1
      client/windows/CCastleInterface.h
  79. 7 9
      client/windows/CCreatureWindow.cpp
  80. 21 22
      client/windows/CHeroWindow.cpp
  81. 3 3
      client/windows/CHeroWindow.h
  82. 8 8
      client/windows/CKingdomInterface.cpp
  83. 2 2
      client/windows/CKingdomInterface.h
  84. 2 2
      client/windows/CPuzzleWindow.cpp
  85. 2 1
      client/windows/CQuestLog.cpp
  86. 14 42
      client/windows/CSpellWindow.cpp
  87. 1 1
      client/windows/CSpellWindow.h
  88. 71 98
      client/windows/CTradeWindow.cpp
  89. 3 3
      client/windows/CTradeWindow.h
  90. 3 2
      client/windows/CreaturePurchaseCard.cpp
  91. 36 41
      client/windows/GUIClasses.cpp
  92. 3 3
      client/windows/GUIClasses.h
  93. 9 8
      client/windows/InfoWindows.cpp
  94. 2 2
      client/windows/InfoWindows.h
  95. 4 4
      client/windows/QuickRecruitmentWindow.cpp
  96. 16 16
      config/campaignSets.json
  97. 20 20
      config/mainmenu.json
  98. 20 10
      config/objects/generic.json
  99. 11 22
      config/widgets/battleWindow.json
  100. 0 1
      config/widgets/settings/generalOptionsTab.json

+ 2 - 8
AI/BattleAI/BattleExchangeVariant.h

@@ -56,10 +56,7 @@ struct EvaluationResult
 class BattleExchangeVariant
 {
 public:
-	BattleExchangeVariant()
-		:dpsScore(0), attackerValue()
-	{
-	}
+	BattleExchangeVariant(): dpsScore(0) {}
 
 	int64_t trackAttack(const AttackPossibility & ap, HypotheticBattle & state);
 
@@ -92,10 +89,7 @@ private:
 	std::vector<battle::Units> turnOrder;
 
 public:
-	BattleExchangeEvaluator(std::shared_ptr<CBattleInfoCallback> cb, std::shared_ptr<Environment> env)
-		:cb(cb), reachabilityMap(), env(env), turnOrder()
-	{
-	}
+	BattleExchangeEvaluator(std::shared_ptr<CBattleInfoCallback> cb, std::shared_ptr<Environment> env): cb(cb), env(env) {}
 
 	EvaluationResult findBestTarget(const battle::Unit * activeStack, PotentialTargets & targets, HypotheticBattle & hb);
 	int64_t calculateExchange(const AttackPossibility & ap, PotentialTargets & targets, HypotheticBattle & hb);

+ 4 - 4
AI/Nullkiller/AIUtility.h

@@ -54,7 +54,7 @@
 
 using namespace tbb;
 
-typedef std::pair<ui32, std::vector<CreatureID>> dwellingContent;
+using dwellingContent = std::pair<ui32, std::vector<CreatureID>>;
 
 namespace NKAI
 {
@@ -305,10 +305,10 @@ public:
 public:
 	using ptr_type = std::unique_ptr<T, External_Deleter>;
 
-	SharedPool(std::function<std::unique_ptr<T>()> elementFactory)
-		: elementFactory(elementFactory), pool(), sync(), instance_tracker(new SharedPool<T>*(this))
+	SharedPool(std::function<std::unique_ptr<T>()> elementFactory):
+		elementFactory(elementFactory), pool(), instance_tracker(new SharedPool<T> *(this))
 	{}
-	
+
 	void add(std::unique_ptr<T> t)
 	{
 		boost::lock_guard<boost::mutex> lock(sync);

+ 1 - 6
AI/Nullkiller/Analyzers/ArmyManager.h

@@ -32,13 +32,8 @@ struct SlotInfo
 struct ArmyUpgradeInfo
 {
 	std::vector<SlotInfo> resultingArmy;
-	uint64_t upgradeValue;
+	uint64_t upgradeValue = 0;
 	TResources upgradeCost;
-
-	ArmyUpgradeInfo()
-		: resultingArmy(), upgradeValue(0), upgradeCost()
-	{
-	}
 };
 
 class DLL_EXPORT IArmyManager //: public: IAbstractManager

+ 5 - 2
AI/Nullkiller/Analyzers/BuildAnalyzer.h

@@ -62,8 +62,11 @@ public:
 	HeroRole townRole;
 	bool hasSomethingToBuild;
 
-	TownDevelopmentInfo(const CGTownInstance* town)
-		:town(town), armyStrength(0), toBuild(), townDevelopmentCost(), requiredResources(), townRole(HeroRole::SCOUT), hasSomethingToBuild(false)
+	TownDevelopmentInfo(const CGTownInstance * town):
+		town(town),
+		armyStrength(0),
+		townRole(HeroRole::SCOUT),
+		hasSomethingToBuild(false)
 	{
 	}
 

+ 5 - 11
AI/Nullkiller/Analyzers/ObjectClusterizer.h

@@ -22,7 +22,7 @@ struct ClusterObjectInfo
 	uint8_t turn;
 };
 
-typedef tbb::concurrent_hash_map<const CGObjectInstance *, ClusterObjectInfo> ClusterObjects;
+using ClusterObjects = tbb::concurrent_hash_map<const CGObjectInstance *, ClusterObjectInfo>;
 
 struct ObjectCluster
 {
@@ -36,11 +36,8 @@ public:
 	}
 
 	void addObject(const CGObjectInstance * object, const AIPath & path, float priority);
-	
-	ObjectCluster(const CGObjectInstance * blocker)
-		:objects(), blocker(blocker)
-	{
-	}
+
+	ObjectCluster(const CGObjectInstance * blocker): blocker(blocker) {}
 
 	ObjectCluster() : ObjectCluster(nullptr)
 	{
@@ -50,7 +47,7 @@ public:
 	const CGObjectInstance * calculateCenter() const;
 };
 
-typedef tbb::concurrent_hash_map<const CGObjectInstance *, std::shared_ptr<ObjectCluster>> ClusterMap;
+using ClusterMap = tbb::concurrent_hash_map<const CGObjectInstance *, std::shared_ptr<ObjectCluster>>;
 
 class ObjectClusterizer
 {
@@ -67,10 +64,7 @@ public:
 	std::vector<std::shared_ptr<ObjectCluster>> getLockedClusters() const;
 	const CGObjectInstance * getBlocker(const AIPath & path) const;
 
-	ObjectClusterizer(const Nullkiller * ai)
-		:nearObjects(), farObjects(), blockedObjects(), ai(ai)
-	{
-	}
+	ObjectClusterizer(const Nullkiller * ai): ai(ai) {}
 
 private:
 	bool shouldVisitObject(const CGObjectInstance * obj) const;

+ 1 - 1
AI/Nullkiller/Engine/DeepDecomposer.h

@@ -22,7 +22,7 @@ struct GoalHash
 	}
 };
 
-typedef std::unordered_map<Goals::TSubgoal, Goals::TGoalVec, GoalHash> TGoalHashSet;
+using TGoalHashSet = std::unordered_map<Goals::TSubgoal, Goals::TGoalVec, GoalHash>;
 
 class DeepDecomposer
 {

+ 1 - 1
AI/Nullkiller/Engine/FuzzyHelper.h

@@ -28,7 +28,7 @@ private:
 	TacticalAdvantageEngine tacticalAdvantageEngine;
 
 public:
-	FuzzyHelper(const Nullkiller * ai) : ai(ai), tacticalAdvantageEngine() {}
+	FuzzyHelper(const Nullkiller * ai): ai(ai) {}
 
 	ui64 estimateBankDanger(const CBank * bank); //TODO: move to another class?
 

+ 6 - 2
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -297,6 +297,12 @@ int RewardEvaluator::getGoldCost(const CGObjectInstance * target, const CGHeroIn
 {
 	if(!target)
 		return 0;
+	
+	if(auto * m = dynamic_cast<const IMarket *>(target))
+	{
+		if(m->allowsTrade(EMarketMode::RESOURCE_SKILL))
+			return 2000;
+	}
 
 	switch(target->ID)
 	{
@@ -305,8 +311,6 @@ int RewardEvaluator::getGoldCost(const CGObjectInstance * target, const CGHeroIn
 	case Obj::SCHOOL_OF_MAGIC:
 	case Obj::SCHOOL_OF_WAR:
 		return 1000;
-	case Obj::UNIVERSITY:
-		return 2000;
 	case Obj::CREATURE_GENERATOR1:
 	case Obj::CREATURE_GENERATOR2:
 	case Obj::CREATURE_GENERATOR3:

+ 4 - 5
AI/Nullkiller/Goals/AbstractGoal.h

@@ -81,9 +81,9 @@ namespace Goals
 		bool operator<(const TSubgoal & rhs) const;
 	};
 
-	typedef std::shared_ptr<ITask> TTask;
-	typedef std::vector<TTask> TTaskVec;
-	typedef std::vector<TSubgoal> TGoalVec;
+	using TTask = std::shared_ptr<ITask>;
+	using TTaskVec = std::vector<TTask>;
+	using TGoalVec = std::vector<TSubgoal>;
 
 	//method chaining + clone pattern
 #define SETTER(type, field) AbstractGoal & set ## field(const type &rhs) {field = rhs; return *this;};
@@ -107,8 +107,7 @@ namespace Goals
 		const CGTownInstance *town; SETTER(CGTownInstance *, town)
 		int bid; SETTER(int, bid)
 
-		AbstractGoal(EGoals goal = EGoals::INVALID)
-			: goalType(goal), hero()
+		AbstractGoal(EGoals goal = EGoals::INVALID): goalType(goal)
 		{
 			isAbstract = false;
 			value = 0;

+ 2 - 2
AI/Nullkiller/Pathfinding/AINodeStorage.h

@@ -205,14 +205,14 @@ public:
 
 	inline void updateAINode(CGPathNode * node, std::function<void (AIPathNode *)> updater)
 	{
-		auto aiNode = static_cast<AIPathNode *>(node);
+		auto * aiNode = static_cast<AIPathNode *>(node);
 
 		updater(aiNode);
 	}
 
 	inline const CGHeroInstance * getHero(const CGPathNode * node) const
 	{
-		auto aiNode = getAINode(node);
+		const auto * aiNode = getAINode(node);
 
 		return aiNode->actor->hero;
 	}

+ 1 - 3
AI/Nullkiller/Pathfinding/Actors.h

@@ -32,9 +32,7 @@ public:
 	virtual bool needsLastStack() const override;
 	std::shared_ptr<SpecialAction> getActorAction() const;
 
-	HeroExchangeArmy() : CArmedInstance(true), armyCost(), requireBuyArmy(false)
-	{
-	}
+	HeroExchangeArmy(): CArmedInstance(true), requireBuyArmy(false) {}
 };
 
 struct ExchangeResult

+ 3 - 3
AI/VCAI/AIUtility.h

@@ -23,9 +23,9 @@
 class CCallback;
 struct creInfo;
 
-typedef const int3 & crint3;
-typedef const std::string & crstring;
-typedef std::pair<ui32, std::vector<CreatureID>> dwellingContent;
+using crint3 = const int3 &;
+using crstring = const std::string &;
+using dwellingContent = std::pair<ui32, std::vector<CreatureID>>;
 
 const int GOLD_MINE_PRODUCTION = 1000, WOOD_ORE_MINE_PRODUCTION = 2, RESOURCE_MINE_PRODUCTION = 1;
 const int ACTUAL_RESOURCE_COUNT = 7;

+ 2 - 3
AI/VCAI/Goals/AbstractGoal.h

@@ -76,7 +76,7 @@ namespace Goals
 		//TODO: serialize?
 	};
 
-	typedef std::vector<TSubgoal> TGoalVec;
+	using TGoalVec = std::vector<TSubgoal>;
 
 	//method chaining + clone pattern
 #define VSETTER(type, field) virtual AbstractGoal & set ## field(const type &rhs) {field = rhs; return *this;};
@@ -121,8 +121,7 @@ namespace Goals
 			TSubgoal parent; VSETTER(TSubgoal, parent)
 			EvaluationContext evaluationContext; VSETTER(EvaluationContext, evaluationContext)
 
-			AbstractGoal(EGoals goal = EGoals::INVALID)
-			: goalType(goal), evaluationContext()
+		AbstractGoal(EGoals goal = EGoals::INVALID): goalType(goal)
 		{
 			priority = 0;
 			isElementar = false;

+ 9 - 6
AI/VCAI/Goals/CollectRes.cpp

@@ -131,13 +131,16 @@ TSubgoal CollectRes::whatToDoToTrade()
 
 	std::vector<const CGObjectInstance *> visObjs;
 	ai->retrieveVisitableObjs(visObjs, true);
-	for (const CGObjectInstance * obj : visObjs)
+	for(const CGObjectInstance * obj : visObjs)
 	{
-		if (const IMarket * m = IMarket::castFrom(obj, false))
+		if(const IMarket * m = IMarket::castFrom(obj, false); m->allowsTrade(EMarketMode::RESOURCE_RESOURCE))
 		{
-			if (obj->ID == Obj::TOWN && obj->tempOwner == ai->playerID && m->allowsTrade(EMarketMode::RESOURCE_RESOURCE))
-				markets.push_back(m);
-			else if (obj->ID == Obj::TRADING_POST)
+			if(obj->ID == Obj::TOWN)
+			{
+				if(obj->tempOwner == ai->playerID)
+					markets.push_back(m);
+			}
+			else
 				markets.push_back(m);
 		}
 	}
@@ -171,7 +174,7 @@ TSubgoal CollectRes::whatToDoToTrade()
 		const IMarket * m = markets.back();
 		//attempt trade at back (best prices)
 		int howManyCanWeBuy = 0;
-		for (auto i = EGameResID::WOOD; i <= EGameResID::GOLD; vstd::advance(i, 1))
+		for (GameResID i = EGameResID::WOOD; i <= EGameResID::GOLD; ++i)
 		{
 			if (GameResID(i) == resID)
 				continue;

+ 0 - 2
AI/VCAI/Pathfinding/AINodeStorage.h

@@ -19,8 +19,6 @@
 
 class CCallback;
 
-extern boost::thread_specific_ptr<CCallback> cb; //for templates
-
 struct AIPathNode : public CGPathNode
 {
 	uint32_t chainMask;

+ 4 - 2
Global.h

@@ -543,10 +543,12 @@ namespace vstd
 		});
 	}
 
+	/// Increments value by specific delta
+	/// similar to std::next but works with other types, e.g. enum class
 	template<typename T>
-	void advance(T &obj, int change)
+	T next(const T &obj, int change)
 	{
-		obj = (T)(((int)obj) + change);
+		return static_cast<T>(static_cast<ptrdiff_t>(obj) + change);
 	}
 
 	template <typename Container>

+ 2 - 3
Mods/vcmi/config/vcmi/rmg/symmetric/3sm3d.JSON

@@ -112,9 +112,8 @@
 				"matchTerrainToTown" : false,
 				"treasure" :
 				[
-					{ "min" : 0, "max" : 0, "density" : 1 },
-					{ "min" : 0, "max" : 0, "density" : 1 },
-					{ "min" : 0, "max" : 0, "density" : 1 }
+					{ "min" : 100, "max" : 1000, "density" : 10 },
+					{ "min" : 1000, "max" : 3000, "density" : 1 }
 				]
 			},
 			"12" :

+ 15 - 0
client/CMakeLists.txt

@@ -31,6 +31,7 @@ set(client_SRCS
 	gui/CursorHandler.cpp
 	gui/InterfaceObjectConfigurable.cpp
 	gui/NotificationHandler.cpp
+	gui/ShortcutHandler.cpp
 
 	lobby/CBonusSelection.cpp
 	lobby/CCampaignInfoScreen.cpp
@@ -85,6 +86,12 @@ set(client_SRCS
 	widgets/MiscWidgets.cpp
 	widgets/ObjectLists.cpp
 	widgets/TextControls.cpp
+	widgets/CArtifactsOfHeroBase.cpp
+	widgets/CArtifactsOfHeroMain.cpp
+	widgets/CArtifactsOfHeroKingdom.cpp
+	widgets/CArtifactsOfHeroAltar.cpp
+	widgets/CArtifactsOfHeroMarket.cpp
+	widgets/CWindowWithArtifacts.cpp
 
 	windows/CCastleInterface.cpp
 	windows/CCreatureWindow.cpp
@@ -153,6 +160,8 @@ set(client_HEADERS
 	gui/InterfaceObjectConfigurable.h
 	gui/MouseButton.h
 	gui/NotificationHandler.h
+	gui/Shortcut.h
+	gui/ShortcutHandler.h
 	gui/TextAlignment.h
 
 	lobby/CBonusSelection.h
@@ -214,6 +223,12 @@ set(client_HEADERS
 	widgets/MiscWidgets.h
 	widgets/ObjectLists.h
 	widgets/TextControls.h
+	widgets/CArtifactsOfHeroBase.h
+	widgets/CArtifactsOfHeroMain.h
+	widgets/CArtifactsOfHeroKingdom.h
+	widgets/CArtifactsOfHeroAltar.h
+	widgets/CArtifactsOfHeroMarket.h
+	widgets/CWindowWithArtifacts.h
 
 	windows/CCastleInterface.h
 	windows/CCreatureWindow.h

+ 1 - 1
client/CMusicHandler.h

@@ -14,7 +14,7 @@
 
 struct _Mix_Music;
 struct SDL_RWops;
-typedef struct _Mix_Music Mix_Music;
+using Mix_Music = struct _Mix_Music;
 struct Mix_Chunk;
 
 class CAudioBase {

+ 10 - 17
client/CPlayerInterface.cpp

@@ -1709,22 +1709,15 @@ void CPlayerInterface::stopMovement()
 void CPlayerInterface::showMarketWindow(const IMarket *market, const CGHeroInstance *visitor)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	auto * o = dynamic_cast<const CGObjectInstance *>(market);
-	if(o && o->ID == Obj::ALTAR_OF_SACRIFICE)
-	{
-		//EEMarketMode mode = market->availableModes().front();
-		if (market->allowsTrade(EMarketMode::ARTIFACT_EXP) && visitor->getAlignment() != EAlignment::EVIL)
-			GH.pushIntT<CAltarWindow>(market, visitor, EMarketMode::ARTIFACT_EXP);
-		else if (market->allowsTrade(EMarketMode::CREATURE_EXP) && visitor->getAlignment() != EAlignment::GOOD)
-			GH.pushIntT<CAltarWindow>(market, visitor, EMarketMode::CREATURE_EXP);
-	}
-	else
-	{
-		if(market->allowsTrade(EMarketMode::CREATURE_UNDEAD))
-			GH.pushIntT<CTransformerWindow>(market, visitor);
-		else
-			GH.pushIntT<CMarketplaceWindow>(market, visitor, market->availableModes().front());
-	}
+
+	if(market->allowsTrade(EMarketMode::ARTIFACT_EXP) && visitor->getAlignment() != EAlignment::EVIL)
+		GH.pushIntT<CAltarWindow>(market, visitor, EMarketMode::ARTIFACT_EXP);
+	else if(market->allowsTrade(EMarketMode::CREATURE_EXP) && visitor->getAlignment() != EAlignment::GOOD)
+		GH.pushIntT<CAltarWindow>(market, visitor, EMarketMode::CREATURE_EXP);
+	else if(market->allowsTrade(EMarketMode::CREATURE_UNDEAD))
+		GH.pushIntT<CTransformerWindow>(market, visitor);
+	else if(!market->availableModes().empty())
+		GH.pushIntT<CMarketplaceWindow>(market, visitor, market->availableModes().front());
 }
 
 void CPlayerInterface::showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor)
@@ -1796,7 +1789,7 @@ void CPlayerInterface::askToAssembleArtifact(const ArtifactLocation &al)
 							 al.slot.num);
 			return;
 		}
-		CHeroArtPlace::askToAssemble(hero, al.slot);
+		ArtifactUtilsClient::askToAssemble(hero, al.slot);
 	}
 }
 

+ 2 - 2
client/Client.h

@@ -61,8 +61,8 @@ namespace boost { class thread; }
 template<typename T>
 class ThreadSafeVector
 {
-	typedef std::vector<T> TVector;
-	typedef boost::unique_lock<boost::mutex> TLock;
+	using TVector = std::vector<T>;
+	using TLock = boost::unique_lock<boost::mutex>;
 	TVector items;
 	boost::mutex mx;
 	boost::condition_variable cond;

+ 69 - 88
client/adventureMap/CAdventureMapInterface.cpp

@@ -33,6 +33,7 @@
 #include "../gui/CursorHandler.h"
 #include "../render/IImage.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../widgets/TextControls.h"
 #include "../widgets/Buttons.h"
 #include "../windows/settings/SettingsMainWindow.h"
@@ -95,7 +96,7 @@ CAdventureMapInterface::CAdventureMapInterface():
 		gems.push_back(std::make_shared<CAnimImage>(ADVOPT.gemG[g], 0, 0, ADVOPT.gemX[g], ADVOPT.gemY[g]));
 	}
 
-	auto makeButton = [&](int textID, std::function<void()> callback, config::ButtonInfo info, int key) -> std::shared_ptr<CButton>
+	auto makeButton = [&](int textID, std::function<void()> callback, config::ButtonInfo info, EShortcut key) -> std::shared_ptr<CButton>
 	{
 		auto button = std::make_shared<CButton>(Point(info.x, info.y), info.defName, CGI->generaltexth->zelp[textID], callback, key, info.playerColoured);
 		for(auto image : info.additionalDefs)
@@ -103,16 +104,16 @@ CAdventureMapInterface::CAdventureMapInterface():
 		return button;
 	};
 
-	kingOverview = makeButton(293, std::bind(&CAdventureMapInterface::fshowOverview,this),     ADVOPT.kingOverview, SDLK_k);
-	underground  = makeButton(294, std::bind(&CAdventureMapInterface::fswitchLevel,this),      ADVOPT.underground,  SDLK_u);
-	questlog     = makeButton(295, std::bind(&CAdventureMapInterface::fshowQuestlog,this),     ADVOPT.questlog,     SDLK_q);
-	sleepWake    = makeButton(296, std::bind(&CAdventureMapInterface::fsleepWake,this),        ADVOPT.sleepWake,    SDLK_w);
-	moveHero     = makeButton(297, std::bind(&CAdventureMapInterface::fmoveHero,this),         ADVOPT.moveHero,     SDLK_m);
-	spellbook    = makeButton(298, std::bind(&CAdventureMapInterface::fshowSpellbok,this),     ADVOPT.spellbook,    SDLK_c);
-	advOptions   = makeButton(299, std::bind(&CAdventureMapInterface::fadventureOPtions,this), ADVOPT.advOptions,   SDLK_a);
-	sysOptions   = makeButton(300, std::bind(&CAdventureMapInterface::fsystemOptions,this),    ADVOPT.sysOptions,   SDLK_o);
-	nextHero     = makeButton(301, std::bind(&CAdventureMapInterface::fnextHero,this),         ADVOPT.nextHero,     SDLK_h);
-	endTurn      = makeButton(302, std::bind(&CAdventureMapInterface::fendTurn,this),          ADVOPT.endTurn,      SDLK_e);
+	kingOverview = makeButton(293, std::bind(&CAdventureMapInterface::fshowOverview,this),     ADVOPT.kingOverview, EShortcut::ADVENTURE_KINGDOM_OVERVIEW);
+	underground  = makeButton(294, std::bind(&CAdventureMapInterface::fswitchLevel,this),      ADVOPT.underground,  EShortcut::ADVENTURE_TOGGLE_MAP_LEVEL);
+	questlog     = makeButton(295, std::bind(&CAdventureMapInterface::fshowQuestlog,this),     ADVOPT.questlog,     EShortcut::ADVENTURE_QUEST_LOG);
+	sleepWake    = makeButton(296, std::bind(&CAdventureMapInterface::fsleepWake,this),        ADVOPT.sleepWake,    EShortcut::ADVENTURE_TOGGLE_SLEEP);
+	moveHero     = makeButton(297, std::bind(&CAdventureMapInterface::fmoveHero,this),         ADVOPT.moveHero,     EShortcut::ADVENTURE_MOVE_HERO);
+	spellbook    = makeButton(298, std::bind(&CAdventureMapInterface::fshowSpellbok,this),     ADVOPT.spellbook,    EShortcut::ADVENTURE_CAST_SPELL);
+	advOptions   = makeButton(299, std::bind(&CAdventureMapInterface::fadventureOPtions,this), ADVOPT.advOptions,   EShortcut::ADVENTURE_GAME_OPTIONS);
+	sysOptions   = makeButton(300, std::bind(&CAdventureMapInterface::fsystemOptions,this),    ADVOPT.sysOptions,   EShortcut::GLOBAL_OPTIONS);
+	nextHero     = makeButton(301, std::bind(&CAdventureMapInterface::fnextHero,this),         ADVOPT.nextHero,     EShortcut::ADVENTURE_NEXT_HERO);
+	endTurn      = makeButton(302, std::bind(&CAdventureMapInterface::fendTurn,this),          ADVOPT.endTurn,      EShortcut::ADVENTURE_END_TURN);
 
 	int panelSpaceBottom = GH.screenDimensions().y - resdatabar->pos.h - 4;
 
@@ -139,7 +140,7 @@ CAdventureMapInterface::CAdventureMapInterface():
 	worldViewBackConfig.y = 343 + 195;
 	worldViewBackConfig.playerColoured = false;
 	panelWorldView->addChildToPanel(
-		makeButton(288, std::bind(&CAdventureMapInterface::fworldViewBack,this), worldViewBackConfig, SDLK_ESCAPE), ACTIVATE | DEACTIVATE);
+		makeButton(288, std::bind(&CAdventureMapInterface::fworldViewBack,this), worldViewBackConfig, EShortcut::GLOBAL_CANCEL), ACTIVATE | DEACTIVATE);
 
 	config::ButtonInfo worldViewPuzzleConfig = config::ButtonInfo();
 	worldViewPuzzleConfig.defName = "VWPUZ.DEF";
@@ -148,7 +149,7 @@ CAdventureMapInterface::CAdventureMapInterface():
 	worldViewPuzzleConfig.playerColoured = false;
 	panelWorldView->addChildToPanel( // no help text for this one
 		std::make_shared<CButton>(Point(worldViewPuzzleConfig.x, worldViewPuzzleConfig.y), worldViewPuzzleConfig.defName, std::pair<std::string, std::string>(),
-				std::bind(&CPlayerInterface::showPuzzleMap,LOCPLINT), SDLK_p, worldViewPuzzleConfig.playerColoured), ACTIVATE | DEACTIVATE);
+				std::bind(&CPlayerInterface::showPuzzleMap,LOCPLINT), EShortcut::ADVENTURE_VIEW_PUZZLE, worldViewPuzzleConfig.playerColoured), ACTIVATE | DEACTIVATE);
 
 	config::ButtonInfo worldViewScale1xConfig = config::ButtonInfo();
 	worldViewScale1xConfig.defName = "VWMAG1.DEF";
@@ -156,7 +157,7 @@ CAdventureMapInterface::CAdventureMapInterface():
 	worldViewScale1xConfig.y = 23 + 195;
 	worldViewScale1xConfig.playerColoured = false;
 	panelWorldView->addChildToPanel( // help text is wrong for this button
-		makeButton(291, std::bind(&CAdventureMapInterface::fworldViewScale1x,this), worldViewScale1xConfig, SDLK_1), ACTIVATE | DEACTIVATE);
+		makeButton(291, std::bind(&CAdventureMapInterface::fworldViewScale1x,this), worldViewScale1xConfig, EShortcut::SELECT_INDEX_1), ACTIVATE | DEACTIVATE);
 
 	config::ButtonInfo worldViewScale2xConfig = config::ButtonInfo();
 	worldViewScale2xConfig.defName = "VWMAG2.DEF";
@@ -164,7 +165,7 @@ CAdventureMapInterface::CAdventureMapInterface():
 	worldViewScale2xConfig.y = 23 + 195;
 	worldViewScale2xConfig.playerColoured = false;
 	panelWorldView->addChildToPanel( // help text is wrong for this button
-		makeButton(291, std::bind(&CAdventureMapInterface::fworldViewScale2x,this), worldViewScale2xConfig, SDLK_2), ACTIVATE | DEACTIVATE);
+		makeButton(291, std::bind(&CAdventureMapInterface::fworldViewScale2x,this), worldViewScale2xConfig, EShortcut::SELECT_INDEX_2), ACTIVATE | DEACTIVATE);
 
 	config::ButtonInfo worldViewScale4xConfig = config::ButtonInfo();
 	worldViewScale4xConfig.defName = "VWMAG4.DEF";
@@ -172,7 +173,7 @@ CAdventureMapInterface::CAdventureMapInterface():
 	worldViewScale4xConfig.y = 23 + 195;
 	worldViewScale4xConfig.playerColoured = false;
 	panelWorldView->addChildToPanel( // help text is wrong for this button
-		makeButton(291, std::bind(&CAdventureMapInterface::fworldViewScale4x,this), worldViewScale4xConfig, SDLK_4), ACTIVATE | DEACTIVATE);
+		makeButton(291, std::bind(&CAdventureMapInterface::fworldViewScale4x,this), worldViewScale4xConfig, EShortcut::SELECT_INDEX_4), ACTIVATE | DEACTIVATE);
 
 	config::ButtonInfo worldViewUndergroundConfig = config::ButtonInfo();
 	worldViewUndergroundConfig.defName = "IAM010.DEF";
@@ -180,7 +181,7 @@ CAdventureMapInterface::CAdventureMapInterface():
 	worldViewUndergroundConfig.x = GH.screenDimensions().x - 115;
 	worldViewUndergroundConfig.y = 343 + 195;
 	worldViewUndergroundConfig.playerColoured = true;
-	worldViewUnderground = makeButton(294, std::bind(&CAdventureMapInterface::fswitchLevel,this), worldViewUndergroundConfig, SDLK_u);
+	worldViewUnderground = makeButton(294, std::bind(&CAdventureMapInterface::fswitchLevel,this), worldViewUndergroundConfig, EShortcut::ADVENTURE_TOGGLE_MAP_LEVEL);
 	panelWorldView->addChildColorableButton(worldViewUnderground);
 
 	onCurrentPlayerChanged(LOCPLINT->playerID);
@@ -392,7 +393,6 @@ void CAdventureMapInterface::updateButtons()
 	{
 		bool state = LOCPLINT->localState->isHeroSleeping(hero);
 		sleepWake->setIndex(state ? 1 : 0, true);
-		sleepWake->assignedKeys = {state ? SDLK_w : SDLK_z};
 		sleepWake->redraw();
 	}
 }
@@ -607,17 +607,20 @@ void CAdventureMapInterface::centerOnObject(const CGObjectInstance * obj)
 	terrain->onCenteredObject(obj);
 }
 
-void CAdventureMapInterface::keyPressed(const SDL_Keycode & key)
+void CAdventureMapInterface::keyPressed(EShortcut key)
 {
 	if (state != EGameState::MAKING_TURN)
 		return;
 
+	//fake mouse use to trigger onTileHovered()
+	GH.fakeMouseMove();
+
 	const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero(); //selected hero
 	const CGTownInstance *t = LOCPLINT->localState->getCurrentTown(); //selected town
 
 	switch(key)
 	{
-	case SDLK_g:
+	case EShortcut::ADVENTURE_THIEVES_GUILD:
 		if(GH.topInt()->type & BLOCK_ADV_HOTKEYS)
 			return;
 
@@ -634,40 +637,40 @@ void CAdventureMapInterface::keyPressed(const SDL_Keycode & key)
 				LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithTavern"));
 		}
 		return;
-	case SDLK_i:
+	case EShortcut::ADVENTURE_VIEW_SCENARIO:
 		if(isActive())
 			CAdventureOptions::showScenarioInfo();
 		return;
-	case SDLK_s:
+	case EShortcut::GAME_SAVE_GAME:
 		if(isActive())
 			GH.pushIntT<CSavingScreen>();
 		return;
-	case SDLK_l:
+	case EShortcut::GAME_LOAD_GAME:
 		if(isActive())
 			LOCPLINT->proposeLoadingGame();
 		return;
-	case SDLK_d:
+	case EShortcut::ADVENTURE_DIG_GRAIL:
 		{
 			if(h && isActive() && LOCPLINT->makingTurn)
 				LOCPLINT->tryDiggging(h);
 			return;
 		}
-	case SDLK_p:
+	case EShortcut::ADVENTURE_VIEW_PUZZLE:
 		if(isActive())
 			LOCPLINT->showPuzzleMap();
 		return;
-	case SDLK_v:
+	case EShortcut::ADVENTURE_VIEW_WORLD:
 		if(isActive())
 			LOCPLINT->viewWorldMap();
 		return;
-	case SDLK_r:
+	case EShortcut::GAME_RESTART_GAME:
 		if(isActive() && GH.isKeyboardCtrlDown())
 		{
 			LOCPLINT->showYesNoDialog(CGI->generaltexth->translate("vcmi.adventureMap.confirmRestartGame"),
 				[](){ GH.pushUserEvent(EUserEvent::RESTART_GAME); }, nullptr);
 		}
 		return;
-	case SDLK_SPACE: //space - try to revisit current object with selected hero
+	case EShortcut::ADVENTURE_VISIT_OBJECT: //space - try to revisit current object with selected hero
 		{
 			if(!isActive())
 				return;
@@ -677,7 +680,7 @@ void CAdventureMapInterface::keyPressed(const SDL_Keycode & key)
 			}
 		}
 		return;
-	case SDLK_RETURN:
+	case EShortcut::ADVENTURE_VIEW_SELECTED:
 		{
 			if(!isActive() || !LOCPLINT->localState->getCurrentArmy())
 				return;
@@ -687,7 +690,7 @@ void CAdventureMapInterface::keyPressed(const SDL_Keycode & key)
 				LOCPLINT->openTownWindow(t);
 			return;
 		}
-	case SDLK_ESCAPE:
+	case EShortcut::GLOBAL_CANCEL:
 		{
 			//FIXME: this case is never executed since AdvMapInt is disabled while in spellcasting mode
 			if(!isActive() || GH.topInt().get() != this || !spellBeingCasted)
@@ -696,7 +699,7 @@ void CAdventureMapInterface::keyPressed(const SDL_Keycode & key)
 			abortCastingMode();
 			return;
 		}
-	case SDLK_t:
+	case EShortcut::GAME_OPEN_MARKETPLACE:
 		{
 			//act on key down if marketplace windows is not already opened
 			if(GH.topInt()->type & BLOCK_ADV_HOTKEYS)
@@ -720,77 +723,55 @@ void CAdventureMapInterface::keyPressed(const SDL_Keycode & key)
 				else //if not - complain
 					LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket"));
 			}
-			else if(isActive()) //no ctrl, advmapint is on the top => switch to town
+	case EShortcut::ADVENTURE_NEXT_TOWN:
+			if(isActive() && !GH.isKeyboardCtrlDown()) //no ctrl, advmapint is on the top => switch to town
 			{
 				townList->selectNext();
 			}
 			return;
 		}
-	case SDLK_LALT:
-	case SDLK_RALT:
-		{
-			//fake mouse use to trigger onTileHovered()
-			GH.fakeMouseMove();
-			return;
-		}
-	default:
-		{
-			auto direction = keyToMoveDirection(key);
-
-			if (!direction)
-				return;
-
-			if(!h || !isActive())
-				return;
+	case EShortcut::ADVENTURE_MOVE_HERO_SW: return hotkeyMoveHeroDirectional({-1, +1});
+	case EShortcut::ADVENTURE_MOVE_HERO_SS: return hotkeyMoveHeroDirectional({ 0, +1});
+	case EShortcut::ADVENTURE_MOVE_HERO_SE: return hotkeyMoveHeroDirectional({+1, +1});
+	case EShortcut::ADVENTURE_MOVE_HERO_WW: return hotkeyMoveHeroDirectional({-1,  0});
+	case EShortcut::ADVENTURE_MOVE_HERO_EE: return hotkeyMoveHeroDirectional({+1,  0});
+	case EShortcut::ADVENTURE_MOVE_HERO_NW: return hotkeyMoveHeroDirectional({-1, -1});
+	case EShortcut::ADVENTURE_MOVE_HERO_NN: return hotkeyMoveHeroDirectional({ 0, -1});
+	case EShortcut::ADVENTURE_MOVE_HERO_NE: return hotkeyMoveHeroDirectional({+1, -1});
+	}
+}
 
-			if (CGI->mh->hasOngoingAnimations())
-				return;
+void CAdventureMapInterface::hotkeyMoveHeroDirectional(Point direction)
+{
+	const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero(); //selected hero
 
-			if(*direction == Point(0,0))
-			{
-				centerOnObject(h);
-				return;
-			}
+	if(!h || !isActive())
+		return;
 
-			int3 dst = h->visitablePos() + int3(direction->x, direction->y, 0);
+	if (CGI->mh->hasOngoingAnimations())
+		return;
 
-			if (!CGI->mh->isInMap((dst)))
-				return;
+	if(direction == Point(0,0))
+	{
+		centerOnObject(h);
+		return;
+	}
 
-			if ( !LOCPLINT->localState->setPath(h, dst))
-				return;
+	int3 dst = h->visitablePos() + int3(direction.x, direction.y, 0);
 
-			const CGPath & path = LOCPLINT->localState->getPath(h);
+	if (!CGI->mh->isInMap((dst)))
+		return;
 
-			if (path.nodes.size() > 2)
-				onHeroChanged(h);
-			else
-			if(!path.nodes[0].turns)
-				LOCPLINT->moveHero(h, path);
-		}
+	if ( !LOCPLINT->localState->setPath(h, dst))
 		return;
-	}
-}
 
-std::optional<Point> CAdventureMapInterface::keyToMoveDirection(const SDL_Keycode & key)
-{
-	switch (key) {
-		case SDLK_DOWN:  return Point( 0, +1);
-		case SDLK_LEFT:  return Point(-1,  0);
-		case SDLK_RIGHT: return Point(+1,  0);
-		case SDLK_UP:    return Point( 0, -1);
-
-		case SDLK_KP_1: return Point(-1, +1);
-		case SDLK_KP_2: return Point( 0, +1);
-		case SDLK_KP_3: return Point(+1, +1);
-		case SDLK_KP_4: return Point(-1,  0);
-		case SDLK_KP_5: return Point( 0,  0);
-		case SDLK_KP_6: return Point(+1,  0);
-		case SDLK_KP_7: return Point(-1, -1);
-		case SDLK_KP_8: return Point( 0, -1);
-		case SDLK_KP_9: return Point(+1, -1);
-	}
-	return std::nullopt;
+	const CGPath & path = LOCPLINT->localState->getPath(h);
+
+	if (path.nodes.size() > 2)
+		onHeroChanged(h);
+	else
+		if(!path.nodes[0].turns)
+			LOCPLINT->moveHero(h, path);
 }
 
 void CAdventureMapInterface::onSelectionChanged(const CArmedInstance *sel)

+ 4 - 2
client/adventureMap/CAdventureMapInterface.h

@@ -116,6 +116,8 @@ private:
 	void fnextHero();
 	void fendTurn();
 
+	void hotkeyMoveHeroDirectional(Point direction);
+
 	bool isActive();
 	void adjustActiveness(bool aiTurnStart); //should be called every time at AI/human turn transition; blocks GUI during AI turn
 
@@ -130,7 +132,7 @@ private:
 
 	const CGObjectInstance *getActiveObject(const int3 &tile);
 
-	std::optional<Point> keyToMoveDirection(const SDL_Keycode & key);
+	std::optional<Point> keyToMoveDirection(EShortcut key);
 
 	void endingTurn();
 
@@ -149,7 +151,7 @@ protected:
 	void show(SDL_Surface * to) override;
 	void showAll(SDL_Surface * to) override;
 
-	void keyPressed(const SDL_Keycode & key) override;
+	void keyPressed(EShortcut key) override;
 
 public:
 	CAdventureMapInterface();

+ 6 - 6
client/adventureMap/CAdventureOptions.cpp

@@ -17,6 +17,7 @@
 #include "../lobby/CCampaignInfoScreen.h"
 #include "../lobby/CScenarioInfoScreen.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../widgets/Buttons.h"
 
 #include "../../CCallback.h"
@@ -27,19 +28,18 @@ CAdventureOptions::CAdventureOptions()
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 
-	viewWorld = std::make_shared<CButton>(Point(24, 23), "ADVVIEW.DEF", CButton::tooltip(), [&](){ close(); }, SDLK_v);
+	viewWorld = std::make_shared<CButton>(Point(24, 23), "ADVVIEW.DEF", CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_WORLD);
 	viewWorld->addCallback( [] { LOCPLINT->viewWorldMap(); });
 
-	exit = std::make_shared<CButton>(Point(204, 313), "IOK6432.DEF", CButton::tooltip(), std::bind(&CAdventureOptions::close, this), SDLK_RETURN);
-	exit->assignedKeys.insert(SDLK_ESCAPE);
+	exit = std::make_shared<CButton>(Point(204, 313), "IOK6432.DEF", CButton::tooltip(), std::bind(&CAdventureOptions::close, this), EShortcut::GLOBAL_RETURN);
 
-	scenInfo = std::make_shared<CButton>(Point(24, 198), "ADVINFO.DEF", CButton::tooltip(), [&](){ close(); }, SDLK_i);
+	scenInfo = std::make_shared<CButton>(Point(24, 198), "ADVINFO.DEF", CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_SCENARIO);
 	scenInfo->addCallback(CAdventureOptions::showScenarioInfo);
 
-	puzzle = std::make_shared<CButton>(Point(24, 81), "ADVPUZ.DEF", CButton::tooltip(), [&](){ close(); }, SDLK_p);
+	puzzle = std::make_shared<CButton>(Point(24, 81), "ADVPUZ.DEF", CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_PUZZLE);
 	puzzle->addCallback(std::bind(&CPlayerInterface::showPuzzleMap, LOCPLINT));
 
-	dig = std::make_shared<CButton>(Point(24, 139), "ADVDIG.DEF", CButton::tooltip(), [&](){ close(); }, SDLK_d);
+	dig = std::make_shared<CButton>(Point(24, 139), "ADVDIG.DEF", CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_DIG_GRAIL);
 	if(const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero())
 		dig->addCallback(std::bind(&CPlayerInterface::tryDiggging, LOCPLINT, h));
 	else

+ 19 - 23
client/adventureMap/CInGameConsole.cpp

@@ -17,6 +17,7 @@
 #include "../PlayerLocalState.h"
 #include "../ClientCommandManager.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../render/Colors.h"
 
 #include "../../CCallback.h"
@@ -108,30 +109,29 @@ void CInGameConsole::print(const std::string & txt)
 	GH.totalRedraw(); // FIXME: ingame console has no parent widget set
 }
 
-void CInGameConsole::keyPressed (const SDL_Keycode & key)
+void CInGameConsole::keyPressed (EShortcut key)
 {
 	if (LOCPLINT->cingconsole != this)
 		return;
 
-	if(!captureAllKeys && key != SDLK_TAB)
+	if(!captureAllKeys && key != EShortcut::GAME_ACTIVATE_CONSOLE)
 		return; //because user is not entering any text
 
 	switch(key)
 	{
-	case SDLK_TAB:
-	case SDLK_ESCAPE:
-		{
-			if(captureAllKeys)
-			{
-				endEnteringText(false);
-			}
-			else if(SDLK_TAB == key)
-			{
-				startEnteringText();
-			}
-			break;
-		}
-	case SDLK_RETURN: //enter key
+	case EShortcut::GLOBAL_CANCEL:
+		if(captureAllKeys)
+			endEnteringText(false);
+		break;
+
+	case EShortcut::GAME_ACTIVATE_CONSOLE:
+		if(captureAllKeys)
+			endEnteringText(false);
+		else
+			startEnteringText();
+		break;
+
+	case EShortcut::GLOBAL_ACCEPT:
 		{
 			if(!enteredText.empty() && captureAllKeys)
 			{
@@ -145,7 +145,7 @@ void CInGameConsole::keyPressed (const SDL_Keycode & key)
 			}
 			break;
 		}
-	case SDLK_BACKSPACE:
+	case EShortcut::GLOBAL_BACKSPACE:
 		{
 			if(enteredText.size() > 1)
 			{
@@ -155,7 +155,7 @@ void CInGameConsole::keyPressed (const SDL_Keycode & key)
 			}
 			break;
 		}
-	case SDLK_UP: //up arrow
+	case EShortcut::MOVE_UP:
 		{
 			if(previouslyEntered.empty())
 				break;
@@ -174,7 +174,7 @@ void CInGameConsole::keyPressed (const SDL_Keycode & key)
 			}
 			break;
 		}
-	case SDLK_DOWN: //down arrow
+	case EShortcut::MOVE_DOWN:
 		{
 			if(prevEntDisp != -1 && prevEntDisp+1 < previouslyEntered.size())
 			{
@@ -190,10 +190,6 @@ void CInGameConsole::keyPressed (const SDL_Keycode & key)
 			}
 			break;
 		}
-	default:
-		{
-			break;
-		}
 	}
 }
 

+ 1 - 1
client/adventureMap/CInGameConsole.h

@@ -47,7 +47,7 @@ public:
 	void tick(uint32_t msPassed) override;
 	void show(SDL_Surface * to) override;
 	void showAll(SDL_Surface * to) override;
-	void keyPressed(const SDL_Keycode & key) override;
+	void keyPressed(EShortcut key) override;
 	void textInputed(const std::string & enteredText) override;
 	void textEdited(const std::string & enteredText) override;
 

+ 1 - 1
client/adventureMap/CResDataBar.cpp

@@ -80,7 +80,7 @@ std::string CResDataBar::buildDateString()
 void CResDataBar::draw(SDL_Surface * to)
 {
 	//TODO: all this should be labels, but they require proper text update on change
-	for (auto i=GameResID(EGameResID::WOOD); i <= GameResID(EGameResID::GOLD); vstd::advance(i, 1))
+	for (GameResID i=EGameResID::WOOD; i <= GameResID(EGameResID::GOLD); ++i)
 	{
 		std::string text = std::to_string(LOCPLINT->cb->getResourceAmount(i));
 

+ 1 - 1
client/battle/BattleFieldController.cpp

@@ -243,7 +243,7 @@ std::set<BattleHex> BattleFieldController::getMovementRangeForHoveredStack()
 	if (!owner.stacksController->getActiveStack())
 		return result;
 
-	if (!settings["battle"]["movementHighlightOnHover"].Bool())
+	if (!settings["battle"]["movementHighlightOnHover"].Bool() && !GH.isKeyboardShiftDown())
 		return result;
 
 	auto hoveredHex = getHoveredHex();

+ 3 - 2
client/battle/BattleInterfaceClasses.cpp

@@ -24,6 +24,7 @@
 #include "../CVideoHandler.h"
 #include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../render/Canvas.h"
 #include "../render/IImage.h"
 #include "../widgets/Buttons.h"
@@ -406,12 +407,12 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface
 	background->colorize(owner.playerID);
 	pos = center(background->pos);
 
-	exit = std::make_shared<CButton>(Point(384, 505), "iok6432.def", std::make_pair("", ""), [&](){ bExitf();}, SDLK_RETURN);
+	exit = std::make_shared<CButton>(Point(384, 505), "iok6432.def", std::make_pair("", ""), [&](){ bExitf();}, EShortcut::GLOBAL_ACCEPT);
 	exit->setBorderColor(Colors::METALLIC_GOLD);
 	
 	if(allowReplay)
 	{
-		repeat = std::make_shared<CButton>(Point(24, 505), "icn6432.def", std::make_pair("", ""), [&](){ bRepeatf();}, SDLK_ESCAPE);
+		repeat = std::make_shared<CButton>(Point(24, 505), "icn6432.def", std::make_pair("", ""), [&](){ bRepeatf();}, EShortcut::GLOBAL_CANCEL);
 		repeat->setBorderColor(Colors::METALLIC_GOLD);
 		labels.push_back(std::make_shared<CLabel>(232, 520, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("vcmi.battleResultsWindow.applyResultsLabel")));
 	}

+ 32 - 78
client/battle/BattleWindow.cpp

@@ -21,6 +21,7 @@
 #include "../CMusicHandler.h"
 #include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../windows/CSpellWindow.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/Images.h"
@@ -50,19 +51,23 @@ BattleWindow::BattleWindow(BattleInterface & owner):
 	
 	const JsonNode config(ResourceID("config/widgets/BattleWindow.json"));
 	
-	addCallback("options", std::bind(&BattleWindow::bOptionsf, this));
-	addCallback("surrender", std::bind(&BattleWindow::bSurrenderf, this));
-	addCallback("flee", std::bind(&BattleWindow::bFleef, this));
-	addCallback("autofight", std::bind(&BattleWindow::bAutofightf, this));
-	addCallback("spellbook", std::bind(&BattleWindow::bSpellf, this));
-	addCallback("wait", std::bind(&BattleWindow::bWaitf, this));
-	addCallback("defence", std::bind(&BattleWindow::bDefencef, this));
-	addCallback("consoleUp", std::bind(&BattleWindow::bConsoleUpf, this));
-	addCallback("consoleDown", std::bind(&BattleWindow::bConsoleDownf, this));
-	addCallback("tacticNext", std::bind(&BattleWindow::bTacticNextStack, this));
-	addCallback("tacticEnd", std::bind(&BattleWindow::bTacticPhaseEnd, this));
-	addCallback("alternativeAction", std::bind(&BattleWindow::bSwitchActionf, this));
-	
+	addShortcut(EShortcut::GLOBAL_OPTIONS, std::bind(&BattleWindow::bOptionsf, this));
+	addShortcut(EShortcut::BATTLE_SURRENDER, std::bind(&BattleWindow::bSurrenderf, this));
+	addShortcut(EShortcut::BATTLE_RETREAT, std::bind(&BattleWindow::bFleef, this));
+	addShortcut(EShortcut::BATTLE_AUTOCOMBAT, std::bind(&BattleWindow::bAutofightf, this));
+	addShortcut(EShortcut::BATTLE_CAST_SPELL, std::bind(&BattleWindow::bSpellf, this));
+	addShortcut(EShortcut::BATTLE_WAIT, std::bind(&BattleWindow::bWaitf, this));
+	addShortcut(EShortcut::BATTLE_DEFEND, std::bind(&BattleWindow::bDefencef, this));
+	addShortcut(EShortcut::BATTLE_CONSOLE_UP, std::bind(&BattleWindow::bConsoleUpf, this));
+	addShortcut(EShortcut::BATTLE_CONSOLE_DOWN, std::bind(&BattleWindow::bConsoleDownf, this));
+	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_TOGGLE_QUEUE, [this](){ this->toggleQueueVisibility();});
+	addShortcut(EShortcut::BATTLE_USE_CREATURE_SPELL, [this](){ this->owner.actionsController->enterCreatureCastingMode(); });
+	addShortcut(EShortcut::GLOBAL_CANCEL, [this](){ this->owner.actionsController->endCastingSpell(); });
+
 	build(config);
 	
 	console = widget<BattleConsole>("console");
@@ -182,43 +187,14 @@ void BattleWindow::deactivate()
 	LOCPLINT->cingconsole->deactivate();
 }
 
-void BattleWindow::keyPressed(const SDL_Keycode & key)
+void BattleWindow::keyPressed(EShortcut key)
 {
 	if (owner.openingPlaying())
 	{
 		owner.openingEnd();
 		return;
 	}
-
-	if(key == SDLK_q)
-	{
-		toggleQueueVisibility();
-	}
-	else if(key == SDLK_f)
-	{
-		owner.actionsController->enterCreatureCastingMode();
-	}
-	else if(key == SDLK_ESCAPE)
-	{
-		owner.actionsController->endCastingSpell();
-	}
-	else if(GH.isKeyboardShiftDown())
-	{
-		// save and activate setting
-		Settings movementHighlightOnHover = settings.write["battle"]["movementHighlightOnHover"];
-		movementHighlightOnHoverCache = movementHighlightOnHover->Bool();
-		movementHighlightOnHover->Bool() = true;
-	}
-}
-
-void BattleWindow::keyReleased(const SDL_Keycode & key)
-{
-	if(!GH.isKeyboardShiftDown())
-	{
-		// set back to initial state
-		Settings movementHighlightOnHover = settings.write["battle"]["movementHighlightOnHover"];
-		movementHighlightOnHover->Bool() = movementHighlightOnHoverCache;
-	}
+	InterfaceObjectConfigurable::keyPressed(key);
 }
 
 void BattleWindow::clickRight(tribool down, bool previousState)
@@ -554,40 +530,18 @@ void BattleWindow::blockUI(bool on)
 
 	bool canWait = owner.stacksController->getActiveStack() ? !owner.stacksController->getActiveStack()->waitedThisTurn : false;
 
-	if(auto w = widget<CButton>("options"))
-		w->block(on);
-	if(auto w = widget<CButton>("flee"))
-		w->block(on || !owner.curInt->cb->battleCanFlee());
-	if(auto w = widget<CButton>("surrender"))
-		w->block(on || owner.curInt->cb->battleGetSurrenderCost() < 0);
-	if(auto w = widget<CButton>("cast"))
-		w->block(on || owner.tacticsMode || !canCastSpells);
-	if(auto w = widget<CButton>("wait"))
-		w->block(on || owner.tacticsMode || !canWait);
-	if(auto w = widget<CButton>("defence"))
-		w->block(on || owner.tacticsMode);
-	if(auto w = widget<CButton>("alternativeAction"))
-		w->block(on || owner.tacticsMode);
-	if(auto w = widget<CButton>("autofight"))
-		w->block(owner.actionsController->spellcastingModeActive());
-
-	auto btactEnd = widget<CButton>("tacticEnd");
-	auto btactNext = widget<CButton>("tacticNext");
-	if(owner.tacticsMode && btactEnd && btactNext)
-	{
-		btactNext->block(on);
-		btactEnd->block(on);
-	}
-	else
-	{
-		auto bConsoleUp = widget<CButton>("consoleUp");
-		auto bConsoleDown = widget<CButton>("consoleDown");
-		if(bConsoleUp && bConsoleDown)
-		{
-			bConsoleUp->block(on);
-			bConsoleDown->block(on);
-		}
-	}
+	setShortcutBlocked(EShortcut::GLOBAL_OPTIONS, on);
+	setShortcutBlocked(EShortcut::BATTLE_RETREAT, on || !owner.curInt->cb->battleCanFlee());
+	setShortcutBlocked(EShortcut::BATTLE_SURRENDER, on || owner.curInt->cb->battleGetSurrenderCost() < 0);
+	setShortcutBlocked(EShortcut::BATTLE_CAST_SPELL, on || owner.tacticsMode || !canCastSpells);
+	setShortcutBlocked(EShortcut::BATTLE_WAIT, on || owner.tacticsMode || !canWait);
+	setShortcutBlocked(EShortcut::BATTLE_DEFEND, on || owner.tacticsMode);
+	setShortcutBlocked(EShortcut::BATTLE_SELECT_ACTION, on || owner.tacticsMode);
+	setShortcutBlocked(EShortcut::BATTLE_AUTOCOMBAT, owner.actionsController->spellcastingModeActive());
+	setShortcutBlocked(EShortcut::BATTLE_TACTICS_END, on && owner.tacticsMode);
+	setShortcutBlocked(EShortcut::BATTLE_TACTICS_NEXT, on && owner.tacticsMode);
+	setShortcutBlocked(EShortcut::BATTLE_CONSOLE_DOWN, on && !owner.tacticsMode);
+	setShortcutBlocked(EShortcut::BATTLE_CONSOLE_UP, on && !owner.tacticsMode);
 }
 
 std::optional<uint32_t> BattleWindow::getQueueHoveredUnitId()

+ 1 - 6
client/battle/BattleWindow.h

@@ -84,8 +84,7 @@ public:
 
 	void activate() override;
 	void deactivate() override;
-	void keyPressed(const SDL_Keycode & key) override;
-	void keyReleased(const SDL_Keycode& key) override;
+	void keyPressed(EShortcut key) override;
 	void clickRight(tribool down, bool previousState) override;
 	void show(SDL_Surface *to) override;
 	void showAll(SDL_Surface *to) override;
@@ -98,9 +97,5 @@ public:
 
 	/// Set possible alternative options. If more than 1 - the last will be considered as default option
 	void setAlternativeActions(const std::list<PossiblePlayerBattleAction> &);
-
-private:
-	/// used to save the state of this setting on toggle.
-	bool movementHighlightOnHoverCache;
 };
 

+ 1 - 1
client/battle/CreatureAnimation.h

@@ -68,7 +68,7 @@ namespace AnimationControls
 class CreatureAnimation : public CIntObject
 {
 public:
-	typedef std::function<float(CreatureAnimation *, ECreatureAnimType)> TSpeedController;
+	using TSpeedController = std::function<float(CreatureAnimation *, ECreatureAnimType)>;
 
 private:
 	std::string name;

+ 28 - 77
client/gui/CGuiHandler.cpp

@@ -13,6 +13,7 @@
 
 #include "CIntObject.h"
 #include "CursorHandler.h"
+#include "ShortcutHandler.h"
 
 #include "../CGameInfo.h"
 #include "../render/Colors.h"
@@ -27,6 +28,7 @@
 #include <SDL_render.h>
 #include <SDL_timer.h>
 #include <SDL_events.h>
+#include <SDL_keycode.h>
 
 #ifdef VCMI_APPLE
 #include <dispatch/dispatch.h>
@@ -93,6 +95,7 @@ void CGuiHandler::processLists(const ui16 activityFlag, std::function<void (std:
 
 void CGuiHandler::init()
 {
+	shortcutsHandlerInstance = std::make_unique<ShortcutHandler>();
 	mainFPSmng = new CFramerateManager();
 	mainFPSmng->init(settings["video"]["targetfps"].Integer());
 	isPointerRelativeMode = settings["general"]["userRelativePointer"].Bool();
@@ -356,6 +359,10 @@ void CGuiHandler::handleCurrentEvent( SDL_Event & current )
 	if(current.type == SDL_KEYDOWN || current.type == SDL_KEYUP)
 	{
 		SDL_KeyboardEvent key = current.key;
+
+		if (key.repeat != 0)
+			return; // ignore periodic event resends
+
 		if(current.type == SDL_KEYDOWN && key.keysym.sym >= SDLK_F1 && key.keysym.sym <= SDLK_F15 && settings["session"]["spectate"].Bool())
 		{
 			//TODO: we need some central place for all interface-independent hotkeys
@@ -397,32 +404,35 @@ void CGuiHandler::handleCurrentEvent( SDL_Event & current )
 			return;
 		}
 
-		//translate numpad keys
-		if(key.keysym.sym == SDLK_KP_ENTER)
-		{
-			key.keysym.sym = SDLK_RETURN;
-			key.keysym.scancode = SDL_SCANCODE_RETURN;
-		}
+		auto shortcutsVector = shortcutsHandler().translateKeycode(key.keysym.sym);
 
 		bool keysCaptured = false;
 		for(auto i = keyinterested.begin(); i != keyinterested.end() && continueEventHandling; i++)
 		{
-			if((*i)->captureThisKey(key.keysym.sym))
+			for (EShortcut shortcut : shortcutsVector)
 			{
-				keysCaptured = true;
-				break;
+				if((*i)->captureThisKey(shortcut))
+				{
+					keysCaptured = true;
+					break;
+				}
 			}
 		}
 
 		std::list<CIntObject*> miCopy = keyinterested;
 		for(auto i = miCopy.begin(); i != miCopy.end() && continueEventHandling; i++)
-			if(vstd::contains(keyinterested,*i) && (!keysCaptured || (*i)->captureThisKey(key.keysym.sym)))
+		{
+			for (EShortcut shortcut : shortcutsVector)
 			{
-				if (key.state == SDL_PRESSED && key.repeat == 0) // function like key_DOWN, and not like a periodic key_Pressed check 
-					(**i).keyPressed(key.keysym.sym);
-				if (key.state == SDL_RELEASED)
-					(**i).keyReleased(key.keysym.sym);
+				if(vstd::contains(keyinterested,*i) && (!keysCaptured || (*i)->captureThisKey(shortcut)))
+				{
+					if (key.state == SDL_PRESSED)
+						(**i).keyPressed(shortcut);
+					if (key.state == SDL_RELEASED)
+						(**i).keyReleased(shortcut);
+				}
 			}
+		}
 	}
 	else if(current.type == SDL_MOUSEMOTION)
 	{
@@ -704,6 +714,10 @@ CGuiHandler::~CGuiHandler()
 	delete terminate_cond;
 }
 
+ShortcutHandler & CGuiHandler::shortcutsHandler()
+{
+	return *shortcutsHandlerInstance;
+}
 
 void CGuiHandler::moveCursorToPosition(const Point & position)
 {
@@ -770,69 +784,6 @@ void CGuiHandler::drawFPSCounter()
 	graphics->fonts[FONT_BIG]->renderTextLeft(screen, fps, Colors::YELLOW, Point(10, 10));
 }
 
-SDL_Keycode CGuiHandler::arrowToNum(SDL_Keycode key)
-{
-	switch(key)
-	{
-	case SDLK_DOWN:
-		return SDLK_KP_2;
-	case SDLK_UP:
-		return SDLK_KP_8;
-	case SDLK_LEFT:
-		return SDLK_KP_4;
-	case SDLK_RIGHT:
-		return SDLK_KP_6;
-	default:
-		throw std::runtime_error("Wrong key!");
-	}
-}
-
-SDL_Keycode CGuiHandler::numToDigit(SDL_Keycode key)
-{
-
-#define REMOVE_KP(keyName) case SDLK_KP_ ## keyName : return SDLK_ ## keyName;
-	switch(key)
-	{
-		REMOVE_KP(0)
-		REMOVE_KP(1)
-		REMOVE_KP(2)
-		REMOVE_KP(3)
-		REMOVE_KP(4)
-		REMOVE_KP(5)
-		REMOVE_KP(6)
-		REMOVE_KP(7)
-		REMOVE_KP(8)
-		REMOVE_KP(9)
-		REMOVE_KP(PERIOD)
-		REMOVE_KP(MINUS)
-		REMOVE_KP(PLUS)
-		REMOVE_KP(EQUALS)
-
-	case SDLK_KP_MULTIPLY:
-		return SDLK_ASTERISK;
-	case SDLK_KP_DIVIDE:
-		return SDLK_SLASH;
-	case SDLK_KP_ENTER:
-		return SDLK_RETURN;
-	default:
-		return SDLK_UNKNOWN;
-	}
-#undef REMOVE_KP
-}
-
-bool CGuiHandler::isNumKey(SDL_Keycode key, bool number)
-{
-	if(number)
-		return key >= SDLK_KP_1 && key <= SDLK_KP_0;
-	else
-		return (key >= SDLK_KP_1 && key <= SDLK_KP_0) || key == SDLK_KP_MINUS || key == SDLK_KP_PLUS || key == SDLK_KP_EQUALS;
-}
-
-bool CGuiHandler::isArrowKey(SDL_Keycode key)
-{
-	return key == SDLK_UP || key == SDLK_DOWN || key == SDLK_LEFT || key == SDLK_RIGHT;
-}
-
 bool CGuiHandler::amIGuiThread()
 {
 	return inGuiThread.get() && *inGuiThread;

+ 6 - 9
client/gui/CGuiHandler.h

@@ -12,8 +12,6 @@
 #include "MouseButton.h"
 #include "../../lib/Point.h"
 
-#include <SDL_keycode.h>
-
 VCMI_LIB_NAMESPACE_BEGIN
 
 template <typename T> struct CondSh;
@@ -24,6 +22,7 @@ VCMI_LIB_NAMESPACE_END
 union SDL_Event;
 struct SDL_MouseMotionEvent;
 
+class ShortcutHandler;
 class CFramerateManager;
 class IStatusBar;
 class CIntObject;
@@ -79,8 +78,10 @@ private:
 
 	std::vector<std::shared_ptr<IShowActivatable>> disposed;
 
+	std::unique_ptr<ShortcutHandler> shortcutsHandlerInstance;
+
 	std::atomic<bool> continueEventHandling;
-	typedef std::list<CIntObject*> CIntObjectList;
+	using CIntObjectList = std::list<CIntObject *>;
 
 	//active GUI elements (listening for events
 	CIntObjectList lclickable;
@@ -107,14 +108,14 @@ private:
 public:
 	void handleElementActivate(CIntObject * elem, ui16 activityFlag);
 	void handleElementDeActivate(CIntObject * elem, ui16 activityFlag);
-
 public:
 	//objs to blit
 	std::vector<std::shared_ptr<IShowActivatable>> objsToBlit;
-
 	/// returns current position of mouse cursor, relative to vcmi window
 	const Point & getCursorPosition() const;
 
+	ShortcutHandler & shortcutsHandler();
+
 	Point screenDimensions() const;
 
 	/// returns true if at least one mouse button is pressed
@@ -175,10 +176,6 @@ public:
 	void breakEventHandling(); //current event won't be propagated anymore
 	void drawFPSCounter(); // draws the FPS to the upper left corner of the screen
 
-	static SDL_Keycode arrowToNum(SDL_Keycode key); //converts arrow key to according numpad key
-	static SDL_Keycode numToDigit(SDL_Keycode key);//converts numpad digit key to normal digit key
-	static bool isNumKey(SDL_Keycode key, bool number = true); //checks if key is on numpad (numbers - check only for numpad digits)
-	static bool isArrowKey(SDL_Keycode key);
 	static bool amIGuiThread();
 	static void pushUserEvent(EUserEvent usercode);
 	static void pushUserEvent(EUserEvent usercode, void * userdata);

+ 9 - 14
client/gui/CIntObject.cpp

@@ -11,6 +11,7 @@
 #include "CIntObject.h"
 
 #include "CGuiHandler.h"
+#include "Shortcut.h"
 #include "../renderSDL/SDL_Extensions.h"
 #include "../windows/CMessage.h"
 #include "../CMT.h"
@@ -305,28 +306,23 @@ const Rect & CIntObject::center(const Point & p, bool propagate)
 	return pos;
 }
 
-bool CIntObject::captureThisKey(const SDL_Keycode & key)
+bool CIntObject::captureThisKey(EShortcut key)
 {
 	return captureAllKeys;
 }
 
 CKeyShortcut::CKeyShortcut()
+	: assignedKey(EShortcut::NONE)
 {}
 
-CKeyShortcut::CKeyShortcut(int key)
+CKeyShortcut::CKeyShortcut(EShortcut key)
+	: assignedKey(key)
 {
-	if (key != SDLK_UNKNOWN)
-		assignedKeys.insert(key);
 }
 
-CKeyShortcut::CKeyShortcut(std::set<int> Keys)
-	:assignedKeys(Keys)
-{}
-
-void CKeyShortcut::keyPressed(const SDL_Keycode & key)
+void CKeyShortcut::keyPressed(EShortcut key)
 {
-	if(vstd::contains(assignedKeys,key)
-	 || vstd::contains(assignedKeys, CGuiHandler::numToDigit(key)))
+	if( assignedKey == key && assignedKey != EShortcut::NONE)
 	{
 		bool prev = mouseState(MouseButton::LEFT);
 		updateMouseState(MouseButton::LEFT, true);
@@ -335,10 +331,9 @@ void CKeyShortcut::keyPressed(const SDL_Keycode & key)
 	}
 }
 
-void CKeyShortcut::keyReleased(const SDL_Keycode & key)
+void CKeyShortcut::keyReleased(EShortcut key)
 {
-	if(vstd::contains(assignedKeys,key)
-	 || vstd::contains(assignedKeys, CGuiHandler::numToDigit(key)))
+	if( assignedKey == key && assignedKey != EShortcut::NONE)
 	{
 		bool prev = mouseState(MouseButton::LEFT);
 		updateMouseState(MouseButton::LEFT, false);

+ 8 - 10
client/gui/CIntObject.h

@@ -16,8 +16,7 @@
 struct SDL_Surface;
 class CGuiHandler;
 class CPicture;
-
-typedef int32_t SDL_Keycode;
+enum class EShortcut;
 
 using boost::logic::tribool;
 
@@ -111,9 +110,9 @@ public:
 
 	//keyboard handling
 	bool captureAllKeys; //if true, only this object should get info about pressed keys
-	virtual void keyPressed(const SDL_Keycode & key){}
-	virtual void keyReleased(const SDL_Keycode & key){}
-	virtual bool captureThisKey(const SDL_Keycode & key); //allows refining captureAllKeys against specific events (eg. don't capture ENTER)
+	virtual void keyPressed(EShortcut key){}
+	virtual void keyReleased(EShortcut key){}
+	virtual bool captureThisKey(EShortcut key); //allows refining captureAllKeys against specific events (eg. don't capture ENTER)
 
 	virtual void textInputed(const std::string & enteredText){};
 	virtual void textEdited(const std::string & enteredText){};
@@ -190,12 +189,11 @@ public:
 class CKeyShortcut : public virtual CIntObject
 {
 public:
-	std::set<int> assignedKeys;
+	EShortcut assignedKey;
 	CKeyShortcut();
-	CKeyShortcut(int key);
-	CKeyShortcut(std::set<int> Keys);
-	void keyPressed(const SDL_Keycode & key) override;
-	void keyReleased(const SDL_Keycode & key) override;
+	CKeyShortcut(EShortcut key);
+	void keyPressed(EShortcut key) override;
+	void keyReleased(EShortcut key) override;
 
 };
 

+ 67 - 35
client/gui/InterfaceObjectConfigurable.cpp

@@ -15,6 +15,8 @@
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/ShortcutHandler.h"
+#include "../gui/Shortcut.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/MiscWidgets.h"
@@ -25,18 +27,6 @@
 
 #include "../../lib/CGeneralTextHandler.h"
 
-static std::map<std::string, int> KeycodeMap{
-	{"up", SDLK_UP},
-	{"down", SDLK_DOWN},
-	{"left", SDLK_LEFT},
-	{"right", SDLK_RIGHT},
-	{"space", SDLK_SPACE},
-	{"escape", SDLK_ESCAPE},
-	{"backspace", SDLK_BACKSPACE},
-	{"enter", SDLK_RETURN}
-};
-
-
 InterfaceObjectConfigurable::InterfaceObjectConfigurable(const JsonNode & config, int used, Point offset):
 	InterfaceObjectConfigurable(used, offset)
 {
@@ -211,27 +201,20 @@ std::pair<std::string, std::string> InterfaceObjectConfigurable::readHintText(co
 	return result;
 }
 
-int InterfaceObjectConfigurable::readKeycode(const JsonNode & config) const
+EShortcut InterfaceObjectConfigurable::readHotkey(const JsonNode & config) const
 {
-	logGlobal->debug("Reading keycode");
-	if(config.getType() == JsonNode::JsonType::DATA_INTEGER)
-		return config.Integer();
-	
-	if(config.getType() == JsonNode::JsonType::DATA_STRING)
-	{
-		auto s = config.String();
-		if(s.size() == 1) //keyboard symbol
-			return s[0];
+	logGlobal->debug("Reading hotkey");
 
-		if (KeycodeMap.count(s))
-			return KeycodeMap[s];
-
-		logGlobal->error("Invalid keycode '%s' in interface configuration!", config.String());
-		return SDLK_UNKNOWN;
+	if(config.getType() != JsonNode::JsonType::DATA_STRING)
+	{
+		logGlobal->error("Invalid hotket format in interface configuration! Expected string!", config.String());
+		return EShortcut::NONE;
 	}
 
-	logGlobal->error("Invalid keycode format in interface configuration! Expected string or integer!", config.String());
-	return SDLK_UNKNOWN;
+	EShortcut result = GH.shortcutsHandler().findShortcut(config.String());
+	if (result == EShortcut::NONE)
+		logGlobal->error("Invalid hotkey '%s' in interface configuration!", config.String());
+	return result;;
 }
 
 std::shared_ptr<CPicture> InterfaceObjectConfigurable::buildPicture(const JsonNode & config) const
@@ -337,16 +320,27 @@ std::shared_ptr<CButton> InterfaceObjectConfigurable::buildButton(const JsonNode
 		button->setImageOrder(imgOrder[0].Integer(), imgOrder[1].Integer(), imgOrder[2].Integer(), imgOrder[3].Integer());
 	}
 	if(!config["callback"].isNull())
-		button->addCallback(std::bind(callbacks.at(config["callback"].String()), 0));
+	{
+		std::string callbackName = config["callback"].String();
+
+		if (callbacks.count(callbackName) > 0)
+			button->addCallback(std::bind(callbacks.at(callbackName), 0));
+		else
+			logGlobal->error("Invalid callback '%s' in widget", callbackName );
+	}
 	if(!config["hotkey"].isNull())
 	{
-		if(config["hotkey"].getType() == JsonNode::JsonType::DATA_VECTOR)
+		if(config["hotkey"].getType() == JsonNode::JsonType::DATA_STRING)
 		{
-			for(auto k : config["hotkey"].Vector())
-				button->assignedKeys.insert(readKeycode(k));
+			button->assignedKey = readHotkey(config["hotkey"]);
+
+			auto target = shortcuts.find(button->assignedKey);
+			if (target != shortcuts.end())
+			{
+				button->addCallback(target->second.callback);
+				target->second.assignedToButton = true;
+			}
 		}
-		else
-			button->assignedKeys.insert(readKeycode(config["hotkey"]));
 	}
 	return button;
 }
@@ -452,3 +446,41 @@ std::shared_ptr<CIntObject> InterfaceObjectConfigurable::buildWidget(JsonNode co
 	logGlobal->error("Builder with type %s is not registered", type);
 	return nullptr;
 }
+
+void InterfaceObjectConfigurable::setShortcutBlocked(EShortcut shortcut, bool isBlocked)
+{
+	auto target = shortcuts.find(shortcut);
+	if (target == shortcuts.end())
+		return;
+
+	target->second.blocked = isBlocked;
+
+	for	(auto & entry : widgets)
+	{
+		auto button = std::dynamic_pointer_cast<CButton>(entry.second);
+
+		if (button && button->assignedKey == shortcut)
+			button->block(isBlocked);
+	}
+}
+
+void InterfaceObjectConfigurable::addShortcut(EShortcut shortcut, std::function<void()> callback)
+{
+	assert(shortcuts.count(shortcut) == 0);
+	shortcuts[shortcut].callback = callback;
+}
+
+void InterfaceObjectConfigurable::keyPressed(EShortcut key)
+{
+	auto target = shortcuts.find(key);
+	if (target == shortcuts.end())
+		return;
+
+	if (target->second.assignedToButton)
+		return; // will be handled by button instance
+
+	if (target->second.blocked)
+		return;
+
+	target->second.callback();
+}

+ 16 - 2
client/gui/InterfaceObjectConfigurable.h

@@ -35,7 +35,14 @@ public:
 	InterfaceObjectConfigurable(const JsonNode & config, int used=0, Point offset=Point());
 
 protected:
-	
+	/// Set blocked status for all buttons assotiated with provided shortcut
+	void setShortcutBlocked(EShortcut shortcut, bool isBlocked);
+
+	/// Registers provided callback to be called whenever specified shortcut is triggered
+	void addShortcut(EShortcut shortcut, std::function<void()> callback);
+
+	void keyPressed(EShortcut key) override;
+
 	using BuilderFunction = std::function<std::shared_ptr<CIntObject>(const JsonNode &)>;
 	void registerBuilder(const std::string &, BuilderFunction);
 	
@@ -64,7 +71,7 @@ protected:
 	EFonts readFont(const JsonNode &) const;
 	std::string readText(const JsonNode &) const;
 	std::pair<std::string, std::string> readHintText(const JsonNode &) const;
-	int readKeycode(const JsonNode &) const;
+	EShortcut readHotkey(const JsonNode &) const;
 	
 	//basic widgets
 	std::shared_ptr<CPicture> buildPicture(const JsonNode &) const;
@@ -82,9 +89,16 @@ protected:
 	std::shared_ptr<CIntObject> buildWidget(JsonNode config) const;
 	
 private:
+	struct ShortcutState
+	{
+		std::function<void()> callback;
+		mutable bool assignedToButton = false;
+		bool blocked = false;
+	};
 	
 	int unnamedObjectId = 0;
 	std::map<std::string, BuilderFunction> builders;
 	std::map<std::string, std::shared_ptr<CIntObject>> widgets;
 	std::map<std::string, std::function<void(int)>> callbacks;
+	std::map<EShortcut, ShortcutState> shortcuts;
 };

+ 156 - 0
client/gui/Shortcut.h

@@ -0,0 +1,156 @@
+/*
+ * Shortcut.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
+
+enum class EShortcut
+{
+	NONE,
+
+	// Global hotkeys that are available in multiple dialogs
+	GLOBAL_ACCEPT,     // Return - Accept query
+	GLOBAL_CANCEL,     // Escape - Cancel query
+	GLOBAL_RETURN,     // Enter, Escape - Close current window and return to previous view
+	GLOBAL_FULLSCREEN, // F4 - TODO: remove hardcoded check for key
+	GLOBAL_OPTIONS,    // 'O' - Open System Options dialog
+	GLOBAL_BACKSPACE,  // Backspace - erase last symbol in text input
+	GLOBAL_MOVE_FOCUS, // Tab - move focus to next text input
+
+	// Movement hotkeys, usually - for moving through lists with slider
+	MOVE_LEFT,
+	MOVE_RIGHT,
+	MOVE_UP,
+	MOVE_DOWN,
+	MOVE_FIRST,
+	MOVE_LAST,
+	MOVE_PAGE_UP,
+	MOVE_PAGE_DOWN,
+
+	// Element selection - for multiple choice dialog popups
+	SELECT_INDEX_1,
+	SELECT_INDEX_2,
+	SELECT_INDEX_3,
+	SELECT_INDEX_4,
+	SELECT_INDEX_5,
+	SELECT_INDEX_6,
+	SELECT_INDEX_7,
+	SELECT_INDEX_8,
+
+	// Main menu hotkeys - for navigation between main menu windows
+	MAIN_MENU_NEW_GAME,
+	MAIN_MENU_LOAD_GAME,
+	MAIN_MENU_HIGH_SCORES,
+	MAIN_MENU_CREDITS,
+	MAIN_MENU_BACK,
+	MAIN_MENU_QUIT,
+	MAIN_MENU_SINGLEPLAYER,
+	MAIN_MENU_MULTIPLAYER,
+	MAIN_MENU_CAMPAIGN,
+	MAIN_MENU_TUTORIAL,
+	MAIN_MENU_CAMPAIGN_SOD,
+	MAIN_MENU_CAMPAIGN_ROE,
+	MAIN_MENU_CAMPAIGN_AB,
+	MAIN_MENU_CAMPAIGN_CUSTOM,
+
+	// Game lobby / scenario selection
+	LOBBY_BEGIN_GAME, // b, Return
+	LOBBY_LOAD_GAME,  // l, Return
+	LOBBY_SAVE_GAME,  // s, Return
+	LOBBY_RANDOM_MAP, // Open random map tab
+	LOBBY_HIDE_CHAT,
+	LOBBY_ADDITIONAL_OPTIONS, // Open additional options tab
+	LOBBY_SELECT_SCENARIO,    // Open map list tab
+
+	// 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
+	ADVENTURE_GAME_OPTIONS, // 'o', Open CAdventureOptions window
+	ADVENTURE_TOGGLE_GRID,  // F6, Toggles map grid
+	ADVENTURE_TOGGLE_SLEEP, // z,w, Toggles hero sleep status
+	ADVENTURE_MOVE_HERO,    // Moves hero alongside set path
+	ADVENTURE_VISIT_OBJECT, // Revisits object hero is standing on
+	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_VIEW_SCENARIO,// View Scenario Information window
+	ADVENTURE_DIG_GRAIL,
+	ADVENTURE_VIEW_PUZZLE,
+	ADVENTURE_VIEW_WORLD,
+	ADVENTURE_TOGGLE_MAP_LEVEL,
+	ADVENTURE_KINGDOM_OVERVIEW,
+	ADVENTURE_QUEST_LOG,
+	ADVENTURE_CAST_SPELL,
+	ADVENTURE_END_TURN,
+	ADVENTURE_THIEVES_GUILD,
+
+	// Move hero one tile in specified direction. Bound to cursors & numpad buttons
+	ADVENTURE_MOVE_HERO_SW,
+	ADVENTURE_MOVE_HERO_SS,
+	ADVENTURE_MOVE_HERO_SE,
+	ADVENTURE_MOVE_HERO_WW,
+	ADVENTURE_MOVE_HERO_EE,
+	ADVENTURE_MOVE_HERO_NW,
+	ADVENTURE_MOVE_HERO_NN,
+	ADVENTURE_MOVE_HERO_NE,
+
+	// Battle screen
+	BATTLE_TOGGLE_QUEUE,
+	BATTLE_USE_CREATURE_SPELL,
+	BATTLE_SURRENDER,
+	BATTLE_RETREAT,
+	BATTLE_AUTOCOMBAT,
+	BATTLE_CAST_SPELL,
+	BATTLE_WAIT,
+	BATTLE_DEFEND,
+	BATTLE_CONSOLE_UP,
+	BATTLE_CONSOLE_DOWN,
+	BATTLE_TACTICS_NEXT,
+	BATTLE_TACTICS_END,
+	BATTLE_SELECT_ACTION, // Alternative actions toggle
+
+	// Town screen
+	TOWN_OPEN_TAVERN,
+	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_UPGRADE, // Upgrade current creature
+	RECRUITMENT_UPGRADE_ALL, // Upgrade all creatures (Hill Fort / Skeleton Transformer)
+
+	// Kingdom Overview window
+	KINGDOM_HEROES_TAB,
+	KINGDOM_TOWNS_TAB,
+
+	// Hero screen
+	HERO_DISMISS,
+	HERO_COMMANDER,
+	HERO_LOOSE_FORMATION,
+	HERO_TIGHT_FORMATION,
+	HERO_TOGGLE_TACTICS, // b
+
+	// Spellbook screen
+	SPELLBOOK_TAB_ADVENTURE,
+	SPELLBOOK_TAB_COMBAT,
+
+	AFTER_LAST
+};
+

+ 362 - 0
client/gui/ShortcutHandler.cpp

@@ -0,0 +1,362 @@
+/*
+ * ShortcutHandler.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 "ShortcutHandler.h"
+#include "Shortcut.h"
+#include <SDL_keycode.h>
+
+std::vector<EShortcut> ShortcutHandler::translateKeycode(SDL_Keycode key) const
+{
+	static const std::multimap<SDL_Keycode, EShortcut> keyToShortcut = {
+		{SDLK_RETURN,    EShortcut::GLOBAL_ACCEPT             },
+		{SDLK_KP_ENTER,  EShortcut::GLOBAL_ACCEPT             },
+		{SDLK_ESCAPE,    EShortcut::GLOBAL_CANCEL             },
+		{SDLK_RETURN,    EShortcut::GLOBAL_RETURN             },
+		{SDLK_KP_ENTER,  EShortcut::GLOBAL_RETURN             },
+		{SDLK_ESCAPE,    EShortcut::GLOBAL_RETURN             },
+		{SDLK_F4,        EShortcut::GLOBAL_FULLSCREEN         },
+		{SDLK_BACKSPACE, EShortcut::GLOBAL_BACKSPACE          },
+		{SDLK_TAB,       EShortcut::GLOBAL_MOVE_FOCUS         },
+		{SDLK_o,         EShortcut::GLOBAL_OPTIONS            },
+		{SDLK_LEFT,      EShortcut::MOVE_LEFT                 },
+		{SDLK_RIGHT,     EShortcut::MOVE_RIGHT                },
+		{SDLK_UP,        EShortcut::MOVE_UP                   },
+		{SDLK_DOWN,      EShortcut::MOVE_DOWN                 },
+		{SDLK_HOME,      EShortcut::MOVE_FIRST                },
+		{SDLK_END,       EShortcut::MOVE_LAST                 },
+		{SDLK_PAGEUP,    EShortcut::MOVE_PAGE_UP              },
+		{SDLK_PAGEDOWN,  EShortcut::MOVE_PAGE_DOWN            },
+		{SDLK_1,         EShortcut::SELECT_INDEX_1            },
+		{SDLK_2,         EShortcut::SELECT_INDEX_2            },
+		{SDLK_3,         EShortcut::SELECT_INDEX_3            },
+		{SDLK_4,         EShortcut::SELECT_INDEX_4            },
+		{SDLK_5,         EShortcut::SELECT_INDEX_5            },
+		{SDLK_6,         EShortcut::SELECT_INDEX_6            },
+		{SDLK_7,         EShortcut::SELECT_INDEX_7            },
+		{SDLK_8,         EShortcut::SELECT_INDEX_8            },
+		{SDLK_n,         EShortcut::MAIN_MENU_NEW_GAME        },
+		{SDLK_l,         EShortcut::MAIN_MENU_LOAD_GAME       },
+		{SDLK_h,         EShortcut::MAIN_MENU_HIGH_SCORES     },
+		{SDLK_c,         EShortcut::MAIN_MENU_CREDITS         },
+		{SDLK_q,         EShortcut::MAIN_MENU_QUIT            },
+		{SDLK_b,         EShortcut::MAIN_MENU_BACK            },
+		{SDLK_s,         EShortcut::MAIN_MENU_SINGLEPLAYER    },
+		{SDLK_m,         EShortcut::MAIN_MENU_MULTIPLAYER     },
+		{SDLK_c,         EShortcut::MAIN_MENU_CAMPAIGN        },
+		{SDLK_t,         EShortcut::MAIN_MENU_TUTORIAL        },
+		{SDLK_s,         EShortcut::MAIN_MENU_CAMPAIGN_SOD    },
+		{SDLK_r,         EShortcut::MAIN_MENU_CAMPAIGN_ROE    },
+		{SDLK_a,         EShortcut::MAIN_MENU_CAMPAIGN_AB     },
+		{SDLK_c,         EShortcut::MAIN_MENU_CAMPAIGN_CUSTOM },
+		{SDLK_b,         EShortcut::LOBBY_BEGIN_GAME          },
+		{SDLK_RETURN,    EShortcut::LOBBY_BEGIN_GAME          },
+		{SDLK_KP_ENTER,  EShortcut::LOBBY_BEGIN_GAME          },
+		{SDLK_l,         EShortcut::LOBBY_LOAD_GAME           },
+		{SDLK_RETURN,    EShortcut::LOBBY_LOAD_GAME           },
+		{SDLK_KP_ENTER,  EShortcut::LOBBY_LOAD_GAME           },
+		{SDLK_s,         EShortcut::LOBBY_SAVE_GAME           },
+		{SDLK_r,         EShortcut::LOBBY_RANDOM_MAP          },
+		{SDLK_h,         EShortcut::LOBBY_HIDE_CHAT           },
+		{SDLK_a,         EShortcut::LOBBY_ADDITIONAL_OPTIONS  },
+		{SDLK_s,         EShortcut::LOBBY_SELECT_SCENARIO     },
+		{SDLK_e,         EShortcut::GAME_END_TURN             },
+		{SDLK_l,         EShortcut::GAME_LOAD_GAME            },
+		{SDLK_s,         EShortcut::GAME_SAVE_GAME            },
+		{SDLK_r,         EShortcut::GAME_RESTART_GAME         },
+		{SDLK_m,         EShortcut::GAME_TO_MAIN_MENU         },
+		{SDLK_q,         EShortcut::GAME_QUIT_GAME            },
+		{SDLK_t,         EShortcut::GAME_OPEN_MARKETPLACE     },
+		{SDLK_g,         EShortcut::GAME_OPEN_THIEVES_GUILD   },
+		{SDLK_TAB,       EShortcut::GAME_ACTIVATE_CONSOLE     },
+		{SDLK_o,         EShortcut::ADVENTURE_GAME_OPTIONS    },
+		{SDLK_F6,        EShortcut::ADVENTURE_TOGGLE_GRID     },
+		{SDLK_z,         EShortcut::ADVENTURE_TOGGLE_SLEEP    },
+		{SDLK_w,         EShortcut::ADVENTURE_TOGGLE_SLEEP    },
+		{SDLK_m,         EShortcut::ADVENTURE_MOVE_HERO       },
+		{SDLK_SPACE,     EShortcut::ADVENTURE_VISIT_OBJECT    },
+		{SDLK_KP_1,      EShortcut::ADVENTURE_MOVE_HERO_SW    },
+		{SDLK_KP_2,      EShortcut::ADVENTURE_MOVE_HERO_SS    },
+		{SDLK_KP_3,      EShortcut::ADVENTURE_MOVE_HERO_SE    },
+		{SDLK_KP_4,      EShortcut::ADVENTURE_MOVE_HERO_WW    },
+		{SDLK_KP_6,      EShortcut::ADVENTURE_MOVE_HERO_EE    },
+		{SDLK_KP_7,      EShortcut::ADVENTURE_MOVE_HERO_NW    },
+		{SDLK_KP_8,      EShortcut::ADVENTURE_MOVE_HERO_NN    },
+		{SDLK_KP_9,      EShortcut::ADVENTURE_MOVE_HERO_NE    },
+		{SDLK_DOWN,      EShortcut::ADVENTURE_MOVE_HERO_SS    },
+		{SDLK_LEFT,      EShortcut::ADVENTURE_MOVE_HERO_WW    },
+		{SDLK_RIGHT,     EShortcut::ADVENTURE_MOVE_HERO_EE    },
+		{SDLK_UP,        EShortcut::ADVENTURE_MOVE_HERO_NN    },
+		{SDLK_RETURN,    EShortcut::ADVENTURE_VIEW_SELECTED   },
+		{SDLK_KP_ENTER,  EShortcut::ADVENTURE_VIEW_SELECTED   },
+ //		{SDLK_,          EShortcut::ADVENTURE_NEXT_OBJECT     },
+		{SDLK_t,         EShortcut::ADVENTURE_NEXT_TOWN       },
+		{SDLK_h,         EShortcut::ADVENTURE_NEXT_HERO       },
+ //		{SDLK_,          EShortcut::ADVENTURE_FIRST_TOWN      },
+  //		{SDLK_,          EShortcut::ADVENTURE_FIRST_HERO      },
+		{SDLK_i,         EShortcut::ADVENTURE_VIEW_SCENARIO   },
+		{SDLK_d,         EShortcut::ADVENTURE_DIG_GRAIL       },
+		{SDLK_p,         EShortcut::ADVENTURE_VIEW_PUZZLE     },
+		{SDLK_v,         EShortcut::ADVENTURE_VIEW_WORLD      },
+		{SDLK_u,         EShortcut::ADVENTURE_TOGGLE_MAP_LEVEL},
+		{SDLK_k,         EShortcut::ADVENTURE_KINGDOM_OVERVIEW},
+		{SDLK_q,         EShortcut::ADVENTURE_QUEST_LOG       },
+		{SDLK_c,         EShortcut::ADVENTURE_CAST_SPELL      },
+		{SDLK_e,         EShortcut::ADVENTURE_END_TURN        },
+		{SDLK_g,         EShortcut::ADVENTURE_THIEVES_GUILD   },
+		{SDLK_q,         EShortcut::BATTLE_TOGGLE_QUEUE       },
+		{SDLK_c,         EShortcut::BATTLE_USE_CREATURE_SPELL },
+		{SDLK_s,         EShortcut::BATTLE_SURRENDER          },
+		{SDLK_r,         EShortcut::BATTLE_RETREAT            },
+		{SDLK_a,         EShortcut::BATTLE_AUTOCOMBAT         },
+		{SDLK_c,         EShortcut::BATTLE_CAST_SPELL         },
+		{SDLK_w,         EShortcut::BATTLE_WAIT               },
+		{SDLK_d,         EShortcut::BATTLE_DEFEND             },
+		{SDLK_SPACE,     EShortcut::BATTLE_DEFEND             },
+		{SDLK_UP,        EShortcut::BATTLE_CONSOLE_UP         },
+		{SDLK_DOWN,      EShortcut::BATTLE_CONSOLE_DOWN       },
+		{SDLK_SPACE,     EShortcut::BATTLE_TACTICS_NEXT       },
+		{SDLK_RETURN,    EShortcut::BATTLE_TACTICS_END        },
+		{SDLK_KP_ENTER,  EShortcut::BATTLE_TACTICS_END        },
+		{SDLK_s,         EShortcut::BATTLE_SELECT_ACTION      },
+		{SDLK_t,         EShortcut::TOWN_OPEN_TAVERN          },
+		{SDLK_SPACE,     EShortcut::TOWN_SWAP_ARMIES          },
+		{SDLK_END,       EShortcut::RECRUITMENT_MAX           },
+		{SDLK_HOME,      EShortcut::RECRUITMENT_MIN           },
+		{SDLK_u,         EShortcut::RECRUITMENT_UPGRADE       },
+		{SDLK_a,         EShortcut::RECRUITMENT_UPGRADE_ALL   },
+		{SDLK_u,         EShortcut::RECRUITMENT_UPGRADE_ALL   },
+		{SDLK_h,         EShortcut::KINGDOM_HEROES_TAB        },
+		{SDLK_t,         EShortcut::KINGDOM_TOWNS_TAB         },
+		{SDLK_d,         EShortcut::HERO_DISMISS              },
+		{SDLK_c,         EShortcut::HERO_COMMANDER            },
+		{SDLK_l,         EShortcut::HERO_LOOSE_FORMATION      },
+		{SDLK_t,         EShortcut::HERO_TIGHT_FORMATION      },
+		{SDLK_b,         EShortcut::HERO_TOGGLE_TACTICS       },
+		{SDLK_a,         EShortcut::SPELLBOOK_TAB_ADVENTURE   },
+		{SDLK_c,         EShortcut::SPELLBOOK_TAB_COMBAT      }
+	};
+
+	auto range = keyToShortcut.equal_range(key);
+
+	// FIXME: some code expects calls to keyPressed / captureThisKey even without defined hotkeys
+	if (range.first == range.second)
+		return {EShortcut::NONE};
+
+	std::vector<EShortcut> result;
+
+	for (auto it = range.first; it != range.second; ++it)
+		result.push_back(it->second);
+
+	return result;
+}
+
+EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
+{
+	static const std::map<std::string, EShortcut> shortcutNames = {
+		{"globalAccept",             EShortcut::GLOBAL_ACCEPT             },
+		{"globalCancel",             EShortcut::GLOBAL_CANCEL             },
+		{"globalReturn",             EShortcut::GLOBAL_RETURN             },
+		{"globalFullscreen",         EShortcut::GLOBAL_FULLSCREEN         },
+		{"globalOptions",            EShortcut::GLOBAL_OPTIONS            },
+		{"globalBackspace",          EShortcut::GLOBAL_BACKSPACE          },
+		{"globalMoveFocus",          EShortcut::GLOBAL_MOVE_FOCUS         },
+		{"moveLeft",                 EShortcut::MOVE_LEFT                 },
+		{"moveRight",                EShortcut::MOVE_RIGHT                },
+		{"moveUp",                   EShortcut::MOVE_UP                   },
+		{"moveDown",                 EShortcut::MOVE_DOWN                 },
+		{"moveFirst",                EShortcut::MOVE_FIRST                },
+		{"moveLast",                 EShortcut::MOVE_LAST                 },
+		{"movePageUp",               EShortcut::MOVE_PAGE_UP              },
+		{"movePageDown",             EShortcut::MOVE_PAGE_DOWN            },
+		{"selectIndex1",             EShortcut::SELECT_INDEX_1            },
+		{"selectIndex2",             EShortcut::SELECT_INDEX_2            },
+		{"selectIndex3",             EShortcut::SELECT_INDEX_3            },
+		{"selectIndex4",             EShortcut::SELECT_INDEX_4            },
+		{"selectIndex5",             EShortcut::SELECT_INDEX_5            },
+		{"selectIndex6",             EShortcut::SELECT_INDEX_6            },
+		{"selectIndex7",             EShortcut::SELECT_INDEX_7            },
+		{"selectIndex8",             EShortcut::SELECT_INDEX_8            },
+		{"mainMenuNewGame",          EShortcut::MAIN_MENU_NEW_GAME        },
+		{"mainMenuLoadGame",         EShortcut::MAIN_MENU_LOAD_GAME       },
+		{"mainMenuHighScores",       EShortcut::MAIN_MENU_HIGH_SCORES     },
+		{"mainMenuCredits",          EShortcut::MAIN_MENU_CREDITS         },
+		{"mainMenuQuit",             EShortcut::MAIN_MENU_QUIT            },
+		{"mainMenuBack",             EShortcut::MAIN_MENU_BACK            },
+		{"mainMenuSingleplayer",     EShortcut::MAIN_MENU_SINGLEPLAYER    },
+		{"mainMenuMultiplayer",      EShortcut::MAIN_MENU_MULTIPLAYER     },
+		{"mainMenuCampaign",         EShortcut::MAIN_MENU_CAMPAIGN        },
+		{"mainMenuTutorial",         EShortcut::MAIN_MENU_TUTORIAL        },
+		{"mainMenuCampaignSod",      EShortcut::MAIN_MENU_CAMPAIGN_SOD    },
+		{"mainMenuCampaignRoe",      EShortcut::MAIN_MENU_CAMPAIGN_ROE    },
+		{"mainMenuCampaignAb",       EShortcut::MAIN_MENU_CAMPAIGN_AB     },
+		{"mainMenuCampaignCustom",   EShortcut::MAIN_MENU_CAMPAIGN_CUSTOM },
+		{"lobbyBeginGame",           EShortcut::LOBBY_BEGIN_GAME          },
+		{"lobbyLoadGame",            EShortcut::LOBBY_LOAD_GAME           },
+		{"lobbySaveGame",            EShortcut::LOBBY_SAVE_GAME           },
+		{"lobbyRandomMap",           EShortcut::LOBBY_RANDOM_MAP          },
+		{"lobbyHideChat",            EShortcut::LOBBY_HIDE_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   },
+		{"gameActivateConsole",      EShortcut::GAME_ACTIVATE_CONSOLE     },
+		{"adventureGameOptions",     EShortcut::ADVENTURE_GAME_OPTIONS    },
+		{"adventureToggleGrid",      EShortcut::ADVENTURE_TOGGLE_GRID     },
+		{"adventureToggleSleep",     EShortcut::ADVENTURE_TOGGLE_SLEEP    },
+		{"adventureMoveHero",        EShortcut::ADVENTURE_MOVE_HERO       },
+		{"adventureVisitObject",     EShortcut::ADVENTURE_VISIT_OBJECT    },
+		{"adventureMoveHeroSW",      EShortcut::ADVENTURE_MOVE_HERO_SW    },
+		{"adventureMoveHeroSS",      EShortcut::ADVENTURE_MOVE_HERO_SS    },
+		{"adventureMoveHeroSE",      EShortcut::ADVENTURE_MOVE_HERO_SE    },
+		{"adventureMoveHeroWW",      EShortcut::ADVENTURE_MOVE_HERO_WW    },
+		{"adventureMoveHeroEE",      EShortcut::ADVENTURE_MOVE_HERO_EE    },
+		{"adventureMoveHeroNW",      EShortcut::ADVENTURE_MOVE_HERO_NW    },
+		{"adventureMoveHeroNN",      EShortcut::ADVENTURE_MOVE_HERO_NN    },
+		{"adventureMoveHeroNE",      EShortcut::ADVENTURE_MOVE_HERO_NE    },
+		{"adventureViewSelected",    EShortcut::ADVENTURE_VIEW_SELECTED   },
+		{"adventureNextObject",      EShortcut::ADVENTURE_NEXT_OBJECT     },
+		{"adventureNextTown",        EShortcut::ADVENTURE_NEXT_TOWN       },
+		{"adventureNextHero",        EShortcut::ADVENTURE_NEXT_HERO       },
+		{"adventureFirstTown",       EShortcut::ADVENTURE_FIRST_TOWN      },
+		{"adventureFirstHero",       EShortcut::ADVENTURE_FIRST_HERO      },
+		{"adventureViewScenario",    EShortcut::ADVENTURE_VIEW_SCENARIO   },
+		{"adventureDigGrail",        EShortcut::ADVENTURE_DIG_GRAIL       },
+		{"adventureViewPuzzle",      EShortcut::ADVENTURE_VIEW_PUZZLE     },
+		{"adventureViewWorld",       EShortcut::ADVENTURE_VIEW_WORLD      },
+		{"adventureToggleMapLevel",  EShortcut::ADVENTURE_TOGGLE_MAP_LEVEL},
+		{"adventureKingdomOverview", EShortcut::ADVENTURE_KINGDOM_OVERVIEW},
+		{"adventureQuestLog",        EShortcut::ADVENTURE_QUEST_LOG       },
+		{"adventureCastSpell",       EShortcut::ADVENTURE_CAST_SPELL      },
+		{"adventureEndTurn",         EShortcut::ADVENTURE_END_TURN        },
+		{"adventureThievesGuild",    EShortcut::ADVENTURE_THIEVES_GUILD   },
+		{"battleToggleQueue",        EShortcut::BATTLE_TOGGLE_QUEUE       },
+		{"battleUseCreatureSpell",   EShortcut::BATTLE_USE_CREATURE_SPELL },
+		{"battleSurrender",          EShortcut::BATTLE_SURRENDER          },
+		{"battleRetreat",            EShortcut::BATTLE_RETREAT            },
+		{"battleAutocombat",         EShortcut::BATTLE_AUTOCOMBAT         },
+		{"battleCastSpell",          EShortcut::BATTLE_CAST_SPELL         },
+		{"battleWait",               EShortcut::BATTLE_WAIT               },
+		{"battleDefend",             EShortcut::BATTLE_DEFEND             },
+		{"battleConsoleUp",          EShortcut::BATTLE_CONSOLE_UP         },
+		{"battleConsoleDown",        EShortcut::BATTLE_CONSOLE_DOWN       },
+		{"battleTacticsNext",        EShortcut::BATTLE_TACTICS_NEXT       },
+		{"battleTacticsEnd",         EShortcut::BATTLE_TACTICS_END        },
+		{"battleSelectAction",       EShortcut::BATTLE_SELECT_ACTION      },
+		{"townOpenTavern",           EShortcut::TOWN_OPEN_TAVERN          },
+		{"townSwapArmies",           EShortcut::TOWN_SWAP_ARMIES          },
+		{"recruitmentMax",           EShortcut::RECRUITMENT_MAX           },
+		{"recruitmentMin",           EShortcut::RECRUITMENT_MIN           },
+		{"recruitmentUpgrade",       EShortcut::RECRUITMENT_UPGRADE       },
+		{"recruitmentUpgradeAll",    EShortcut::RECRUITMENT_UPGRADE_ALL   },
+		{"kingdomHeroesTab",         EShortcut::KINGDOM_HEROES_TAB        },
+		{"kingdomTownsTab",          EShortcut::KINGDOM_TOWNS_TAB         },
+		{"heroDismiss",              EShortcut::HERO_DISMISS              },
+		{"heroCommander",            EShortcut::HERO_COMMANDER            },
+		{"heroLooseFormation",       EShortcut::HERO_LOOSE_FORMATION      },
+		{"heroTightFormation",       EShortcut::HERO_TIGHT_FORMATION      },
+		{"heroToggleTactics",        EShortcut::HERO_TOGGLE_TACTICS       },
+		{"spellbookTabAdventure",    EShortcut::SPELLBOOK_TAB_ADVENTURE   },
+		{"spellbookTabCombat",       EShortcut::SPELLBOOK_TAB_COMBAT      }
+	};
+
+	if (shortcutNames.count(identifier))
+		return shortcutNames.at(identifier);
+	return EShortcut::NONE;
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 24 - 0
client/gui/ShortcutHandler.h

@@ -0,0 +1,24 @@
+/*
+ * ShortcutHandler.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
+
+enum class EShortcut;
+using SDL_Keycode = int32_t;
+
+class ShortcutHandler
+{
+public:
+	/// returns list of shortcuts assigned to provided SDL keycode
+	std::vector<EShortcut> translateKeycode(SDL_Keycode key) const;
+
+	/// attempts to find shortcut by its unique identifier. Returns EShortcut::NONE on failure
+	EShortcut findShortcut(const std::string & identifier ) const;
+};

+ 4 - 3
client/lobby/CBonusSelection.cpp

@@ -33,6 +33,7 @@
 #include "../render/IImage.h"
 #include "../render/CAnimation.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 
 #include "../../lib/filesystem/Filesystem.h"
 #include "../../lib/CGeneralTextHandler.h"
@@ -67,9 +68,9 @@ CBonusSelection::CBonusSelection()
 
 	panelBackground = std::make_shared<CPicture>("CAMPBRF.BMP", 456, 6);
 
-	buttonStart = std::make_shared<CButton>(Point(475, 536), "CBBEGIB.DEF", CButton::tooltip(), std::bind(&CBonusSelection::startMap, this), SDLK_RETURN);
-	buttonRestart = std::make_shared<CButton>(Point(475, 536), "CBRESTB.DEF", CButton::tooltip(), std::bind(&CBonusSelection::restartMap, this), SDLK_RETURN);
-	buttonBack = std::make_shared<CButton>(Point(624, 536), "CBCANCB.DEF", CButton::tooltip(), std::bind(&CBonusSelection::goBack, this), SDLK_ESCAPE);
+	buttonStart = std::make_shared<CButton>(Point(475, 536), "CBBEGIB.DEF", CButton::tooltip(), std::bind(&CBonusSelection::startMap, this), EShortcut::GLOBAL_ACCEPT);
+	buttonRestart = std::make_shared<CButton>(Point(475, 536), "CBRESTB.DEF", CButton::tooltip(), std::bind(&CBonusSelection::restartMap, this), EShortcut::GLOBAL_ACCEPT);
+	buttonBack = std::make_shared<CButton>(Point(624, 536), "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());
 

+ 9 - 10
client/lobby/CLobbyScreen.cpp

@@ -17,6 +17,7 @@
 #include "../CServerHandler.h"
 
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../widgets/Buttons.h"
 #include "../windows/InfoWindows.h"
 
@@ -40,17 +41,17 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType)
 	{
 		tabSel->callOnSelect = std::bind(&IServerAPI::setMapInfo, CSH, _1, nullptr);
 
-		buttonSelect = std::make_shared<CButton>(Point(411, 80), "GSPBUTT.DEF", CGI->generaltexth->zelp[45], 0, SDLK_s);
+		buttonSelect = std::make_shared<CButton>(Point(411, 80), "GSPBUTT.DEF", CGI->generaltexth->zelp[45], 0, EShortcut::LOBBY_SELECT_SCENARIO);
 		buttonSelect->addCallback([&]()
 		{
 			toggleTab(tabSel);
 			CSH->setMapInfo(tabSel->getSelectedMapInfo());
 		});
 
-		buttonOptions = std::make_shared<CButton>(Point(411, 510), "GSPBUTT.DEF", CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabOpt), SDLK_a);
+		buttonOptions = std::make_shared<CButton>(Point(411, 510), "GSPBUTT.DEF", CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabOpt), EShortcut::LOBBY_ADDITIONAL_OPTIONS);
 	};
 
-	buttonChat = std::make_shared<CButton>(Point(619, 83), "GSPBUT2.DEF", CGI->generaltexth->zelp[48], std::bind(&CLobbyScreen::toggleChat, this), SDLK_h);
+	buttonChat = std::make_shared<CButton>(Point(619, 83), "GSPBUT2.DEF", CGI->generaltexth->zelp[48], std::bind(&CLobbyScreen::toggleChat, this), EShortcut::LOBBY_HIDE_CHAT);
 	buttonChat->addTextOverlay(CGI->generaltexth->allTexts[532], FONT_SMALL);
 
 	switch(screenType)
@@ -60,7 +61,7 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType)
 		tabOpt = std::make_shared<OptionsTab>();
 		tabRand = std::make_shared<RandomMapTab>();
 		tabRand->mapInfoChanged += std::bind(&IServerAPI::setMapInfo, CSH, _1, _2);
-		buttonRMG = std::make_shared<CButton>(Point(411, 105), "GSPBUTT.DEF", CGI->generaltexth->zelp[47], 0, SDLK_r);
+		buttonRMG = std::make_shared<CButton>(Point(411, 105), "GSPBUTT.DEF", CGI->generaltexth->zelp[47], 0, EShortcut::LOBBY_RANDOM_MAP);
 		buttonRMG->addCallback([&]()
 		{
 			toggleTab(tabRand);
@@ -69,30 +70,28 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType)
 
 		card->iconDifficulty->addCallback(std::bind(&IServerAPI::setDifficulty, CSH, _1));
 
-		buttonStart = std::make_shared<CButton>(Point(411, 535), "SCNRBEG.DEF", CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, true), SDLK_b);
+		buttonStart = std::make_shared<CButton>(Point(411, 535), "SCNRBEG.DEF", CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, true), EShortcut::LOBBY_BEGIN_GAME);
 		initLobby();
 		break;
 	}
 	case ESelectionScreen::loadGame:
 	{
 		tabOpt = std::make_shared<OptionsTab>();
-		buttonStart = std::make_shared<CButton>(Point(411, 535), "SCNRLOD.DEF", CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, true), SDLK_l);
+		buttonStart = std::make_shared<CButton>(Point(411, 535), "SCNRLOD.DEF", CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, true), EShortcut::LOBBY_LOAD_GAME);
 		initLobby();
 		break;
 	}
 	case ESelectionScreen::campaignList:
 		tabSel->callOnSelect = std::bind(&IServerAPI::setMapInfo, CSH, _1, nullptr);
-		buttonStart = std::make_shared<CButton>(Point(411, 535), "SCNRLOD.DEF", CButton::tooltip(), std::bind(&CLobbyScreen::startCampaign, this), SDLK_b);
+		buttonStart = std::make_shared<CButton>(Point(411, 535), "SCNRLOD.DEF", CButton::tooltip(), std::bind(&CLobbyScreen::startCampaign, this), EShortcut::LOBBY_BEGIN_GAME);
 		break;
 	}
 
-	buttonStart->assignedKeys.insert(SDLK_RETURN);
-
 	buttonBack = std::make_shared<CButton>(Point(581, 535), "SCNRBACK.DEF", CGI->generaltexth->zelp[105], [&]()
 	{
 		CSH->sendClientDisconnecting();
 		close();
-	}, SDLK_ESCAPE);
+	}, EShortcut::GLOBAL_CANCEL);
 }
 
 CLobbyScreen::~CLobbyScreen()

+ 2 - 2
client/lobby/CSavingScreen.cpp

@@ -15,6 +15,7 @@
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/TextControls.h"
 
@@ -39,8 +40,7 @@ CSavingScreen::CSavingScreen()
 	tabSel->toggleMode();
 
 	tabSel->callOnSelect = std::bind(&CSavingScreen::changeSelection, this, _1);
-	buttonStart = std::make_shared<CButton>(Point(411, 535), "SCNRSAV.DEF", CGI->generaltexth->zelp[103], std::bind(&CSavingScreen::saveGame, this), SDLK_s);
-	buttonStart->assignedKeys.insert(SDLK_RETURN);
+	buttonStart = std::make_shared<CButton>(Point(411, 535), "SCNRSAV.DEF", CGI->generaltexth->zelp[103], std::bind(&CSavingScreen::saveGame, this), EShortcut::LOBBY_SAVE_GAME);
 }
 
 const CMapInfo * CSavingScreen::getMapInfo()

+ 2 - 1
client/lobby/CScenarioInfoScreen.cpp

@@ -15,6 +15,7 @@
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../widgets/Buttons.h"
 
 #include "../../CCallback.h"
@@ -43,7 +44,7 @@ CScenarioInfoScreen::CScenarioInfoScreen()
 	card->changeSelection();
 
 	card->iconDifficulty->setSelected(getCurrentDifficulty());
-	buttonBack = std::make_shared<CButton>(Point(584, 535), "SCNRBACK.DEF", CGI->generaltexth->zelp[105], [=](){ close();}, SDLK_ESCAPE);
+	buttonBack = std::make_shared<CButton>(Point(584, 535), "SCNRBACK.DEF", CGI->generaltexth->zelp[105], [=](){ close();}, EShortcut::GLOBAL_CANCEL);
 }
 
 CScenarioInfoScreen::~CScenarioInfoScreen()

+ 4 - 3
client/lobby/CSelectionBase.cpp

@@ -25,6 +25,7 @@
 #include "../CPlayerInterface.h"
 #include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../mainmenu/CMainMenu.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/Buttons.h"
@@ -84,7 +85,7 @@ CSelectionBase::CSelectionBase(ESelectionScreen type)
 	}
 	pos = background->center();
 	card = std::make_shared<InfoCard>();
-	buttonBack = std::make_shared<CButton>(Point(581, 535), "SCNRBACK.DEF", CGI->generaltexth->zelp[105], [=](){ close();}, SDLK_ESCAPE);
+	buttonBack = std::make_shared<CButton>(Point(581, 535), "SCNRBACK.DEF", CGI->generaltexth->zelp[105], [=](){ close();}, EShortcut::GLOBAL_CANCEL);
 }
 
 void CSelectionBase::toggleTab(std::shared_ptr<CIntObject> tab)
@@ -318,9 +319,9 @@ CChatBox::CChatBox(const Rect & rect)
 	chatHistory->label->color = Colors::GREEN;
 }
 
-void CChatBox::keyPressed(const SDL_Keycode & key)
+void CChatBox::keyPressed(EShortcut key)
 {
-	if(key == SDLK_RETURN && inputBox->getText().size())
+	if(key == EShortcut::GLOBAL_ACCEPT && inputBox->getText().size())
 	{
 		CSH->sendMessage(inputBox->getText());
 		inputBox->setText("");

+ 1 - 1
client/lobby/CSelectionBase.h

@@ -120,7 +120,7 @@ public:
 
 	CChatBox(const Rect & rect);
 
-	void keyPressed(const SDL_Keycode & key) override;
+	void keyPressed(EShortcut key) override;
 	void addNewMessage(const std::string & text);
 };
 

+ 8 - 7
client/lobby/SelectionTab.cpp

@@ -17,6 +17,7 @@
 #include "../CPlayerInterface.h"
 #include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/MiscWidgets.h"
@@ -281,27 +282,27 @@ void SelectionTab::clickLeft(tribool down, bool previousState)
 	}
 }
 
-void SelectionTab::keyPressed(const SDL_Keycode & key)
+void SelectionTab::keyPressed(EShortcut key)
 {
 	int moveBy = 0;
 	switch(key)
 	{
-	case SDLK_UP:
+	case EShortcut::MOVE_UP:
 		moveBy = -1;
 		break;
-	case SDLK_DOWN:
+	case EShortcut::MOVE_DOWN:
 		moveBy = +1;
 		break;
-	case SDLK_PAGEUP:
+	case EShortcut::MOVE_PAGE_UP:
 		moveBy = -(int)listItems.size() + 1;
 		break;
-	case SDLK_PAGEDOWN:
+	case EShortcut::MOVE_PAGE_DOWN:
 		moveBy = +(int)listItems.size() - 1;
 		break;
-	case SDLK_HOME:
+	case EShortcut::MOVE_FIRST:
 		select(-slider->getValue());
 		return;
-	case SDLK_END:
+	case EShortcut::MOVE_LAST:
 		select((int)curItems.size() - slider->getValue());
 		return;
 	default:

+ 1 - 1
client/lobby/SelectionTab.h

@@ -66,7 +66,7 @@ public:
 	void toggleMode();
 
 	void clickLeft(tribool down, bool previousState) override;
-	void keyPressed(const SDL_Keycode & key) override;
+	void keyPressed(EShortcut key) override;
 
 	void onDoubleClick() override;
 

+ 2 - 1
client/mainmenu/CCampaignScreen.cpp

@@ -19,6 +19,7 @@
 #include "../CPlayerInterface.h"
 #include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/MiscWidgets.h"
@@ -77,7 +78,7 @@ std::shared_ptr<CButton> CCampaignScreen::createExitButton(const JsonNode & butt
 	if(!button["help"].isNull() && button["help"].Float() > 0)
 		help = CGI->generaltexth->zelp[(size_t)button["help"].Float()];
 
-	return std::make_shared<CButton>(Point((int)button["x"].Float(), (int)button["y"].Float()), button["name"].String(), help, [=](){ close();}, (int)button["hotkey"].Float());
+	return std::make_shared<CButton>(Point((int)button["x"].Float(), (int)button["y"].Float()), button["name"].String(), help, [=](){ close();}, EShortcut::GLOBAL_CANCEL);
 }
 
 CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config)

+ 10 - 6
client/mainmenu/CMainMenu.cpp

@@ -19,6 +19,8 @@
 #include "../gui/CursorHandler.h"
 #include "../windows/GUIClasses.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/ShortcutHandler.h"
+#include "../gui/Shortcut.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/MiscWidgets.h"
@@ -230,7 +232,9 @@ std::shared_ptr<CButton> CMenuEntry::createButton(CMenuScreen * parent, const Js
 	if(posy < 0)
 		posy = pos.h + posy;
 
-	auto result = std::make_shared<CButton>(Point(posx, posy), button["name"].String(), help, command, (int)button["hotkey"].Float());
+	EShortcut shortcut = GH.shortcutsHandler().findShortcut(button["shortcut"].String());
+
+	auto result = std::make_shared<CButton>(Point(posx, posy), button["name"].String(), help, command, shortcut);
 
 	if (button["center"].Bool())
 		result->moveBy(Point(-result->pos.w/2, -result->pos.h/2));
@@ -382,7 +386,7 @@ CMultiMode::CMultiMode(ESelectionScreen ScreenType)
 	buttonHotseat = std::make_shared<CButton>(Point(373, 78), "MUBHOT.DEF", CGI->generaltexth->zelp[266], std::bind(&CMultiMode::hostTCP, this));
 	buttonHost = std::make_shared<CButton>(Point(373, 78 + 57 * 1), "MUBHOST.DEF", CButton::tooltip("Host TCP/IP game", ""), std::bind(&CMultiMode::hostTCP, this));
 	buttonJoin = std::make_shared<CButton>(Point(373, 78 + 57 * 2), "MUBJOIN.DEF", CButton::tooltip("Join TCP/IP game", ""), std::bind(&CMultiMode::joinTCP, this));
-	buttonCancel = std::make_shared<CButton>(Point(373, 424), "MUBCANC.DEF", CGI->generaltexth->zelp[288], [=](){ close();}, SDLK_ESCAPE);
+	buttonCancel = std::make_shared<CButton>(Point(373, 424), "MUBCANC.DEF", CGI->generaltexth->zelp[288], [=](){ close();}, EShortcut::GLOBAL_CANCEL);
 }
 
 void CMultiMode::hostTCP()
@@ -422,8 +426,8 @@ CMultiPlayers::CMultiPlayers(const std::string & firstPlayer, ESelectionScreen S
 		inputNames[i]->cb += std::bind(&CMultiPlayers::onChange, this, _1);
 	}
 
-	buttonOk = std::make_shared<CButton>(Point(95, 338), "MUBCHCK.DEF", CGI->generaltexth->zelp[560], std::bind(&CMultiPlayers::enterSelectionScreen, this), SDLK_RETURN);
-	buttonCancel = std::make_shared<CButton>(Point(205, 338), "MUBCANC.DEF", CGI->generaltexth->zelp[561], [=](){ close();}, SDLK_ESCAPE);
+	buttonOk = std::make_shared<CButton>(Point(95, 338), "MUBCHCK.DEF", CGI->generaltexth->zelp[560], std::bind(&CMultiPlayers::enterSelectionScreen, this), EShortcut::GLOBAL_ACCEPT);
+	buttonCancel = std::make_shared<CButton>(Point(205, 338), "MUBCANC.DEF", CGI->generaltexth->zelp[561], [=](){ close();}, EShortcut::GLOBAL_CANCEL);
 	statusBar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(7, 381, 348, 18), 7, 381));
 
 	inputNames[0]->setText(firstPlayer, true);
@@ -471,14 +475,14 @@ CSimpleJoinScreen::CSimpleJoinScreen(bool host)
 		inputAddress->cb += std::bind(&CSimpleJoinScreen::onChange, this, _1);
 		inputPort->cb += std::bind(&CSimpleJoinScreen::onChange, this, _1);
 		inputPort->filters += std::bind(&CTextInput::numberFilter, _1, _2, 0, 65535);
-		buttonOk = std::make_shared<CButton>(Point(26, 142), "MUBCHCK.DEF", CGI->generaltexth->zelp[560], std::bind(&CSimpleJoinScreen::connectToServer, this), SDLK_RETURN);
+		buttonOk = std::make_shared<CButton>(Point(26, 142), "MUBCHCK.DEF", CGI->generaltexth->zelp[560], std::bind(&CSimpleJoinScreen::connectToServer, this), EShortcut::GLOBAL_ACCEPT);
 
 		inputAddress->giveFocus();
 	}
 	inputAddress->setText(host ? CServerHandler::localhostAddress : CSH->getHostAddress(), true);
 	inputPort->setText(std::to_string(CSH->getHostPort()), true);
 
-	buttonCancel = std::make_shared<CButton>(Point(142, 142), "MUBCANC.DEF", CGI->generaltexth->zelp[561], std::bind(&CSimpleJoinScreen::leaveScreen, this), SDLK_ESCAPE);
+	buttonCancel = std::make_shared<CButton>(Point(142, 142), "MUBCANC.DEF", CGI->generaltexth->zelp[561], std::bind(&CSimpleJoinScreen::leaveScreen, this), EShortcut::GLOBAL_CANCEL);
 	statusBar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(7, 186, 218, 18), 7, 186));
 }
 

+ 1 - 1
client/renderSDL/CTrueTypeFont.h

@@ -17,7 +17,7 @@ VCMI_LIB_NAMESPACE_END
 
 class CBitmapFont;
 
-typedef struct _TTF_Font TTF_Font;
+using TTF_Font = struct _TTF_Font;
 
 class CTrueTypeFont : public IFont
 {

+ 21 - 21
client/renderSDL/SDL_Extensions.h

@@ -45,8 +45,8 @@ SDL_Color toSDL(const ColorRGBA & color);
 void setColors(SDL_Surface *surface, SDL_Color *colors, int firstcolor, int ncolors);
 void setAlpha(SDL_Surface * bg, int value);
 
-typedef void (*TColorPutter)(uint8_t *&ptr, const uint8_t & R, const uint8_t & G, const uint8_t & B);
-typedef void (*TColorPutterAlpha)(uint8_t *&ptr, const uint8_t & R, const uint8_t & G, const uint8_t & B, const uint8_t & A);
+using TColorPutter = void (*)(uint8_t *&, const uint8_t &, const uint8_t &, const uint8_t &);
+using TColorPutterAlpha = void (*)(uint8_t *&, const uint8_t &, const uint8_t &, const uint8_t &, const uint8_t &);
 
 	void blitAt(SDL_Surface * src, int x, int y, SDL_Surface * dst);
 	void blitAt(SDL_Surface * src, const Rect & pos, SDL_Surface * dst);
@@ -57,23 +57,23 @@ typedef void (*TColorPutterAlpha)(uint8_t *&ptr, const uint8_t & R, const uint8_
 	void blitSurface(SDL_Surface * src, const Rect & srcRect, SDL_Surface * dst, const Point & dest);
 	void blitSurface(SDL_Surface * src, SDL_Surface * dst, const Point & dest);
 
-	void fillSurface(SDL_Surface *dst, const SDL_Color & color);
-	void fillRect(SDL_Surface *dst, const Rect & dstrect, const SDL_Color & color);
+	void fillSurface(SDL_Surface * dst, const SDL_Color & color);
+	void fillRect(SDL_Surface * dst, const Rect & dstrect, const SDL_Color & color);
 
-	void updateRect(SDL_Surface *surface, const Rect & rect);
+	void updateRect(SDL_Surface * surface, const Rect & rect);
 
-	void putPixelWithoutRefresh(SDL_Surface *ekran, const int & x, const int & y, const uint8_t & R, const uint8_t & G, const uint8_t & B, uint8_t A = 255);
+	void putPixelWithoutRefresh(SDL_Surface * ekran, const int & x, const int & y, const uint8_t & R, const uint8_t & G, const uint8_t & B, uint8_t A = 255);
 	void putPixelWithoutRefreshIfInSurf(SDL_Surface *ekran, const int & x, const int & y, const uint8_t & R, const uint8_t & G, const uint8_t & B, uint8_t A = 255);
 
 	SDL_Surface * verticalFlip(SDL_Surface * toRot); //vertical flip
 	SDL_Surface * horizontalFlip(SDL_Surface * toRot); //horizontal flip
-	uint32_t getPixel(SDL_Surface *surface, const int & x, const int & y, bool colorByte = false);
+	uint32_t getPixel(SDL_Surface * surface, const int & x, const int & y, bool colorByte = false);
 	bool isTransparent(SDL_Surface * srf, int x, int y); //checks if surface is transparent at given position
-	bool isTransparent(SDL_Surface * srf, const Point &  position); //checks if surface is transparent at given position
+	bool isTransparent(SDL_Surface * srf, const Point & position); //checks if surface is transparent at given position
 
-	uint8_t *getPxPtr(const SDL_Surface * const &srf, const int x, const int y);
-	TColorPutter getPutterFor(SDL_Surface  * const &dest, int incrementing); //incrementing: -1, 0, 1
-	TColorPutterAlpha getPutterAlphaFor(SDL_Surface  * const &dest, int incrementing); //incrementing: -1, 0, 1
+	uint8_t * getPxPtr(const SDL_Surface * const & srf, const int x, const int y);
+	TColorPutter getPutterFor(SDL_Surface * const & dest, int incrementing); //incrementing: -1, 0, 1
+	TColorPutterAlpha getPutterAlphaFor(SDL_Surface * const & dest, int incrementing); //incrementing: -1, 0, 1
 
 	template<int bpp>
 	int blit8bppAlphaTo24bppT(const SDL_Surface * src, const Rect & srcRect, SDL_Surface * dst, const Point & dstPoint); //blits 8 bpp surface with alpha channel to 24 bpp surface
@@ -84,8 +84,8 @@ typedef void (*TColorPutterAlpha)(uint8_t *&ptr, const uint8_t & R, const uint8_
 	void drawLine(SDL_Surface * sur, const Point & from, const Point & dest, const SDL_Color & color1, const SDL_Color & color2);
 	void drawLineDashed(SDL_Surface * sur, const Point & from, const Point & dest, const SDL_Color & color);
 
-	void drawBorder(SDL_Surface * sur, int x, int y, int w, int h, const SDL_Color &color, int depth = 1);
-	void drawBorder(SDL_Surface * sur, const Rect &r, const SDL_Color &color, int depth = 1);
+	void drawBorder(SDL_Surface * sur, int x, int y, int w, int h, const SDL_Color & color, int depth = 1);
+	void drawBorder(SDL_Surface * sur, const Rect & r, const SDL_Color & color, int depth = 1);
 	void setPlayerColor(SDL_Surface * sur, PlayerColor player); //sets correct color of flags; -1 for neutral
 
 	SDL_Surface * newSurface(int w, int h, SDL_Surface * mod); //creates new surface, with flags/format same as in surface given
@@ -97,18 +97,18 @@ typedef void (*TColorPutterAlpha)(uint8_t *&ptr, const uint8_t & R, const uint8_
 
 	//scale surface to required size.
 	//nearest neighbour algorithm
-	SDL_Surface * scaleSurfaceFast(SDL_Surface *surf, int width, int height);
+	SDL_Surface * scaleSurfaceFast(SDL_Surface * surf, int width, int height);
 	// bilinear filtering. Uses fallback to scaleSurfaceFast in case of indexed surfaces
-	SDL_Surface * scaleSurface(SDL_Surface *surf, int width, int height);
+	SDL_Surface * scaleSurface(SDL_Surface * surf, int width, int height);
 
 	template<int bpp>
-	void convertToGrayscaleBpp( SDL_Surface * surf, const Rect & rect );
+	void convertToGrayscaleBpp(SDL_Surface * surf, const Rect & rect);
 	void convertToGrayscale(SDL_Surface * surf, const Rect & rect);
 
-	bool isResolutionSupported(const std::vector<Point> & resolutions, const Point toTest );
+	bool isResolutionSupported(const std::vector<Point> & resolutions, const Point toTest);
 
 	std::vector<Point> getSupportedResolutions();
-	std::vector<Point> getSupportedResolutions( int displayIndex);
+	std::vector<Point> getSupportedResolutions(int displayIndex);
 
 	void setColorKey(SDL_Surface * surface, SDL_Color color);
 
@@ -118,13 +118,13 @@ typedef void (*TColorPutterAlpha)(uint8_t *&ptr, const uint8_t & R, const uint8_
 	void setDefaultColorKeyPresize(SDL_Surface * surface);
 
 	/// helper that will safely set and un-set ClipRect for SDL_Surface
-	class CClipRectGuard : boost::noncopyable
+	class CClipRectGuard: boost::noncopyable
 	{
 		SDL_Surface * surf;
 		Rect oldRect;
+
 	public:
-		CClipRectGuard(SDL_Surface * surface, const Rect & rect):
-			surf(surface)
+		CClipRectGuard(SDL_Surface * surface, const Rect & rect): surf(surface)
 		{
 			CSDL_Ext::getClipRect(surf, oldRect);
 			CSDL_Ext::setClipRect(surf, rect);

+ 2 - 2
client/renderSDL/SDL_PixelAccess.h

@@ -47,7 +47,7 @@ namespace Channels
 
 		static void STRONG_INLINE set(uint8_t *ptr, uint8_t value)
 		{
-			uint16_t * const pixel = (uint16_t*)ptr;
+			auto * const pixel = (uint16_t *)ptr;
 			uint8_t subpx = value >> (8 - bits);
 			*pixel = (*pixel & ~mask) | ((subpx << shift) & mask );
 		}
@@ -226,7 +226,7 @@ STRONG_INLINE void ColorPutter<2, incrementPtr>::PutColor(uint8_t *&ptr, const u
 	if(incrementPtr == -1)
 		ptr -= 2;
 
-	uint16_t * const px = (uint16_t*)ptr;
+	auto * const px = (uint16_t *)ptr;
 	*px = (B>>3) + ((G>>2) << 5) + ((R>>3) << 11); //drop least significant bits of 24 bpp encoded color
 
 	if(incrementPtr == 1)

+ 12 - 11
client/widgets/Buttons.cpp

@@ -19,6 +19,7 @@
 #include "../battle/BattleInterface.h"
 #include "../battle/BattleInterfaceClasses.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../windows/InfoWindows.h"
 #include "../render/CAnimation.h"
 #include "../renderSDL/SDL_Extensions.h"
@@ -223,7 +224,7 @@ void CButton::hover (bool on)
 	}
 }
 
-CButton::CButton(Point position, const std::string &defName, const std::pair<std::string, std::string> &help, CFunctionList<void()> Callback, int key, bool playerColoredButton):
+CButton::CButton(Point position, const std::string &defName, const std::pair<std::string, std::string> &help, CFunctionList<void()> Callback, EShortcut key, bool playerColoredButton):
     CKeyShortcut(key),
     callback(Callback)
 {
@@ -345,7 +346,7 @@ void CToggleBase::addCallback(std::function<void(bool)> function)
 }
 
 CToggleButton::CToggleButton(Point position, const std::string &defName, const std::pair<std::string, std::string> &help,
-                             CFunctionList<void(bool)> callback, int key, bool playerColoredButton):
+							 CFunctionList<void(bool)> callback, EShortcut key, bool playerColoredButton):
   CButton(position, defName, help, 0, key, playerColoredButton),
   CToggleBase(callback)
 {
@@ -800,37 +801,37 @@ void CSlider::wheelScrolled(bool down, bool in)
 	moveTo(value + 3 * (positive ? +scrollStep : -scrollStep));
 }
 
-void CSlider::keyPressed(const SDL_Keycode & key)
+void CSlider::keyPressed(EShortcut key)
 {
 	int moveDest = value;
 	switch(key)
 	{
-	case SDLK_UP:
+	case EShortcut::MOVE_UP:
 		if (!horizontal)
 			moveDest = value - scrollStep;
 		break;
-	case SDLK_LEFT:
+	case EShortcut::MOVE_LEFT:
 		if (horizontal)
 			moveDest = value - scrollStep;
 		break;
-	case SDLK_DOWN:
+	case EShortcut::MOVE_DOWN:
 		if (!horizontal)
 			moveDest = value + scrollStep;
 		break;
-	case SDLK_RIGHT:
+	case EShortcut::MOVE_RIGHT:
 		if (horizontal)
 			moveDest = value + scrollStep;
 		break;
-	case SDLK_PAGEUP:
+	case EShortcut::MOVE_PAGE_UP:
 		moveDest = value - capacity + scrollStep;
 		break;
-	case SDLK_PAGEDOWN:
+	case EShortcut::MOVE_PAGE_DOWN:
 		moveDest = value + capacity - scrollStep;
 		break;
-	case SDLK_HOME:
+	case EShortcut::MOVE_FIRST:
 		moveDest = 0;
 		break;
-	case SDLK_END:
+	case EShortcut::MOVE_LAST:
 		moveDest = amount - capacity;
 		break;
 	default:

+ 3 - 3
client/widgets/Buttons.h

@@ -101,7 +101,7 @@ public:
 
 	/// Constructor
 	CButton(Point position, const std::string & defName, const std::pair<std::string, std::string> & help,
-	        CFunctionList<void()> Callback = 0, int key=0, bool playerColoredButton = false );
+			CFunctionList<void()> Callback = 0, EShortcut key = {}, bool playerColoredButton = false );
 
 	/// Appearance modifiers
 	void setIndex(size_t index, bool playerColoredButton=false);
@@ -157,7 +157,7 @@ class CToggleButton : public CButton, public CToggleBase
 
 public:
 	CToggleButton(Point position, const std::string &defName, const std::pair<std::string, std::string> &help,
-	              CFunctionList<void(bool)> Callback = 0, int key=0, bool playerColoredButton = false );
+				  CFunctionList<void(bool)> Callback = 0, EShortcut key = {}, bool playerColoredButton = false );
 	void clickLeft(tribool down, bool previousState) override;
 
 	// bring overrides into scope
@@ -276,7 +276,7 @@ public:
 
 	void addCallback(std::function<void(int)> callback);
 
-	void keyPressed(const SDL_Keycode & key) override;
+	void keyPressed(EShortcut key) override;
 	void wheelScrolled(bool down, bool in) override;
 	void clickLeft(tribool down, bool previousState) override;
 	void mouseMoved (const Point & cursorPosition) override;

+ 168 - 923
client/widgets/CArtifactHolder.cpp

@@ -11,13 +11,10 @@
 #include "CArtifactHolder.h"
 
 #include "../gui/CGuiHandler.h"
-#include "../gui/CursorHandler.h"
+#include "../gui/Shortcut.h"
 
-#include "Buttons.h"
 #include "CComponent.h"
 
-#include "../windows/CHeroWindow.h"
-#include "../windows/CSpellWindow.h"
 #include "../windows/GUIClasses.h"
 #include "../renderSDL/SDL_Extensions.h"
 #include "../CPlayerInterface.h"
@@ -25,369 +22,29 @@
 
 #include "../../CCallback.h"
 
-#include "../../lib/CArtHandler.h"
 #include "../../lib/CGeneralTextHandler.h"
 
 #include "../../lib/mapObjects/CGHeroInstance.h"
 
-CHeroArtPlace::CHeroArtPlace(Point position, const CArtifactInstance * Art)
-	: CArtPlace(position, Art),
-	locked(false),
-	picked(false),
-	marked(false),
-	ourOwner(nullptr)
-{
-	createImage();
-}
-
-void CHeroArtPlace::createImage()
-{
-	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
-
-	si32 imageIndex = 0;
-
-	if(locked)
-		imageIndex = ArtifactID::ART_LOCK;
-	else if(ourArt)
-		imageIndex = ourArt->artType->getIconIndex();
-
-	image = std::make_shared<CAnimImage>("artifact", imageIndex);
-	if(!ourArt)
-		image->disable();
-
-	selection = std::make_shared<CAnimImage>("artifact", ArtifactID::ART_SELECTION);
-	selection->disable();
-}
-
-void CHeroArtPlace::lockSlot(bool on)
-{
-	if (locked == on)
-		return;
-
-	locked = on;
-
-	if (on)
-		image->setFrame(ArtifactID::ART_LOCK);
-	else if (ourArt)
-		image->setFrame(ourArt->artType->getIconIndex());
-	else
-		image->setFrame(0);
-}
-
-void CHeroArtPlace::pickSlot(bool on)
-{
-	if (picked == on)
-		return;
-
-	picked = on;
-	if (on)
-		image->disable();
-	else
-		image->enable();
-}
-
-void CHeroArtPlace::selectSlot(bool on)
-{
-	if (marked == on)
-		return;
-
-	marked = on;
-	if (on)
-		selection->enable();
-	else
-		selection->disable();
-}
-
-void CHeroArtPlace::clickLeft(tribool down, bool previousState)
-{
-	//LRClickableAreaWTextComp::clickLeft(down);
-	bool inBackpack = ArtifactUtils::isSlotBackpack(slotID);
-	bool srcInBackpack = ArtifactUtils::isSlotBackpack(ourOwner->commonInfo->src.slotID);
-	bool srcInSameHero = ourOwner->commonInfo->src.AOH == ourOwner;
-
-	if(ourOwner->highlightModeCallback && ourArt)
-	{
-		if(down)
-		{
-			if(!ourArt->artType->isTradable()) //War Machine or Spellbook
-			{
-				LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[21]); //This item can't be traded.
-			}
-			else
-			{
-				ourOwner->unmarkSlots(false);
-				selectSlot(true);
-				ourOwner->highlightModeCallback(this);
-			}
-		}
-		return;
-	}
-
-	// If clicked on spellbook, open it only if no artifact is held at the moment.
-	if(ourArt && !down && previousState && !ourOwner->commonInfo->src.AOH)
-	{
-		if(ourArt->artType->getId() == ArtifactID::SPELLBOOK)
-				GH.pushIntT<CSpellWindow>(ourOwner->curHero, LOCPLINT, LOCPLINT->battleInt.get());
-	}
-
-	if (!down && previousState)
-	{
-		if(ourArt && ourArt->artType->getId() == ArtifactID::SPELLBOOK)
-			return; //this is handled separately
-
-		if(!ourOwner->commonInfo->src.AOH) //nothing has been clicked
-		{
-			if(ourArt  //to prevent selecting empty slots (bugfix to what GrayFace reported)
-				&&  ourOwner->curHero->tempOwner == LOCPLINT->playerID)//can't take art from another player
-			{
-				if(ourArt->artType->getId() == ArtifactID::CATAPULT) //catapult cannot be highlighted
-				{
-					std::vector<std::shared_ptr<CComponent>> catapult(1, std::make_shared<CComponent>(CComponent::artifact, 3, 0));
-					LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[312], catapult); //The Catapult must be equipped.
-					return;
-				}
-				select();
-			}
-		}
-		// Perform artifact transition
-		else if(ourArt != ourOwner->commonInfo->src.art)
-		{
-			if(inBackpack) // Backpack destination.
-			{
-				if(!srcInBackpack || slotID != ourOwner->commonInfo->src.slotID + 1)
-				{
-					const CArtifact * const cur = ourOwner->commonInfo->src.art->artType;
-
-					if(cur->getId() == ArtifactID::CATAPULT)
-					{
-						//should not happen, catapult cannot be selected
-						logGlobal->error("Attempt to move Catapult");
-					}
-					else if(cur->isBig())
-					{
-						//war machines cannot go to backpack
-						LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[153]) % cur->getNameTranslated()));
-					}
-					else
-					{
-						setMeAsDest();
-						vstd::amin(ourOwner->commonInfo->dst.slotID, ourOwner->curHero->artifactsInBackpack.size() + GameConstants::BACKPACK_START);
-						if(ArtifactUtils::isBackpackFreeSlots(ourOwner->curHero))
-						{
-							if(!srcInSameHero || ourOwner->commonInfo->dst.slotID != ourOwner->commonInfo->src.slotID)
-								ourOwner->realizeCurrentTransaction();
-						}
-						else
-						{
-							LOCPLINT->showInfoDialog(CGI->generaltexth->translate("core.genrltxt.152"));
-						}
-					}
-				}
-			}
-			//check if swap is possible
-			else if (fitsHere(ourOwner->commonInfo->src.art) &&
-				(!ourArt || ourOwner->curHero->tempOwner == LOCPLINT->playerID))
-			{
-				setMeAsDest();
-				ourOwner->realizeCurrentTransaction();
-			}
-		}
-	}
-}
-
-bool CHeroArtPlace::askToAssemble(const CGHeroInstance * hero, ArtifactPosition slot)
+void CArtPlace::setInternals(const CArtifactInstance * artInst)
 {
-	assert(hero);
-	const auto art = hero->getArt(slot);
-	assert(art);
-	auto assemblyPossibilities = ArtifactUtils::assemblyPossibilities(hero, art->getTypeId(), ArtifactUtils::isSlotEquipment(slot));
-
-	// If the artifact can be assembled, display dialog.
-	for(const auto * combination : assemblyPossibilities)
-	{
-		LOCPLINT->showArtifactAssemblyDialog(
-			art->artType,
-			combination,
-			std::bind(&CCallback::assembleArtifacts, LOCPLINT->cb.get(), hero, slot, true, combination->getId()));
-
-		if(assemblyPossibilities.size() > 2)
-			logGlobal->warn("More than one possibility of assembling on %s... taking only first", art->artType->getNameTranslated());
-		return true;
-	}
-	return false;
-}
-
-bool CHeroArtPlace::askToDisassemble(const CGHeroInstance * hero, ArtifactPosition slot)
-{
-	assert(hero);
-	const auto art = hero->getArt(slot);
-	assert(art);
-
-	if(art->canBeDisassembled())
-	{
-		if(ArtifactUtils::isSlotBackpack(slot) && !ArtifactUtils::isBackpackFreeSlots(hero, art->artType->constituents->size() - 1))
-			return false;
-		
-		LOCPLINT->showArtifactAssemblyDialog(
-			art->artType,
-			nullptr,
-			std::bind(&CCallback::assembleArtifacts, LOCPLINT->cb.get(), hero, slot, false, ArtifactID()));
-		return true;
-	}
-	return false;
-}
-
-void CHeroArtPlace::clickRight(tribool down, bool previousState)
-{
-	if(ourArt && down && !locked && text.size() && !picked)  //if there is no description or it's a lock, do nothing ;]
-	{
-		if(ourOwner->allowedAssembling)
-		{
-			// If the artifact can be assembled, display dialog.
-			if(askToAssemble(ourOwner->curHero, slotID))
-			{
-				return;
-			}
-			if(askToDisassemble(ourOwner->curHero, slotID))
-			{
-				return;
-			}
-		}
-
-		// Lastly just show the artifact description.
-		LRClickableAreaWTextComp::clickRight(down, previousState);
-	}
-}
-
-void CArtifactsOfHero::activate()
-{
-	if (commonInfo->src.AOH == this && commonInfo->src.art)
-		CCS->curh->dragAndDropCursor("artifact", commonInfo->src.art->artType->getIconIndex());
-
-	CIntObject::activate();
-}
-
-void CArtifactsOfHero::deactivate()
-{
-	if (commonInfo->src.AOH == this && commonInfo->src.art)
-		CCS->curh->dragAndDropCursor(nullptr);
-
-	CIntObject::deactivate();
-}
-
-/**
- * Selects artifact slot so that the containing artifact looks like it's picked up.
- */
-void CHeroArtPlace::select()
-{
-	if(locked)
-		return;
-
-	pickSlot(true);
-	if(ourArt->canBeDisassembled() && ArtifactUtils::isSlotEquipment(slotID)) //worn combined artifact -> locks have to disappear
-	{
-		for(auto slot : ArtifactUtils::constituentWornSlots())
-		{
-			auto ap = ourOwner->getArtPlace(slot);
-			if(ap)//getArtPlace may return null
-				ap->pickSlot(ourArt->isPart(ap->ourArt));
-		}
-	}
-
-	ourOwner->commonInfo->src.setTo(this, false);
-	ourOwner->commonInfo->src.slotID = ArtifactPosition::TRANSITION_POS;
-
-	LOCPLINT->cb->swapArtifacts(ArtifactLocation(ourOwner->curHero, slotID),
-		ArtifactLocation(ourOwner->curHero, ArtifactPosition::TRANSITION_POS));
-}
-
-void CHeroArtPlace::showAll(SDL_Surface * to)
-{
-	if (ourArt && !picked && ourArt == ourOwner->curHero->getArt(slotID, false)) //last condition is needed for disassembling -> artifact may be gone, but we don't know yet TODO: real, nice solution
-	{
-		CIntObject::showAll(to);
-	}
-
-	if(marked && active)
-	{
-		// Draw vertical bars.
-		for (int i = 0; i < pos.h; ++i)
-		{
-			CSDL_Ext::putPixelWithoutRefresh(to, pos.x,             pos.y + i, 240, 220, 120);
-			CSDL_Ext::putPixelWithoutRefresh(to, pos.x + pos.w - 1, pos.y + i, 240, 220, 120);
-		}
-
-		// Draw horizontal bars.
-		for (int i = 0; i < pos.w; ++i)
-		{
-			CSDL_Ext::putPixelWithoutRefresh(to, pos.x + i, pos.y,             240, 220, 120);
-			CSDL_Ext::putPixelWithoutRefresh(to, pos.x + i, pos.y + pos.h - 1, 240, 220, 120);
-		}
-	}
-}
-
-bool CHeroArtPlace::fitsHere(const CArtifactInstance * art) const
-{
-	// You can place 'no artifact' anywhere.
-	if(!art)
-		return true;
-
-	// Anything but War Machines can be placed in backpack.
-	if (slotID >= GameConstants::BACKPACK_START)
-		return !art->artType->isBig();
-
-	return art->canBePutAt(ArtifactLocation(ourOwner->curHero, slotID), true);
-}
-
-void CHeroArtPlace::setMeAsDest(bool backpackAsVoid)
-{
-	ourOwner->commonInfo->dst.setTo(this, backpackAsVoid);
-}
-
-void CHeroArtPlace::setArtifact(const CArtifactInstance *art)
-{
-	baseType = -1; //by default we don't store any component
-	ourArt = art;
-	if(!art)
+	baseType = -1; // By default we don't store any component
+	ourArt = artInst;
+	if(!artInst)
 	{
 		image->disable();
 		text.clear();
 		hoverText = CGI->generaltexth->allTexts[507];
 		return;
 	}
-
 	image->enable();
-	image->setFrame(locked ? ArtifactID::ART_LOCK : art->artType->getIconIndex());
-
-	text = art->getDescription();
-
-	// Display info about set
-	if(ourOwner && ourOwner->curHero && !art->canBeDisassembled())
+	image->setFrame(artInst->artType->getIconIndex());
+	if(artInst->getTypeId() == ArtifactID::SPELL_SCROLL)
 	{
-		for(const auto combinedArt : art->artType->constituentOf)
+		auto spellID = artInst->getScrollSpellID();
+		if(spellID.num >= 0)
 		{
-			std::string artList;
-			text += "\n\n";
-			text += "{" + combinedArt->getNameTranslated() + "}";
-			int wornArtifacts = 0;
-			for(const auto part : *combinedArt->constituents)
-			{
-				if(art->artType->constituentOf.size() <= 1)
-					artList += "\n" + part->getNameTranslated();
-				if(ourOwner->curHero->hasArt(part->getId(), true))
-					wornArtifacts++;
-			}
-			text += " (" + boost::str(boost::format("%d") % wornArtifacts) + " / " +
-				boost::str(boost::format("%d") % combinedArt->constituents->size()) + ")" + artList;
-		}
-	}
-
-	if(art->artType->getId() == ArtifactID::SPELL_SCROLL)
-	{
-		int spellID = art->getScrollSpellID();
-		if(spellID >= 0)
-		{
-			//add spell component info (used to provide a pic in r-click popup)
+			// Add spell component info (used to provide a pic in r-click popup)
 			baseType = CComponent::spell;
 			type = spellID;
 			bonusValue = 0;
@@ -396,687 +53,275 @@ void CHeroArtPlace::setArtifact(const CArtifactInstance *art)
 	else
 	{
 		baseType = CComponent::artifact;
-		type = art->artType->getId();
+		type = artInst->getTypeId();
 		bonusValue = 0;
 	}
-
-	if (locked) // Locks should appear as empty.
-		hoverText = CGI->generaltexth->allTexts[507];
-	else
-		hoverText = boost::str(boost::format(CGI->generaltexth->heroscrn[1]) % ourArt->artType->getNameTranslated());
+	text = artInst->getDescription();
 }
 
-void CArtifactsOfHero::SCommonPart::reset()
+CArtPlace::CArtPlace(Point position, const CArtifactInstance * Art) 
+	: ourArt(Art)
 {
-	src.clear();
-	dst.clear();
-	CCS->curh->dragAndDropCursor(nullptr);
-}
-
-void CArtifactsOfHero::setHero(const CGHeroInstance * hero)
-{
-	curHero = hero;
-	if (curHero->artifactsInBackpack.size() > 0)
-		backpackPos %= curHero->artifactsInBackpack.size();
-	else
-		backpackPos = 0;
-
-	// Fill the slots for worn artifacts and backpack.
-
-	for(auto p : artWorn)
-	{
-		setSlotData(p.second, p.first);
-	}
-
-	scrollBackpack(0);
+	image = nullptr;
+	pos += position;
+	pos.w = pos.h = 44;
 }
 
-void CArtifactsOfHero::dispose()
+void CArtPlace::clickLeft(tribool down, bool previousState)
 {
-	CCS->curh->dragAndDropCursor(nullptr);
+	LRClickableAreaWTextComp::clickLeft(down, previousState);
 }
 
-void CArtifactsOfHero::scrollBackpack(int dir)
+void CArtPlace::clickRight(tribool down, bool previousState)
 {
-	int artsInBackpack = static_cast<int>(curHero->artifactsInBackpack.size());
-	backpackPos += dir;
-	if(backpackPos < 0)// No guarantee of modulus behavior with negative operands -> we keep it positive
-		backpackPos += artsInBackpack;
-
-	if(artsInBackpack)
-		backpackPos %= artsInBackpack;
-
-	std::multiset<const CArtifactInstance *> toOmit = artifactsOnAltar;
-	if(commonInfo->src.art) //if we picked an art from backapck, its slot has to be omitted
-		toOmit.insert(commonInfo->src.art);
-
-	int omitedSoFar = 0;
-
-	//set new data
-	size_t s = 0;
-	for( ; s < artsInBackpack; ++s)
-	{
-
-		if (s < artsInBackpack)
-		{
-			auto slotID = ArtifactPosition(GameConstants::BACKPACK_START + (s + backpackPos)%artsInBackpack);
-			const CArtifactInstance *art = curHero->getArt(slotID);
-			assert(art);
-			if(!vstd::contains(toOmit, art))
-			{
-				if(s - omitedSoFar < backpack.size())
-					setSlotData(backpack[s-omitedSoFar], slotID);
-			}
-			else
-			{
-				toOmit -= art;
-				omitedSoFar++;
-				continue;
-			}
-		}
-	}
-	for( ; s - omitedSoFar < backpack.size(); s++)
-		eraseSlotData(backpack[s-omitedSoFar], ArtifactPosition(GameConstants::BACKPACK_START + (si32)s));
-
-	//in artifact merchant selling artifacts we may have highlight on one of backpack artifacts -> market needs update, cause artifact under highlight changed
-	if(highlightModeCallback)
-	{
-		for(auto & elem : backpack)
-		{
-			if(elem->marked)
-			{
-				highlightModeCallback(elem.get());
-				break;
-			}
-		}
-	}
-
-	//blocking scrolling if there is not enough artifacts to scroll
-	bool scrollingPossible = artsInBackpack - omitedSoFar > backpack.size();
-	leftArtRoll->block(!scrollingPossible);
-	rightArtRoll->block(!scrollingPossible);
-
-	safeRedraw();
+	LRClickableAreaWTextComp::clickRight(down, previousState);
 }
 
-/**
- * Marks possible slots where a given artifact can be placed, except backpack.
- *
- * @param art Artifact checked against.
- */
-void CArtifactsOfHero::markPossibleSlots(const CArtifactInstance* art, bool withRedraw)
+const CArtifactInstance * CArtPlace::getArt()
 {
-	for(CArtifactsOfHero *aoh : commonInfo->participants)
-		for(auto p : aoh->artWorn)
-			p.second->selectSlot(art->canBePutAt(ArtifactLocation(aoh->curHero, p.second->slotID), true));
-
-	if(withRedraw)
-		safeRedraw();
+	return ourArt;
 }
 
-/**
- * Unamarks all slots.
- */
-void CArtifactsOfHero::unmarkSlots(bool withRedraw)
+CCommanderArtPlace::CCommanderArtPlace(Point position, const CGHeroInstance * commanderOwner, ArtifactPosition artSlot, const CArtifactInstance * Art)
+	: CArtPlace(position, Art),
+	commanderOwner(commanderOwner),
+	commanderSlotID(artSlot.num)
 {
-	if(commonInfo)
-		for(CArtifactsOfHero *aoh : commonInfo->participants)
-			aoh->unmarkLocalSlots(false);
-	else
-		unmarkLocalSlots(false);
-
-	if(withRedraw)
-		safeRedraw();
+	createImage();
+	setArtifact(Art);
 }
 
-void CArtifactsOfHero::unmarkLocalSlots(bool withRedraw)
+void CCommanderArtPlace::createImage()
 {
-	for(auto & p : artWorn)
-		p.second->selectSlot(false);
+	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
 
-	for(auto & place : backpack)
-		place->selectSlot(false);
+	int imageIndex = 0;
+	if(ourArt)
+		imageIndex = ourArt->artType->getIconIndex();
 
-	if(withRedraw)
-		safeRedraw();
+	image = std::make_shared<CAnimImage>("artifact", imageIndex);
+	if(!ourArt)
+		image->disable();
 }
 
-/**
- * Assigns an artifacts to an artifact place depending on it's new slot ID.
- */
-void CArtifactsOfHero::setSlotData(ArtPlacePtr artPlace, ArtifactPosition slotID)
+void CCommanderArtPlace::returnArtToHeroCallback()
 {
-	if(!artPlace && slotID >= GameConstants::BACKPACK_START) //spurious call from artifactMoved in attempt to update hidden backpack slot
-	{
-		return;
-	}
-
-	artPlace->pickSlot(false);
-	artPlace->slotID = slotID;
-
-	if(const ArtSlotInfo *asi = curHero->getSlot(slotID))
+	ArtifactPosition artifactPos = commanderSlotID;
+	ArtifactPosition freeSlot = ArtifactUtils::getArtBackpackPosition(commanderOwner, ourArt->getTypeId());
+	if(freeSlot == ArtifactPosition::PRE_FIRST)
 	{
-		artPlace->lockSlot(asi->locked);
-		artPlace->setArtifact(asi->artifact);
+		LOCPLINT->showInfoDialog(CGI->generaltexth->translate("core.genrltxt.152"));
 	}
 	else
-		artPlace->setArtifact(nullptr);
-}
-
-/**
- * Makes given artifact slot appear as empty with a certain slot ID.
- */
-void CArtifactsOfHero::eraseSlotData(ArtPlacePtr artPlace, ArtifactPosition slotID)
-{
-	artPlace->pickSlot(false);
-	artPlace->slotID = slotID;
-	artPlace->setArtifact(nullptr);
-}
-
-CArtifactsOfHero::CArtifactsOfHero(ArtPlaceMap ArtWorn, std::vector<ArtPlacePtr> Backpack,
-		std::shared_ptr<CButton> leftScroll, std::shared_ptr<CButton> rightScroll, bool createCommonPart)
-	: curHero(nullptr),
-	artWorn(ArtWorn),
-	backpack(Backpack),
-	backpackPos(0),
-	commonInfo(nullptr),
-	leftArtRoll(leftScroll),
-	rightArtRoll(rightScroll),
-	allowedAssembling(true),
-	highlightModeCallback(nullptr)
-{
-	if(createCommonPart)
-	{
-		commonInfo = std::make_shared<CArtifactsOfHero::SCommonPart>();
-		commonInfo->participants.insert(this);
-	}
-
-	// Init slots for worn artifacts.
-	for(auto p : artWorn)
-	{
-		p.second->ourOwner = this;
-		eraseSlotData(p.second, p.first);
-	}
-
-	// Init slots for the backpack.
-	for(size_t s=0; s<backpack.size(); ++s)
-	{
-		backpack[s]->ourOwner = this;
-		eraseSlotData(backpack[s], ArtifactPosition(GameConstants::BACKPACK_START + (si32)s));
-	}
-
-	leftArtRoll->addCallback(std::bind(&CArtifactsOfHero::scrollBackpack, this,-1));
-	rightArtRoll->addCallback(std::bind(&CArtifactsOfHero::scrollBackpack, this,+1));
-}
-
-CArtifactsOfHero::CArtifactsOfHero(const Point & position, bool createCommonPart)
-	: curHero(nullptr),
-	backpackPos(0),
-	commonInfo(nullptr),
-	allowedAssembling(true),
-	highlightModeCallback(nullptr)
-{
-	if(createCommonPart)
-	{
-		commonInfo = std::make_shared<CArtifactsOfHero::SCommonPart>();
-		commonInfo->participants.insert(this);
-	}
-
-	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
-	pos += position;
-
-	std::vector<Point> slotPos =
-	{
-		Point(509,30),  Point(567,240), Point(509,80),  //0-2
-		Point(383,68),  Point(564,183), Point(509,130), //3-5
-		Point(431,68),  Point(610,183), Point(515,295), //6-8
-		Point(383,143), Point(399,194), Point(415,245), //9-11
-		Point(431,296), Point(564,30),  Point(610,30), //12-14
-		Point(610,76),  Point(610,122), Point(610,310), //15-17
-		Point(381,296) //18
-	};
-
-	// Create slots for worn artifacts.
-	for(si32 g = 0; g < GameConstants::BACKPACK_START; g++)
 	{
-		artWorn[ArtifactPosition(g)] = std::make_shared<CHeroArtPlace>(slotPos[g]);
-		artWorn[ArtifactPosition(g)]->ourOwner = this;
-		eraseSlotData(artWorn[ArtifactPosition(g)], ArtifactPosition(g));
-	}
-
-	// Create slots for the backpack.
-	for(int s=0; s<5; ++s)
-	{
-		auto add = std::make_shared<CHeroArtPlace>(Point(403 + 46 * s, 365));
-
-		add->ourOwner = this;
-		eraseSlotData(add, ArtifactPosition(GameConstants::BACKPACK_START + s));
-
-		backpack.push_back(add);
-	}
-
-	leftArtRoll = std::make_shared<CButton>(Point(379, 364), "hsbtns3.def", CButton::tooltip(), [&](){ scrollBackpack(-1);}, SDLK_LEFT);
-	rightArtRoll = std::make_shared<CButton>(Point(632, 364), "hsbtns5.def", CButton::tooltip(), [&](){ scrollBackpack(+1);}, SDLK_RIGHT);
-}
+		ArtifactLocation src(commanderOwner->commander.get(), artifactPos);
+		ArtifactLocation dst(commanderOwner, freeSlot);
 
-CArtifactsOfHero::~CArtifactsOfHero()
-{
-	dispose();
-	// Artifact located in artifactsTransitionPos should be returned
-	if(!curHero->artifactsTransitionPos.empty())
-	{
-		auto artPlace = getArtPlace(
-			ArtifactUtils::getArtAnyPosition(curHero, curHero->artifactsTransitionPos.begin()->artifact->getTypeId()));
-		if(artPlace)
-		{
-			assert(artPlace->ourOwner);
-			artPlace->setMeAsDest();
-			artPlace->ourOwner->realizeCurrentTransaction();
-		}
-		else
+		if(ourArt->canBePutAt(dst, true))
 		{
-			LOCPLINT->cb->eraseArtifactByClient(ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS));
+			LOCPLINT->cb->swapArtifacts(src, dst);
+			setArtifact(nullptr);
+			parent->redraw();
 		}
 	}
 }
 
-void CArtifactsOfHero::updateParentWindow()
+void CCommanderArtPlace::clickLeft(tribool down, bool previousState)
 {
-	if(CHeroWindow * chw = dynamic_cast<CHeroWindow*>(GH.topInt().get()))
-	{
-		chw->update(curHero, true);
-	}
-	else if(CExchangeWindow * cew = dynamic_cast<CExchangeWindow*>(GH.topInt().get()))
-	{
-		cew->updateWidgets();
-	}
+	if(ourArt && text.size() && down)
+		LOCPLINT->showYesNoDialog(CGI->generaltexth->translate("vcmi.commanderWindow.artifactMessage"), [this]() { returnArtToHeroCallback(); }, []() {});
 }
 
-void CArtifactsOfHero::safeRedraw()
+void CCommanderArtPlace::clickRight(tribool down, bool previousState)
 {
-	if (active)
-	{
-		if(parent)
-			parent->redraw();
-		else
-			redraw();
-	}
+	if(ourArt && text.size() && down)
+		CArtPlace::clickRight(down, previousState);
 }
 
-void CArtifactsOfHero::realizeCurrentTransaction()
+void CCommanderArtPlace::setArtifact(const CArtifactInstance * art)
 {
-	assert(commonInfo->src.AOH);
-	assert(commonInfo->dst.AOH);
-	LOCPLINT->cb->swapArtifacts(ArtifactLocation(commonInfo->src.AOH->curHero, commonInfo->src.slotID),
-								ArtifactLocation(commonInfo->dst.AOH->curHero, commonInfo->dst.slotID));
+	setInternals(art);
 }
 
-void CArtifactsOfHero::artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst, bool withUIUpdate)
+CHeroArtPlace::CHeroArtPlace(Point position, const CArtifactInstance * Art)
+	: CArtPlace(position, Art),
+	locked(false),
+	marked(false)
 {
-	bool isCurHeroSrc = src.isHolder(curHero),
-		isCurHeroDst = dst.isHolder(curHero);
-	if(isCurHeroSrc && ArtifactUtils::isSlotBackpack(src.slot))
-		updateSlot(src.slot);
-	if(isCurHeroDst && ArtifactUtils::isSlotBackpack(dst.slot))
-		updateSlot(dst.slot);
-	// We need to update all slots, artifact might be combined and affect more slots
-	if(isCurHeroSrc || isCurHeroDst)
-		updateWornSlots(false);
-
-	if(!isCurHeroSrc && !isCurHeroDst)
-		return;
-
-	// When moving one artifact onto another it leads to two art movements: dst->TRANSITION_POS; src->dst
-	// however after first movement we pick the art from TRANSITION_POS and the second movement coming when
-	// we have a different artifact may look surprising... but it's valid.
-
-	// Used when doing dragAndDrop and artifact swap multiple times
-	if(src.slot == ArtifactPosition::TRANSITION_POS && 
-		commonInfo->src.slotID == ArtifactPosition::TRANSITION_POS &&
-		commonInfo->dst.slotID == ArtifactPosition::PRE_FIRST && 
-		isCurHeroDst)
-	{
-		auto art = curHero->getArt(ArtifactPosition::TRANSITION_POS);
-		assert(art);
-		CCS->curh->dragAndDropCursor("artifact", art->artType->getIconIndex());
-		if(withUIUpdate)
-			markPossibleSlots(art);
-
-		commonInfo->src.art = art;
-		commonInfo->src.slotID = src.slot;
-	}
-	// Artifact was taken from us
-	else if(commonInfo->src == src && dst.slot != ArtifactPosition::TRANSITION_POS)
-	{
-		// Expected movement from slot ot slot
-		assert(commonInfo->dst == dst
-			// Artifact moved back to backpack (eg. to make place for art we are moving)
-			|| dst.slot == dst.getHolderArtSet()->artifactsInBackpack.size() + GameConstants::BACKPACK_START
-			|| dst.getHolderArtSet()->bearerType() != ArtBearer::HERO);
-		commonInfo->reset();
-		unmarkSlots();
-	}
-	else
-	{
-		// The dest artifact was moved after the swap -> we are picking it
-		if(commonInfo->dst == src)
-		{
-			assert(dst.slot == ArtifactPosition::TRANSITION_POS);
-			commonInfo->reset();
-
-			for(CArtifactsOfHero * aoh : commonInfo->participants)
-			{
-				if(dst.isHolder(aoh->curHero))
-				{
-					commonInfo->src.AOH = aoh;
-					break;
-				}
-			}
-
-			commonInfo->src.art = dst.getArt();
-			commonInfo->src.slotID = dst.slot;
-			assert(commonInfo->src.AOH);
-			CCS->curh->dragAndDropCursor("artifact", dst.getArt()->artType->getIconIndex());
-		}
-		if(!curHero->artifactsTransitionPos.empty() && withUIUpdate)
-		{
-			auto artInst = curHero->getArt(ArtifactPosition::TRANSITION_POS);
-			markPossibleSlots(artInst);
-			CCS->curh->dragAndDropCursor("artifact", artInst->artType->getIconIndex());
-		}
-	}
-
-	if(withUIUpdate)
-	{
-		updateParentWindow();
-		scrollBackpack(0);
-	}
+	createImage();
 }
 
-void CArtifactsOfHero::artifactRemoved(const ArtifactLocation &al)
+void CHeroArtPlace::lockSlot(bool on)
 {
-	if(al.isHolder(curHero))
-	{
-		if(al.slot < GameConstants::BACKPACK_START)
-			updateWornSlots(0);
-		else
-			scrollBackpack(0); //update backpack slots
-	}
-}
+	if(locked == on)
+		return;
 
-CArtifactsOfHero::ArtPlacePtr CArtifactsOfHero::getArtPlace(ArtifactPosition slot)
-{
-	if(ArtifactUtils::isSlotEquipment(slot))
-	{
-		if(artWorn.find(slot) == artWorn.end())
-		{
-			logGlobal->error("CArtifactsOfHero::getArtPlace: invalid slot %d", slot);
-			return nullptr;
-		}
+	locked = on;
 
-		return artWorn[slot];
-	}
-	if(ArtifactUtils::isSlotBackpack(slot))
-	{
-		for(ArtPlacePtr ap : backpack)
-			if(ap->slotID == slot)
-				return ap;
-		return nullptr;
-	}
+	if(on)
+		image->setFrame(ArtifactID::ART_LOCK);
+	else if(ourArt)
+		image->setFrame(ourArt->artType->getIconIndex());
 	else
-	{
-		return nullptr;
-	}
-}
-
-void CArtifactsOfHero::artifactUpdateSlots(const ArtifactLocation & al)
-{
-	if(al.isHolder(curHero))
-	{
-		if(ArtifactUtils::isSlotBackpack(al.slot))
-			updateBackpackSlots(true);
-		else
-			updateWornSlots(true);
-	}
+		image->setFrame(0);
 }
 
-void CArtifactsOfHero::updateWornSlots(bool redrawParent)
+bool CHeroArtPlace::isLocked()
 {
-	for(auto p : artWorn)
-		updateSlot(p.first);
-
-	if(redrawParent)
-		updateParentWindow();
+	return locked;
 }
 
-void CArtifactsOfHero::updateBackpackSlots(bool redrawParent)
+void CHeroArtPlace::selectSlot(bool on)
 {
-	for(auto artPlace : backpack)
-		updateSlot(artPlace->slotID);
-	scrollBackpack(0);
+	if(marked == on)
+		return;
 
-	if(redrawParent)
-		updateParentWindow();
+	marked = on;
+	if(on)
+		selection->enable();
+	else
+		selection->disable();
 }
 
-const CGHeroInstance * CArtifactsOfHero::getHero() const
+bool CHeroArtPlace::isMarked() const
 {
-	return curHero;
+	return marked;
 }
 
-void CArtifactsOfHero::updateSlot(ArtifactPosition slotID)
+void CHeroArtPlace::clickLeft(tribool down, bool previousState)
 {
-	setSlotData(getArtPlace(slotID), slotID);
-}
+	if(down || !previousState)
+		return;
 
-CArtifactHolder::CArtifactHolder()
-{
+	if(leftClickCallback)
+		leftClickCallback(*this);
 }
 
-void CWindowWithArtifacts::addSet(std::shared_ptr<CArtifactsOfHero> artSet)
-{
-	artSets.emplace_back(artSet);
-}
-
-std::shared_ptr<CArtifactsOfHero::SCommonPart> CWindowWithArtifacts::getCommonPart()
+void CHeroArtPlace::clickRight(tribool down, bool previousState)
 {
-	for(auto artSetWeak : artSets)
+	if(down)
 	{
-		std::shared_ptr<CArtifactsOfHero> realPtr = artSetWeak.lock();
-		if(realPtr)
-			return realPtr->commonInfo;
+		if(rightClickCallback)
+			rightClickCallback(*this);
 	}
-
-	return std::shared_ptr<CArtifactsOfHero::SCommonPart>();
 }
 
-void CWindowWithArtifacts::artifactRemoved(const ArtifactLocation &artLoc)
+void CHeroArtPlace::showAll(SDL_Surface* to)
 {
-	for(auto artSetWeak : artSets)
+	if(ourArt)
 	{
-		std::shared_ptr<CArtifactsOfHero> realPtr = artSetWeak.lock();
-		if(realPtr)
-			realPtr->artifactRemoved(artLoc);
+		CIntObject::showAll(to);
 	}
-}
-
-void CWindowWithArtifacts::artifactMoved(const ArtifactLocation &artLoc, const ArtifactLocation &destLoc, bool withRedraw)
-{
-	CArtifactsOfHero * destaoh = nullptr;
 
-	for(auto artSetWeak : artSets)
+	if(marked && active)
 	{
-		std::shared_ptr<CArtifactsOfHero> realPtr = artSetWeak.lock();
-		if(realPtr)
+		// Draw vertical bars.
+		for(int i = 0; i < pos.h; ++i)
 		{
-			realPtr->artifactMoved(artLoc, destLoc, withRedraw);
-			if(destLoc.isHolder(realPtr->getHero()))
-				destaoh = realPtr.get();
+			CSDL_Ext::putPixelWithoutRefresh(to, pos.x, pos.y + i, 240, 220, 120);
+			CSDL_Ext::putPixelWithoutRefresh(to, pos.x + pos.w - 1, pos.y + i, 240, 220, 120);
 		}
-	}
 
-	//Make sure the status bar is updated so it does not display old text
-	if(destaoh != nullptr && destaoh->getArtPlace(destLoc.slot) != nullptr)
-	{
-		destaoh->getArtPlace(destLoc.slot)->hover(true);
+		// Draw horizontal bars.
+		for(int i = 0; i < pos.w; ++i)
+		{
+			CSDL_Ext::putPixelWithoutRefresh(to, pos.x + i, pos.y, 240, 220, 120);
+			CSDL_Ext::putPixelWithoutRefresh(to, pos.x + i, pos.y + pos.h - 1, 240, 220, 120);
+		}
 	}
 }
 
-void CWindowWithArtifacts::artifactDisassembled(const ArtifactLocation &artLoc)
+void CHeroArtPlace::setArtifact(const CArtifactInstance * art)
 {
-	for(auto artSetWeak : artSets)
+	setInternals(art);
+	if(art)
 	{
-		std::shared_ptr<CArtifactsOfHero> realPtr = artSetWeak.lock();
-		if(realPtr)
-			realPtr->artifactUpdateSlots(artLoc);
-	}
-}
+		image->setFrame(locked ? ArtifactID::ART_LOCK : art->artType->getIconIndex());
 
-void CWindowWithArtifacts::artifactAssembled(const ArtifactLocation &artLoc)
-{
-	for(auto artSetWeak : artSets)
-	{
-		std::shared_ptr<CArtifactsOfHero> realPtr = artSetWeak.lock();
-		if(realPtr)
-			realPtr->artifactUpdateSlots(artLoc);
+		if(locked) // Locks should appear as empty.
+			hoverText = CGI->generaltexth->allTexts[507];
+		else
+			hoverText = boost::str(boost::format(CGI->generaltexth->heroscrn[1]) % ourArt->artType->getNameTranslated());
 	}
-}
-
-void CArtifactsOfHero::SCommonPart::Artpos::clear()
-{
-	slotID = ArtifactPosition::PRE_FIRST;
-	AOH = nullptr;
-	art = nullptr;
-}
-
-void CArtifactsOfHero::SCommonPart::Artpos::setTo(const CHeroArtPlace *place, bool dontTakeBackpack)
-{
-	slotID = place->slotID;
-	AOH = place->ourOwner;
-
-	if(ArtifactUtils::isSlotBackpack(slotID) && dontTakeBackpack)
-		art = nullptr;
 	else
-		art = place->ourArt;
-}
-
-bool CArtifactsOfHero::SCommonPart::Artpos::operator==(const ArtifactLocation &al) const
-{
-	if(!AOH)
-		return false;
-	bool ret = al.isHolder(AOH->curHero)  &&  al.slot == slotID;
-
-	//assert(al.getArt() == art);
-	return ret;
-}
-
-bool CArtifactsOfHero::SCommonPart::Artpos::valid()
-{
-	assert(AOH && art);
-	return art == AOH->curHero->getArt(slotID);
-}
-
-CArtPlace::CArtPlace(Point position, const CArtifactInstance * Art) : ourArt(Art)
-{
-	image = nullptr;
-	pos += position;
-	pos.w = pos.h = 44;
-}
-
-void CArtPlace::clickLeft(tribool down, bool previousState)
-{
-	LRClickableAreaWTextComp::clickLeft(down, previousState);
-}
-
-void CArtPlace::clickRight(tribool down, bool previousState)
-{
-	LRClickableAreaWTextComp::clickRight(down, previousState);
-}
-
-CCommanderArtPlace::CCommanderArtPlace(Point position, const CGHeroInstance * commanderOwner, ArtifactPosition artSlot, const CArtifactInstance * Art) : CArtPlace(position, Art), commanderOwner(commanderOwner), commanderSlotID(artSlot.num)
-{
-	createImage();
-	setArtifact(Art);
+	{
+		lockSlot(false);
+	}
 }
 
-void CCommanderArtPlace::clickLeft(tribool down, bool previousState)
+void CHeroArtPlace::addCombinedArtInfo(std::map<const CArtifact*, int> & arts)
 {
-	if (ourArt && text.size() && down)
-		LOCPLINT->showYesNoDialog(CGI->generaltexth->translate("vcmi.commanderWindow.artifactMessage"), [this](){ returnArtToHeroCallback(); }, [](){});
-}
-
-void CCommanderArtPlace::clickRight(tribool down, bool previousState)
-{
-	if (ourArt && text.size() && down)
-		CArtPlace::clickRight(down, previousState);
+	for(const auto & combinedArt : arts)
+	{
+		std::string artList;
+		text += "\n\n";
+		text += "{" + combinedArt.first->getNameTranslated() + "}";
+		if(arts.size() == 1)
+		{
+			for(const auto part : *combinedArt.first->constituents)
+				artList += "\n" + part->getNameTranslated();
+		}
+		text += " (" + boost::str(boost::format("%d") % combinedArt.second) + " / " +
+			boost::str(boost::format("%d") % combinedArt.first->constituents->size()) + ")" + artList;
+	}
 }
 
-void CCommanderArtPlace::createImage()
+void CHeroArtPlace::createImage()
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 
-	int imageIndex = 0;
-	if(ourArt)
+	si32 imageIndex = 0;
+
+	if(locked)
+		imageIndex = ArtifactID::ART_LOCK;
+	else if(ourArt)
 		imageIndex = ourArt->artType->getIconIndex();
 
 	image = std::make_shared<CAnimImage>("artifact", imageIndex);
 	if(!ourArt)
 		image->disable();
+
+	selection = std::make_shared<CAnimImage>("artifact", ArtifactID::ART_SELECTION);
+	selection->disable();
 }
 
-void CCommanderArtPlace::returnArtToHeroCallback()
+bool ArtifactUtilsClient::askToAssemble(const CGHeroInstance * hero, const ArtifactPosition & slot)
 {
-	ArtifactPosition artifactPos = commanderSlotID;
-	ArtifactPosition freeSlot = ArtifactUtils::getArtBackpackPosition(commanderOwner, ourArt->getTypeId());
-	if(freeSlot == ArtifactPosition::PRE_FIRST)
-	{
-		LOCPLINT->showInfoDialog(CGI->generaltexth->translate("core.genrltxt.152"));
-	}
-	else
+	assert(hero);
+	const auto art = hero->getArt(slot);
+	assert(art);
+	auto assemblyPossibilities = ArtifactUtils::assemblyPossibilities(hero, art->getTypeId(), ArtifactUtils::isSlotEquipment(slot));
+
+	for(const auto combinedArt : assemblyPossibilities)
 	{
-		ArtifactLocation src(commanderOwner->commander.get(), artifactPos);
-		ArtifactLocation dst(commanderOwner, freeSlot);
+		LOCPLINT->showArtifactAssemblyDialog(
+			art->artType,
+			combinedArt,
+			std::bind(&CCallback::assembleArtifacts, LOCPLINT->cb.get(), hero, slot, true, combinedArt->getId()));
 
-		if(ourArt->canBePutAt(dst, true))
-		{
-			LOCPLINT->cb->swapArtifacts(src, dst);
-			setArtifact(nullptr);
-			parent->redraw();
-		}
+		if(assemblyPossibilities.size() > 2)
+			logGlobal->warn("More than one possibility of assembling on %s... taking only first", art->artType->getNameTranslated());
+		return true;
 	}
+	return false;
 }
 
-void CCommanderArtPlace::setArtifact(const CArtifactInstance * art)
+bool ArtifactUtilsClient::askToDisassemble(const CGHeroInstance * hero, const ArtifactPosition & slot)
 {
-	baseType = -1; //by default we don't store any component
-	ourArt = art;
-	if (!art)
-	{
-		image->disable();
-		text.clear();
-		return;
-	}
-
-	image->enable();
-	image->setFrame(art->artType->getIconIndex());
-
-	text = art->getDescription();
+	assert(hero);
+	const auto art = hero->getArt(slot);
+	assert(art);
 
-	if (art->artType->getId() == ArtifactID::SPELL_SCROLL)
-	{
-		int spellID = art->getScrollSpellID();
-		if (spellID >= 0)
-		{
-			//add spell component info (used to provide a pic in r-click popup)
-			baseType = CComponent::spell;
-			type = spellID;
-			bonusValue = 0;
-		}
-	}
-	else
+	if(art->canBeDisassembled())
 	{
-		baseType = CComponent::artifact;
-		type = art->artType->getId();
-		bonusValue = 0;
+		if(ArtifactUtils::isSlotBackpack(slot) && !ArtifactUtils::isBackpackFreeSlots(hero, art->artType->constituents->size() - 1))
+			return false;
+
+		LOCPLINT->showArtifactAssemblyDialog(
+			art->artType,
+			nullptr,
+			std::bind(&CCallback::assembleArtifacts, LOCPLINT->cb.get(), hero, slot, false, ArtifactID()));
+		return true;
 	}
+	return false;
 }

+ 30 - 123
client/widgets/CArtifactHolder.h

@@ -14,37 +14,38 @@
 VCMI_LIB_NAMESPACE_BEGIN
 
 struct ArtifactLocation;
+class CArtifactSet;
 
 VCMI_LIB_NAMESPACE_END
 
-class CArtifactsOfHero;
 class CAnimImage;
 class CButton;
 
 class CArtifactHolder
 {
 public:
-	CArtifactHolder();
-
-	virtual void artifactRemoved(const ArtifactLocation &artLoc)=0;
-	virtual void artifactMoved(const ArtifactLocation &artLoc, const ArtifactLocation &destLoc, bool withRedraw)=0;
-	virtual void artifactDisassembled(const ArtifactLocation &artLoc)=0;
-	virtual void artifactAssembled(const ArtifactLocation &artLoc)=0;
+	virtual void artifactRemoved(const ArtifactLocation & artLoc)=0;
+	virtual void artifactMoved(const ArtifactLocation & artLoc, const ArtifactLocation & destLoc, bool withRedraw)=0;
+	virtual void artifactDisassembled(const ArtifactLocation & artLoc)=0;
+	virtual void artifactAssembled(const ArtifactLocation & artLoc)=0;
 };
 
 class CArtPlace : public LRClickableAreaWTextComp
 {
 protected:
 	std::shared_ptr<CAnimImage> image;
+	const CArtifactInstance * ourArt;
+
+	void setInternals(const CArtifactInstance * artInst);
 	virtual void createImage()=0;
-public:
-	const CArtifactInstance * ourArt; // should be changed only with setArtifact()
 
+public:
 	CArtPlace(Point position, const CArtifactInstance * Art = nullptr);
 	void clickLeft(tribool down, bool previousState) override;
 	void clickRight(tribool down, bool previousState) override;
+	const CArtifactInstance * getArt();
 
-	virtual void setArtifact(const CArtifactInstance *art)=0;
+	virtual void setArtifact(const CArtifactInstance * art)=0;
 };
 
 class CCommanderArtPlace : public CArtPlace
@@ -55,138 +56,44 @@ protected:
 
 	void createImage() override;
 	void returnArtToHeroCallback();
+
 public:
 	CCommanderArtPlace(Point position, const CGHeroInstance * commanderOwner, ArtifactPosition artSlot, const CArtifactInstance * Art = nullptr);
 	void clickLeft(tribool down, bool previousState) override;
 	void clickRight(tribool down, bool previousState) override;
-
-	virtual void setArtifact(const CArtifactInstance * art) override;
-
+	void setArtifact(const CArtifactInstance * art) override;
 };
 
-/// Artifacts can be placed there. Gets shown at the hero window
 class CHeroArtPlace: public CArtPlace
 {
-	std::shared_ptr<CAnimImage> selection;
-
-	void createImage() override;
-
 public:
-	// consider these members as const - change them only with appropriate methods e.g. lockSlot()
-	bool locked;
-	bool picked;
-	bool marked;
-
-	ArtifactPosition slotID; //Arts::EPOS enum + backpack starting from Arts::BACKPACK_START
+	using ClickHandler = std::function<void(CHeroArtPlace&)>;
 
-	CArtifactsOfHero * ourOwner;
+	ArtifactPosition slot;
+	ClickHandler leftClickCallback;
+	ClickHandler rightClickCallback;
 
 	CHeroArtPlace(Point position, const CArtifactInstance * Art = nullptr);
-
 	void lockSlot(bool on);
-	void pickSlot(bool on);
+	bool isLocked();
 	void selectSlot(bool on);
-
+	bool isMarked() const;
 	void clickLeft(tribool down, bool previousState) override;
 	void clickRight(tribool down, bool previousState) override;
-	void select();
 	void showAll(SDL_Surface * to) override;
-	bool fitsHere (const CArtifactInstance * art) const; //returns true if given artifact can be placed here
+	void setArtifact(const CArtifactInstance * art) override;
+	void addCombinedArtInfo(std::map<const CArtifact*, int> & arts);
 
-	void setMeAsDest(bool backpackAsVoid = true);
-	void setArtifact(const CArtifactInstance *art) override;
-	static bool askToAssemble(const CGHeroInstance * hero, ArtifactPosition slot);
-	static bool askToDisassemble(const CGHeroInstance * hero, ArtifactPosition slot);
-};
+protected:
+	std::shared_ptr<CAnimImage> selection;
+	bool locked;
+	bool marked;
 
-/// Contains artifacts of hero. Distincts which artifacts are worn or backpacked
-class CArtifactsOfHero : public CIntObject
-{
-public:
-	using ArtPlacePtr = std::shared_ptr<CHeroArtPlace>;
-	using ArtPlaceMap = std::map<ArtifactPosition, ArtPlacePtr>;
-
-	struct SCommonPart
-	{
-		struct Artpos
-		{
-			ArtifactPosition slotID;
-			const CArtifactsOfHero *AOH;
-			const CArtifactInstance *art;
-
-			void clear();
-			void setTo(const CHeroArtPlace *place, bool dontTakeBackpack);
-			bool valid();
-			bool operator==(const ArtifactLocation &al) const;
-		} src, dst;
-
-		std::set<CArtifactsOfHero *> participants; // Needed to mark slots.
-
-		void reset();
-	};
-	std::shared_ptr<SCommonPart> commonInfo; //when we have more than one CArtifactsOfHero in one window with exchange possibility, we use this (eg. in exchange window); to be provided externally
-
-	std::shared_ptr<CButton> leftArtRoll;
-	std::shared_ptr<CButton> rightArtRoll;
-	bool allowedAssembling;
-
-	std::multiset<const CArtifactInstance*> artifactsOnAltar; //artifacts id that are technically present in backpack but in GUI are moved to the altar - they'll be omitted in backpack slots
-	std::function<void(CHeroArtPlace*)> highlightModeCallback; //if set, clicking on art place doesn't pick artifact but highlights the slot and calls this function
-
-	void realizeCurrentTransaction(); //calls callback with parameters stored in commonInfo
-	void artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst, bool withUIUpdate);
-	void artifactRemoved(const ArtifactLocation &al);
-	void artifactUpdateSlots(const ArtifactLocation &al);
-	ArtPlacePtr getArtPlace(ArtifactPosition slot);//may return null
-
-	void setHero(const CGHeroInstance * hero);
-	const CGHeroInstance *getHero() const;
-	void dispose(); //free resources not needed after closing windows and reset state
-	void scrollBackpack(int dir); //dir==-1 => to left; dir==1 => to right
-
-	void activate() override;
-	void deactivate() override;
-
-	void safeRedraw();
-	void markPossibleSlots(const CArtifactInstance * art, bool withRedraw = false);
-	void unmarkSlots(bool withRedraw = false); //unmarks slots in all visible AOHs
-	void unmarkLocalSlots(bool withRedraw = false); //unmarks slots in that particular AOH
-	void updateWornSlots(bool redrawParent = false);
-	void updateBackpackSlots(bool redrawParent = false);
-
-	void updateSlot(ArtifactPosition i);
-
-	CArtifactsOfHero(const Point& position, bool createCommonPart = false);
-	//Alternative constructor, used if custom artifacts positioning required (Kingdom interface)
-	CArtifactsOfHero(ArtPlaceMap ArtWorn, std::vector<ArtPlacePtr> Backpack,
-		std::shared_ptr<CButton> leftScroll, std::shared_ptr<CButton> rightScroll, bool createCommonPart = false);
-	~CArtifactsOfHero();
-	void updateParentWindow();
-	friend class CHeroArtPlace;
-
-private:
-
-	const CGHeroInstance * curHero;
-
-	ArtPlaceMap artWorn;
-
-	std::vector<ArtPlacePtr> backpack; //hero's visible backpack (only 5 elements!)
-	int backpackPos; //number of first art visible in backpack (in hero's vector)
-
-	void eraseSlotData(ArtPlacePtr artPlace, ArtifactPosition slotID);
-	void setSlotData(ArtPlacePtr artPlace, ArtifactPosition slotID);
+	void createImage() override;
 };
 
-class CWindowWithArtifacts : public CArtifactHolder
+namespace ArtifactUtilsClient
 {
-	std::vector<std::weak_ptr<CArtifactsOfHero>> artSets;
-public:
-	void addSet(std::shared_ptr<CArtifactsOfHero> artSet);
-
-	std::shared_ptr<CArtifactsOfHero::SCommonPart> getCommonPart();
-
-	void artifactRemoved(const ArtifactLocation &artLoc) override;
-	void artifactMoved(const ArtifactLocation &artLoc, const ArtifactLocation &destLoc, bool withRedraw) override;
-	void artifactDisassembled(const ArtifactLocation &artLoc) override;
-	void artifactAssembled(const ArtifactLocation &artLoc) override;
-};
+	bool askToAssemble(const CGHeroInstance * hero, const ArtifactPosition & slot);
+	bool askToDisassemble(const CGHeroInstance * hero, const ArtifactPosition & slot);
+}

+ 109 - 0
client/widgets/CArtifactsOfHeroAltar.cpp

@@ -0,0 +1,109 @@
+/*
+ * CArtifactsOfHeroAltar.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 "CArtifactsOfHeroAltar.h"
+
+#include "../CPlayerInterface.h"
+
+#include "../../CCallback.h"
+
+#include "../../lib/mapObjects/CGHeroInstance.h"
+
+CArtifactsOfHeroAltar::CArtifactsOfHeroAltar(const Point & position)
+	: visibleArtSet(ArtBearer::ArtBearer::HERO)
+{
+	init(
+		std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1), 
+		std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1), 
+		position,
+		std::bind(&CArtifactsOfHeroAltar::scrollBackpack, this, _1));
+	pickedArtFromSlot = ArtifactPosition::PRE_FIRST;
+};
+
+void CArtifactsOfHeroAltar::setHero(const CGHeroInstance * hero)
+{
+	if(hero)
+	{
+		visibleArtSet.artifactsWorn = hero->artifactsWorn;
+		visibleArtSet.artifactsInBackpack = hero->artifactsInBackpack;
+		CArtifactsOfHeroBase::setHero(hero);
+	}
+}
+
+void CArtifactsOfHeroAltar::updateWornSlots()
+{
+	for(auto place : artWorn)
+		setSlotData(getArtPlace(place.first), place.first, visibleArtSet);
+}
+
+void CArtifactsOfHeroAltar::updateBackpackSlots()
+{
+	for(auto artPlace : backpack)
+		setSlotData(getArtPlace(artPlace->slot), artPlace->slot, visibleArtSet);
+}
+
+void CArtifactsOfHeroAltar::scrollBackpack(int offset)
+{
+	CArtifactsOfHeroBase::scrollBackpackForArtSet(offset, visibleArtSet);
+	safeRedraw();
+}
+
+void CArtifactsOfHeroAltar::pickUpArtifact(CHeroArtPlace & artPlace)
+{
+	if(const auto art = artPlace.getArt())
+	{
+		pickedArtFromSlot = artPlace.slot;
+		artPlace.setArtifact(nullptr);
+		deleteFromVisible(art);
+		if(ArtifactUtils::isSlotBackpack(pickedArtFromSlot))
+			pickedArtFromSlot = curHero->getSlotByInstance(art);
+		assert(pickedArtFromSlot != ArtifactPosition::PRE_FIRST);
+		LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, pickedArtFromSlot), ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS));
+	}
+}
+
+void CArtifactsOfHeroAltar::swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc)
+{
+	LOCPLINT->cb->swapArtifacts(srcLoc, dstLoc);
+	const auto pickedArtInst = curHero->getArt(ArtifactPosition::TRANSITION_POS);
+	assert(pickedArtInst);
+	visibleArtSet.putArtifact(dstLoc.slot, const_cast<CArtifactInstance*>(pickedArtInst));
+}
+
+void CArtifactsOfHeroAltar::pickedArtMoveToAltar(const ArtifactPosition & slot)
+{
+	if(ArtifactUtils::isSlotBackpack(slot) || ArtifactUtils::isSlotEquipment(slot) || slot == ArtifactPosition::TRANSITION_POS)
+	{
+		assert(!curHero->getSlot(pickedArtFromSlot)->getArt());
+		LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, slot), ArtifactLocation(curHero, pickedArtFromSlot));
+		pickedArtFromSlot = ArtifactPosition::PRE_FIRST;
+	}
+}
+
+void CArtifactsOfHeroAltar::deleteFromVisible(const CArtifactInstance * artInst)
+{
+	const auto slot = visibleArtSet.getSlotByInstance(artInst);
+	visibleArtSet.removeArtifact(slot);
+	if(ArtifactUtils::isSlotBackpack(slot))
+	{
+		scrollBackpackForArtSet(0, visibleArtSet);
+	}
+	else
+	{
+		if(artInst->canBeDisassembled())
+		{
+			for(const auto & part : dynamic_cast<const CCombinedArtifactInstance*>(artInst)->constituentsInfo)
+			{
+				if(part.slot != ArtifactPosition::PRE_FIRST)
+					getArtPlace(part.slot)->setArtifact(nullptr);
+			}
+		}
+	}
+}

+ 32 - 0
client/widgets/CArtifactsOfHeroAltar.h

@@ -0,0 +1,32 @@
+/*
+ * CArtifactsOfHeroAltar.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 "CArtifactsOfHeroBase.h"
+
+#include "../../lib/CArtHandler.h"
+
+class CArtifactsOfHeroAltar : public CArtifactsOfHeroBase
+{
+public:
+	std::set<const CArtifactInstance*> artifactsOnAltar;
+	ArtifactPosition pickedArtFromSlot;
+	CArtifactFittingSet visibleArtSet;
+
+	CArtifactsOfHeroAltar(const Point & position);
+	void setHero(const CGHeroInstance * hero) override;
+	void updateWornSlots() override;
+	void updateBackpackSlots() override;
+	void scrollBackpack(int offset) override;
+	void pickUpArtifact(CHeroArtPlace & artPlace);
+	void swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc);
+	void pickedArtMoveToAltar(const ArtifactPosition & slot);
+	void deleteFromVisible(const CArtifactInstance * artInst);
+};

+ 278 - 0
client/widgets/CArtifactsOfHeroBase.cpp

@@ -0,0 +1,278 @@
+/*
+ * CArtifactsOfHeroBase.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 "CArtifactsOfHeroBase.h"
+
+#include "../gui/CGuiHandler.h"
+#include "../gui/CursorHandler.h"
+#include "../gui/Shortcut.h"
+
+#include "Buttons.h"
+
+#include "../renderSDL/SDL_Extensions.h"
+#include "../CPlayerInterface.h"
+#include "../CGameInfo.h"
+
+#include "../../CCallback.h"
+
+#include "../../lib/mapObjects/CGHeroInstance.h"
+
+CArtifactsOfHeroBase::CArtifactsOfHeroBase()
+	: backpackPos(0),
+	curHero(nullptr)
+{
+}
+
+CArtifactsOfHeroBase::~CArtifactsOfHeroBase()
+{
+	// TODO: cursor handling is CWindowWithArtifacts level. Should be moved when trading, kingdom and hero window are reworked
+	// This will interfere with the implementation of a separate backpack window
+	CCS->curh->dragAndDropCursor(nullptr);
+
+	// Artifact located in artifactsTransitionPos should be returned
+	if(getPickedArtifact())
+	{
+		auto slot = ArtifactUtils::getArtAnyPosition(curHero, curHero->artifactsTransitionPos.begin()->artifact->getTypeId());
+		if(slot == ArtifactPosition::PRE_FIRST)
+		{
+			LOCPLINT->cb->eraseArtifactByClient(ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS));
+		}
+		else
+		{
+			LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS), ArtifactLocation(curHero, slot));
+		}
+	}
+}
+
+void CArtifactsOfHeroBase::init(
+	CHeroArtPlace::ClickHandler lClickCallback,
+	CHeroArtPlace::ClickHandler rClickCallback,
+	const Point & position,
+	BpackScrollHandler scrollHandler)
+{
+	// CArtifactsOfHeroBase::init may be transform to CArtifactsOfHeroBase::CArtifactsOfHeroBase if OBJECT_CONSTRUCTION_CAPTURING is removed
+	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
+	pos += position;
+	for(int g = 0; g < GameConstants::BACKPACK_START; g++)
+	{
+		artWorn[ArtifactPosition(g)] = std::make_shared<CHeroArtPlace>(slotPos[g]);
+	}
+	backpack.clear();
+	for(int s = 0; s < 5; s++)
+	{
+		auto artPlace = std::make_shared<CHeroArtPlace>(Point(403 + 46 * s, 365));
+		backpack.push_back(artPlace);
+	}
+	for(auto artPlace : artWorn)
+	{
+		artPlace.second->slot = artPlace.first;
+		artPlace.second->setArtifact(nullptr);
+		artPlace.second->leftClickCallback = lClickCallback;
+		artPlace.second->rightClickCallback = rClickCallback;
+	}
+	for(auto artPlace : backpack)
+	{
+		artPlace->setArtifact(nullptr);
+		artPlace->leftClickCallback = lClickCallback;
+		artPlace->rightClickCallback = rClickCallback;
+	}
+	leftBackpackRoll = std::make_shared<CButton>(Point(379, 364), "hsbtns3.def", CButton::tooltip(), [scrollHandler]() { scrollHandler(-1); }, EShortcut::MOVE_LEFT);
+	rightBackpackRoll = std::make_shared<CButton>(Point(632, 364), "hsbtns5.def", CButton::tooltip(), [scrollHandler]() { scrollHandler(+1); }, EShortcut::MOVE_RIGHT);
+	leftBackpackRoll->block(true);
+	rightBackpackRoll->block(true);
+}
+
+void CArtifactsOfHeroBase::leftClickArtPlace(CHeroArtPlace & artPlace)
+{
+	if(leftClickCallback)
+		leftClickCallback(*this, artPlace);
+}
+
+void CArtifactsOfHeroBase::rightClickArtPlace(CHeroArtPlace & artPlace)
+{
+	if(rightClickCallback)
+		rightClickCallback(*this, artPlace);
+}
+
+void CArtifactsOfHeroBase::setHero(const CGHeroInstance * hero)
+{
+	curHero = hero;
+	if(curHero->artifactsInBackpack.size() > 0)
+		backpackPos %= curHero->artifactsInBackpack.size();
+	else
+		backpackPos = 0;
+
+	for(auto slot : artWorn)
+	{
+		setSlotData(slot.second, slot.first, *curHero);
+	}
+	scrollBackpackForArtSet(0, *curHero);
+}
+
+const CGHeroInstance * CArtifactsOfHeroBase::getHero() const
+{
+	return curHero;
+}
+
+void CArtifactsOfHeroBase::scrollBackpack(int offset)
+{
+	scrollBackpackForArtSet(offset, *curHero);
+	safeRedraw();
+}
+
+void CArtifactsOfHeroBase::scrollBackpackForArtSet(int offset, const CArtifactSet & artSet)
+{
+	// offset==-1 => to left; offset==1 => to right
+	using slotInc = std::function<ArtifactPosition(ArtifactPosition&)>;
+	auto artsInBackpack = static_cast<int>(artSet.artifactsInBackpack.size());
+	auto scrollingPossible = artsInBackpack > backpack.size();
+
+	slotInc inc_straight = [](ArtifactPosition & slot) -> ArtifactPosition
+	{
+		return slot + 1;
+	};
+	slotInc inc_ring = [artsInBackpack](ArtifactPosition & slot) -> ArtifactPosition
+	{
+		return ArtifactPosition(GameConstants::BACKPACK_START + (slot - GameConstants::BACKPACK_START + 1) % artsInBackpack);
+	};
+	slotInc inc;
+	if(scrollingPossible)
+		inc = inc_ring;
+	else
+		inc = inc_straight;
+
+	backpackPos += offset;
+	if(backpackPos < 0)
+		backpackPos += artsInBackpack;
+
+	if(artsInBackpack)
+		backpackPos %= artsInBackpack;
+
+	auto slot = ArtifactPosition(GameConstants::BACKPACK_START + backpackPos);
+	for(auto artPlace : backpack)
+	{
+		setSlotData(artPlace, slot, artSet);
+		slot = inc(slot);
+	}
+
+	// Blocking scrolling if there is not enough artifacts to scroll
+	leftBackpackRoll->block(!scrollingPossible);
+	rightBackpackRoll->block(!scrollingPossible);
+}
+
+void CArtifactsOfHeroBase::safeRedraw()
+{
+	if(active)
+	{
+		if(parent)
+			parent->redraw();
+		else
+			redraw();
+	}
+}
+
+void CArtifactsOfHeroBase::markPossibleSlots(const CArtifactInstance * art, bool assumeDestRemoved)
+{
+	for(auto artPlace : artWorn)
+		artPlace.second->selectSlot(art->artType->canBePutAt(curHero, artPlace.second->slot, assumeDestRemoved));
+}
+
+void CArtifactsOfHeroBase::unmarkSlots()
+{
+	for(auto & artPlace : artWorn)
+		artPlace.second->selectSlot(false);
+
+	for(auto & artPlace : backpack)
+		artPlace->selectSlot(false);
+}
+
+CArtifactsOfHeroBase::ArtPlacePtr CArtifactsOfHeroBase::getArtPlace(const ArtifactPosition & slot)
+{
+	if(ArtifactUtils::isSlotEquipment(slot))
+	{
+		if(artWorn.find(slot) == artWorn.end())
+		{
+			logGlobal->error("CArtifactsOfHero::getArtPlace: invalid slot %d", slot);
+			return nullptr;
+		}
+		return artWorn[slot];
+	}
+	if(ArtifactUtils::isSlotBackpack(slot))
+	{
+		for(ArtPlacePtr artPlace : backpack)
+			if(artPlace->slot == slot)
+				return artPlace;
+		return nullptr;
+	}
+	else
+	{
+		return nullptr;
+	}
+}
+
+void CArtifactsOfHeroBase::updateWornSlots()
+{
+	for(auto place : artWorn)
+		updateSlot(place.first);
+}
+
+void CArtifactsOfHeroBase::updateBackpackSlots()
+{
+	for(auto artPlace : backpack)
+		updateSlot(artPlace->slot);
+	scrollBackpackForArtSet(0, *curHero);
+}
+
+void CArtifactsOfHeroBase::updateSlot(const ArtifactPosition & slot)
+{
+	setSlotData(getArtPlace(slot), slot, *curHero);
+}
+
+const CArtifactInstance * CArtifactsOfHeroBase::getPickedArtifact()
+{
+	// Returns only the picked up artifact. Not just highlighted like in the trading window.
+	if(!curHero || curHero->artifactsTransitionPos.empty())
+		return nullptr;
+	else
+		return curHero->getArt(ArtifactPosition::TRANSITION_POS);
+}
+
+void CArtifactsOfHeroBase::setSlotData(ArtPlacePtr artPlace, const ArtifactPosition & slot, const CArtifactSet & artSet)
+{
+	// Spurious call from artifactMoved in attempt to update hidden backpack slot
+	if(!artPlace && ArtifactUtils::isSlotBackpack(slot))
+	{
+		return;
+	}
+
+	artPlace->slot = slot;
+	if(auto slotInfo = artSet.getSlot(slot))
+	{
+		artPlace->lockSlot(slotInfo->locked);
+		artPlace->setArtifact(slotInfo->artifact);
+		if(!slotInfo->artifact->canBeDisassembled())
+		{
+			// 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->constituentOf)
+			{
+				arts.insert(std::pair(combinedArt, 0));
+				for(const auto part : *combinedArt->constituents)
+					if(artSet.hasArt(part->getId(), true))
+						arts.at(combinedArt)++;
+			}
+			artPlace->addCombinedArtInfo(arts);
+		}
+	}
+	else
+	{
+		artPlace->setArtifact(nullptr);
+	}
+}

+ 67 - 0
client/widgets/CArtifactsOfHeroBase.h

@@ -0,0 +1,67 @@
+/*
+ * CArtifactsOfHeroBase.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 "CArtifactHolder.h"
+
+class CArtifactsOfHeroBase : public CIntObject
+{
+protected:
+	using ArtPlacePtr = std::shared_ptr<CHeroArtPlace>;
+	using BpackScrollHandler = std::function<void(int)>;
+
+public:
+	using ArtPlaceMap = std::map<ArtifactPosition, ArtPlacePtr>;
+	using ClickHandler = std::function<void(CArtifactsOfHeroBase&, CHeroArtPlace&)>;
+
+	const CGHeroInstance * curHero;
+	ClickHandler leftClickCallback;
+	ClickHandler rightClickCallback;
+	
+	CArtifactsOfHeroBase();
+	virtual ~CArtifactsOfHeroBase();
+	virtual void leftClickArtPlace(CHeroArtPlace & artPlace);
+	virtual void rightClickArtPlace(CHeroArtPlace & artPlace);
+	virtual void setHero(const CGHeroInstance * hero);
+	virtual const CGHeroInstance * getHero() const;
+	virtual void scrollBackpack(int offset);
+	virtual void safeRedraw();
+	virtual void markPossibleSlots(const CArtifactInstance * art, bool assumeDestRemoved = true);
+	virtual void unmarkSlots();
+	virtual ArtPlacePtr getArtPlace(const ArtifactPosition & slot);
+	virtual void updateWornSlots();
+	virtual void updateBackpackSlots();
+	virtual void updateSlot(const ArtifactPosition & slot);
+	virtual const CArtifactInstance * getPickedArtifact();
+
+protected:
+	ArtPlaceMap artWorn;
+	std::vector<ArtPlacePtr> backpack;
+	std::shared_ptr<CButton> leftBackpackRoll;
+	std::shared_ptr<CButton> rightBackpackRoll;
+	int backpackPos; // Position to display artifacts in heroes backpack
+
+	const std::vector<Point> slotPos =
+	{
+		Point(509,30),  Point(567,240), Point(509,80),  //0-2
+		Point(383,68),  Point(564,183), Point(509,130), //3-5
+		Point(431,68),  Point(610,183), Point(515,295), //6-8
+		Point(383,143), Point(399,194), Point(415,245), //9-11
+		Point(431,296), Point(564,30),  Point(610,30), //12-14
+		Point(610,76),  Point(610,122), Point(610,310), //15-17
+		Point(381,296) //18
+	};
+
+	virtual void init(CHeroArtPlace::ClickHandler lClickCallback, CHeroArtPlace::ClickHandler rClickCallback,
+		const Point & position, BpackScrollHandler scrollHandler);
+	// Assigns an artifacts to an artifact place depending on it's new slot ID
+	virtual void setSlotData(ArtPlacePtr artPlace, const ArtifactPosition & slot, const CArtifactSet & artSet);
+	virtual void scrollBackpackForArtSet(int offset, const CArtifactSet & artSet);
+};

+ 54 - 0
client/widgets/CArtifactsOfHeroKingdom.cpp

@@ -0,0 +1,54 @@
+/*
+ * CArtifactsOfHeroKingdom.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 "CArtifactsOfHeroKingdom.h"
+
+#include "Buttons.h"
+
+#include "../CPlayerInterface.h"
+
+#include "../../CCallback.h"
+
+CArtifactsOfHeroKingdom::CArtifactsOfHeroKingdom(ArtPlaceMap ArtWorn, std::vector<ArtPlacePtr> Backpack,
+	std::shared_ptr<CButton> leftScroll, std::shared_ptr<CButton> rightScroll)
+{
+	artWorn = ArtWorn;
+	backpack = Backpack;
+	leftBackpackRoll = leftScroll;
+	rightBackpackRoll = rightScroll;
+
+	for(auto artPlace : artWorn)
+	{
+		artPlace.second->slot = artPlace.first;
+		artPlace.second->setArtifact(nullptr);
+		artPlace.second->leftClickCallback = std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1);
+		artPlace.second->rightClickCallback = std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1);
+	}
+	for(auto artPlace : backpack)
+	{
+		artPlace->setArtifact(nullptr);
+		artPlace->leftClickCallback = std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1);
+		artPlace->rightClickCallback = std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1);
+	}
+	leftBackpackRoll->addCallback(std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, -1));
+	rightBackpackRoll->addCallback(std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, +1));
+}
+
+void CArtifactsOfHeroKingdom::swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc)
+{
+	LOCPLINT->cb->swapArtifacts(srcLoc, dstLoc);
+}
+
+void CArtifactsOfHeroKingdom::pickUpArtifact(CHeroArtPlace & artPlace)
+{
+	LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, artPlace.slot),
+		ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS));
+}
+

+ 27 - 0
client/widgets/CArtifactsOfHeroKingdom.h

@@ -0,0 +1,27 @@
+/*
+ * CArtifactsOfHeroKingdom.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 "CArtifactsOfHeroBase.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+struct ArtifactLocation;
+
+VCMI_LIB_NAMESPACE_END
+
+class CArtifactsOfHeroKingdom : public CArtifactsOfHeroBase
+{
+public:
+	CArtifactsOfHeroKingdom(ArtPlaceMap ArtWorn, std::vector<ArtPlacePtr> Backpack,
+		std::shared_ptr<CButton> leftScroll, std::shared_ptr<CButton> rightScroll);
+	void swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc);
+	void pickUpArtifact(CHeroArtPlace & artPlace);
+};

+ 35 - 0
client/widgets/CArtifactsOfHeroMain.cpp

@@ -0,0 +1,35 @@
+/*
+ * CArtifactsOfHeroMain.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 "CArtifactsOfHeroMain.h"
+
+#include "../CPlayerInterface.h"
+
+#include "../../CCallback.h"
+
+CArtifactsOfHeroMain::CArtifactsOfHeroMain(const Point & position)
+{
+	init(
+		std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1),
+		std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1),
+		position,
+		std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, _1));
+}
+
+void CArtifactsOfHeroMain::swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc)
+{
+	LOCPLINT->cb->swapArtifacts(srcLoc, dstLoc);
+}
+
+void CArtifactsOfHeroMain::pickUpArtifact(CHeroArtPlace & artPlace)
+{
+	LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, artPlace.slot),
+		ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS));
+}

+ 26 - 0
client/widgets/CArtifactsOfHeroMain.h

@@ -0,0 +1,26 @@
+/*
+ * CArtifactsOfHeroMain.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 "CArtifactsOfHeroBase.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+struct ArtifactLocation;
+
+VCMI_LIB_NAMESPACE_END
+
+class CArtifactsOfHeroMain : public CArtifactsOfHeroBase
+{
+public:
+	CArtifactsOfHeroMain(const Point & position);
+	void swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc);
+	void pickUpArtifact(CHeroArtPlace & artPlace);
+};

+ 41 - 0
client/widgets/CArtifactsOfHeroMarket.cpp

@@ -0,0 +1,41 @@
+/*
+ * CArtifactsOfHeroMarket.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 "CArtifactsOfHeroMarket.h"
+
+#include "../../lib/mapObjects/CGHeroInstance.h"
+
+CArtifactsOfHeroMarket::CArtifactsOfHeroMarket(const Point & position)
+{
+	init(
+		std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1), 
+		std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1), 
+		position,
+		std::bind(&CArtifactsOfHeroMarket::scrollBackpack, this, _1));
+};
+
+void CArtifactsOfHeroMarket::scrollBackpack(int offset)
+{
+	CArtifactsOfHeroBase::scrollBackpackForArtSet(offset, *curHero);
+
+	// We may have highlight on one of backpack artifacts
+	if(selectArtCallback)
+	{
+		for(auto & artPlace : backpack)
+		{
+			if(artPlace->isMarked())
+			{
+				selectArtCallback(artPlace.get());
+				break;
+			}
+		}
+	}
+	safeRedraw();
+}

+ 21 - 0
client/widgets/CArtifactsOfHeroMarket.h

@@ -0,0 +1,21 @@
+/*
+ * CArtifactsOfHeroMarket.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 "CArtifactsOfHeroBase.h"
+
+class CArtifactsOfHeroMarket : public CArtifactsOfHeroBase
+{
+public:
+	std::function<void(CHeroArtPlace*)> selectArtCallback;
+
+	CArtifactsOfHeroMarket(const Point & position);
+	void scrollBackpack(int offset) override;
+};

+ 4 - 2
client/widgets/CComponent.cpp

@@ -19,6 +19,7 @@
 #include "../gui/CGuiHandler.h"
 #include "../gui/CursorHandler.h"
 #include "../gui/TextAlignment.h"
+#include "../gui/Shortcut.h"
 #include "../renderSDL/SDL_Extensions.h"
 #include "../windows/CMessage.h"
 #include "../windows/InfoWindows.h"
@@ -488,11 +489,12 @@ CComponentBox::CComponentBox(std::vector<std::shared_ptr<CSelectableComponent>>
 
 	assert(!components.empty());
 
-	int key = SDLK_1;
+	auto key = EShortcut::SELECT_INDEX_1;
 	for(auto & comp : _components)
 	{
 		comp->onSelect = std::bind(&CComponentBox::selectionChanged, this, comp);
-		comp->assignedKeys.insert(key++);
+		comp->assignedKey = key;
+		key = vstd::next(key, 1);
 	}
 	selectionChanged(_components.front());
 }

+ 5 - 8
client/widgets/CGarrisonInt.cpp

@@ -185,20 +185,17 @@ bool CGarrisonSlot::highlightOrDropArtifact()
 	bool artSelected = false;
 	if (CWindowWithArtifacts* chw = dynamic_cast<CWindowWithArtifacts*>(GH.topInt().get())) //dirty solution
 	{
-		std::shared_ptr<CArtifactsOfHero::SCommonPart> commonInfo = chw->getCommonPart();
-		const CArtifactInstance * art = nullptr;
-		if(commonInfo)
-			art = commonInfo->src.art;
+		const auto pickedArtInst = chw->getPickedArtifact();
 
-		if(art)
+		if(pickedArtInst)
 		{
-			const CGHeroInstance *srcHero = commonInfo->src.AOH->getHero();
+			const auto * srcHero = chw->getHeroPickedArtifact();
 			artSelected = true;
 			if (myStack) // try dropping the artifact only if the slot isn't empty
 			{
-				ArtifactLocation src(srcHero, commonInfo->src.slotID);
+				ArtifactLocation src(srcHero, ArtifactPosition::TRANSITION_POS);
 				ArtifactLocation dst(myStack, ArtifactPosition::CREATURE_SLOT);
-				if (art->canBePutAt(dst, true))
+				if(pickedArtInst->canBePutAt(dst, true))
 				{	//equip clicked stack
 					if(dst.getArt())
 					{

+ 363 - 0
client/widgets/CWindowWithArtifacts.cpp

@@ -0,0 +1,363 @@
+/*
+ * CWindowWithArtifacts.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 "CWindowWithArtifacts.h"
+
+#include "../gui/CGuiHandler.h"
+#include "../gui/CursorHandler.h"
+
+#include "CComponent.h"
+
+#include "../windows/CHeroWindow.h"
+#include "../windows/CSpellWindow.h"
+#include "../windows/GUIClasses.h"
+#include "../CPlayerInterface.h"
+#include "../CGameInfo.h"
+
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/mapObjects/CGHeroInstance.h"
+
+void CWindowWithArtifacts::addSet(CArtifactsOfHeroPtr artSet)
+{
+	artSets.emplace_back(artSet);
+	std::visit([this](auto artSetWeak)
+		{
+			auto artSet = artSetWeak.lock();
+			artSet->leftClickCallback = std::bind(&CWindowWithArtifacts::leftClickArtPlaceHero, this, _1, _2);
+			artSet->rightClickCallback = std::bind(&CWindowWithArtifacts::rightClickArtPlaceHero, this, _1, _2);
+		}, artSet);
+}
+
+const CGHeroInstance * CWindowWithArtifacts::getHeroPickedArtifact()
+{
+	auto res = getState();
+	if(res.has_value())
+		return std::get<const CGHeroInstance*>(res.value());
+	else
+		return nullptr;
+}
+
+const CArtifactInstance * CWindowWithArtifacts::getPickedArtifact()
+{
+	auto res = getState();
+	if(res.has_value())
+		return std::get<const CArtifactInstance*>(res.value());
+	else
+		return nullptr;
+}
+
+void CWindowWithArtifacts::leftClickArtPlaceHero(CArtifactsOfHeroBase & artsInst, CHeroArtPlace & artPlace)
+{
+	const auto artSetWeak = findAOHbyRef(artsInst);
+	assert(artSetWeak.has_value());
+
+	if(artPlace.isLocked())
+		return;
+
+	const auto checkSpecialArts = [](const CGHeroInstance * hero, CHeroArtPlace & artPlace) -> bool
+	{
+		if(artPlace.getArt()->getTypeId() == ArtifactID::SPELLBOOK)
+		{
+			GH.pushIntT<CSpellWindow>(hero, LOCPLINT, LOCPLINT->battleInt.get());
+			return false;
+		}
+		if(artPlace.getArt()->getTypeId() == ArtifactID::CATAPULT)
+		{
+			// The Catapult must be equipped
+			std::vector<std::shared_ptr<CComponent>> catapult(1, std::make_shared<CComponent>(CComponent::artifact, 3, 0));
+			LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[312], catapult);
+			return false;
+		}
+		return true;
+	};
+
+	std::visit(
+		[checkSpecialArts, this, &artPlace](auto artSetWeak) -> void
+		{
+			const auto artSetPtr = artSetWeak.lock();
+
+			// Hero(Main, Exchange) window, Kingdom window, Altar window left click handler
+			if constexpr(
+				std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroMain>> || 
+				std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroKingdom>> ||
+				std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroAltar>>)
+			{
+				const auto pickedArtInst = getPickedArtifact();
+				const auto heroPickedArt = getHeroPickedArtifact();
+				const auto hero = artSetPtr->getHero();
+
+				if(pickedArtInst)
+				{
+					auto srcLoc = ArtifactLocation(heroPickedArt, ArtifactPosition::TRANSITION_POS);
+					auto dstLoc = ArtifactLocation(hero, artPlace.slot);
+					auto isTransferAllowed = false;
+
+					if(ArtifactUtils::isSlotBackpack(artPlace.slot))
+					{
+						if(pickedArtInst->artType->isBig())
+						{
+							// War machines cannot go to backpack
+							LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[153]) % pickedArtInst->artType->getNameTranslated()));
+						}
+						else
+						{
+							if(ArtifactUtils::isBackpackFreeSlots(heroPickedArt))
+								isTransferAllowed = true;
+							else
+								LOCPLINT->showInfoDialog(CGI->generaltexth->translate("core.genrltxt.152"));
+						}
+					}
+					// Check if artifact transfer is possible
+					else if(pickedArtInst->canBePutAt(dstLoc, true) && (!artPlace.getArt() || hero->tempOwner == LOCPLINT->playerID))
+					{
+						isTransferAllowed = true;
+					}
+					if constexpr(std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroKingdom>>)
+					{
+						if(hero != heroPickedArt)
+							isTransferAllowed = false;
+					}
+					if(isTransferAllowed)
+						artSetPtr->swapArtifacts(srcLoc, dstLoc);
+				}
+				else
+				{
+					if(artPlace.getArt())
+					{			
+						if(artSetPtr->getHero()->tempOwner == LOCPLINT->playerID)
+						{
+							if(checkSpecialArts(hero, artPlace))
+								artSetPtr->pickUpArtifact(artPlace);
+						}
+						else
+						{
+							for(const auto artSlot : ArtifactUtils::unmovableSlots())
+								if(artPlace.slot == artSlot)
+								{
+									LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[21]);
+									break;
+								}
+						}
+					}
+				}
+			}
+			// Market window left click handler
+			else if constexpr(std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroMarket>>)
+			{
+				if(artSetPtr->selectArtCallback && artPlace.getArt())
+				{
+					if(artPlace.getArt()->artType->isTradable())
+					{
+						artSetPtr->unmarkSlots();
+						artPlace.selectSlot(true);
+						artSetPtr->selectArtCallback(&artPlace);
+					}
+					else
+					{
+						// This item can't be traded
+						LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[21]);
+					}
+				}
+			}
+		}, artSetWeak.value());
+}
+
+void CWindowWithArtifacts::rightClickArtPlaceHero(CArtifactsOfHeroBase & artsInst, CHeroArtPlace & artPlace)
+{
+	const auto artSetWeak = findAOHbyRef(artsInst);
+	assert(artSetWeak.has_value());
+
+	if(artPlace.isLocked())
+		return;
+
+	std::visit(
+		[&artPlace](auto artSetWeak) -> void
+		{
+			const auto artSetPtr = artSetWeak.lock();
+
+			// Hero(Main, Exchange) window, Kingdom window right click handler
+			if constexpr(
+				std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroMain>> ||
+				std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroKingdom>>)
+			{
+				if(artPlace.getArt())
+				{
+					if(ArtifactUtilsClient::askToDisassemble(artSetPtr->getHero(), artPlace.slot))
+					{
+						return;
+					}
+					if(ArtifactUtilsClient::askToAssemble(artSetPtr->getHero(), artPlace.slot))
+					{
+						return;
+					}
+					if(artPlace.text.size())
+						artPlace.LRClickableAreaWTextComp::clickRight(boost::logic::tribool::true_value, false);
+				}
+			}
+			// Altar window, Market window right click handler
+			else if constexpr(
+				std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroAltar>> ||
+				std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroMarket>>)
+			{
+				if(artPlace.getArt() && artPlace.text.size())
+					artPlace.LRClickableAreaWTextComp::clickRight(boost::logic::tribool::true_value, false);
+			}
+		}, artSetWeak.value());
+}
+
+void CWindowWithArtifacts::artifactRemoved(const ArtifactLocation & artLoc)
+{
+	updateSlots(artLoc.slot);
+}
+
+void CWindowWithArtifacts::artifactMoved(const ArtifactLocation & srcLoc, const ArtifactLocation & destLoc, bool withRedraw)
+{
+	auto curState = getState();
+	if(!curState.has_value())
+		// Transition state. Nothing to do here. Just skip. Need to wait for final state.
+		return;
+
+	// When moving one artifact onto another it leads to two art movements: dst->TRANSITION_POS; src->dst
+	// However after first movement we pick the art from TRANSITION_POS and the second movement coming when
+	// we have a different artifact may look surprising... but it's valid.
+
+	auto pickedArtInst = std::get<const CArtifactInstance*>(curState.value());
+	assert(srcLoc.isHolder(std::get<const CGHeroInstance*>(curState.value())));
+	assert(srcLoc.getArt() == pickedArtInst);
+
+	auto artifactMovedBody = [this, withRedraw, &srcLoc, &destLoc, &pickedArtInst](auto artSetWeak) -> void
+	{
+		auto artSetPtr = artSetWeak.lock();
+		if(artSetPtr)
+		{
+			const auto hero = artSetPtr->getHero();
+			if(artSetPtr->active)
+			{
+				if(pickedArtInst)
+				{
+					CCS->curh->dragAndDropCursor("artifact", pickedArtInst->artType->getIconIndex());
+					if(srcLoc.isHolder(hero) || !std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroKingdom>>)
+						artSetPtr->markPossibleSlots(pickedArtInst, hero->tempOwner == LOCPLINT->playerID);
+				}
+				else
+				{
+					artSetPtr->unmarkSlots();
+					CCS->curh->dragAndDropCursor(nullptr);
+				}
+			}
+			if(withRedraw)
+			{
+				artSetPtr->updateWornSlots();
+				artSetPtr->updateBackpackSlots();
+
+				// Update arts bonuses on window.
+				// TODO rework this part when CHeroWindow and CExchangeWindow are reworked
+				if(auto * chw = dynamic_cast<CHeroWindow*>(this))
+				{
+					chw->update(hero, true);
+				}
+				else if(auto * cew = dynamic_cast<CExchangeWindow*>(this))
+				{
+					cew->updateWidgets();
+				}
+				artSetPtr->safeRedraw();
+			}
+
+			// Make sure the status bar is updated so it does not display old text
+			if(destLoc.isHolder(hero))
+			{
+				if(auto artPlace = artSetPtr->getArtPlace(destLoc.slot))
+					artPlace->hover(true);
+			}
+		}
+	};
+
+	for(auto artSetWeak : artSets)
+		std::visit(artifactMovedBody, artSetWeak);
+}
+
+void CWindowWithArtifacts::artifactDisassembled(const ArtifactLocation & artLoc)
+{
+	updateSlots(artLoc.slot);
+}
+
+void CWindowWithArtifacts::artifactAssembled(const ArtifactLocation & artLoc)
+{
+	updateSlots(artLoc.slot);
+}
+
+void CWindowWithArtifacts::updateSlots(const ArtifactPosition & slot)
+{
+	auto updateSlotBody = [slot](auto artSetWeak) -> void
+	{
+		if(const auto artSetPtr = artSetWeak.lock())
+		{
+			if(ArtifactUtils::isSlotEquipment(slot))
+				artSetPtr->updateWornSlots();
+			else if(ArtifactUtils::isSlotBackpack(slot))
+				artSetPtr->updateBackpackSlots();
+
+			artSetPtr->safeRedraw();
+		}
+	};
+
+	for(auto artSetWeak : artSets)
+		std::visit(updateSlotBody, artSetWeak);
+}
+
+std::optional<std::tuple<const CGHeroInstance*, const CArtifactInstance*>> CWindowWithArtifacts::getState()
+{
+	const CArtifactInstance * artInst = nullptr;
+	const CGHeroInstance * hero = nullptr;
+	size_t pickedCnt = 0;
+
+	auto getHeroArtBody = [&hero, &artInst, &pickedCnt](auto artSetWeak) -> void
+	{
+		auto artSetPtr = artSetWeak.lock();
+		if(artSetPtr)
+		{
+			if(const auto art = artSetPtr->getPickedArtifact())
+			{
+				artInst = art;
+				hero = artSetPtr->getHero();
+				pickedCnt += hero->artifactsTransitionPos.size();
+			}
+		}
+	};
+	for(auto artSetWeak : artSets)
+		std::visit(getHeroArtBody, artSetWeak);
+
+	// The state is possible when the hero has placed an artifact in the ArtifactPosition::TRANSITION_POS,
+	// and the previous artifact has not yet removed from the ArtifactPosition::TRANSITION_POS.
+	// This is a transitional state. Then return nullopt.
+	if(pickedCnt > 1)
+		return std::nullopt;
+	else
+		return std::make_tuple(hero, artInst);
+}
+
+std::optional<CWindowWithArtifacts::CArtifactsOfHeroPtr> CWindowWithArtifacts::findAOHbyRef(CArtifactsOfHeroBase & artsInst)
+{
+	std::optional<CArtifactsOfHeroPtr> res;
+
+	auto findAOHBody = [&res, &artsInst](auto & artSetWeak) -> void
+	{
+		if(&artsInst == artSetWeak.lock().get())
+			res = artSetWeak;
+	};
+
+	for(auto artSetWeak : artSets)
+	{
+		std::visit(findAOHBody, artSetWeak);
+		if(res.has_value())
+			return res;
+	}
+	return res;
+}

+ 44 - 0
client/widgets/CWindowWithArtifacts.h

@@ -0,0 +1,44 @@
+/*
+ * CWindowWithArtifacts.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 "CArtifactHolder.h"
+#include "CArtifactsOfHeroMain.h"
+#include "CArtifactsOfHeroKingdom.h"
+#include "CArtifactsOfHeroAltar.h"
+#include "CArtifactsOfHeroMarket.h"
+
+class CWindowWithArtifacts : public CArtifactHolder
+{
+public:
+	using CArtifactsOfHeroPtr = std::variant<
+		std::weak_ptr<CArtifactsOfHeroMarket>,
+		std::weak_ptr<CArtifactsOfHeroAltar>,
+		std::weak_ptr<CArtifactsOfHeroKingdom>,
+		std::weak_ptr<CArtifactsOfHeroMain>>;
+
+	void addSet(CArtifactsOfHeroPtr artSet);
+	const CGHeroInstance * getHeroPickedArtifact();
+	const CArtifactInstance * getPickedArtifact();
+	void leftClickArtPlaceHero(CArtifactsOfHeroBase & artsInst, CHeroArtPlace & artPlace);
+	void rightClickArtPlaceHero(CArtifactsOfHeroBase & artsInst, CHeroArtPlace & artPlace);
+
+	void artifactRemoved(const ArtifactLocation & artLoc) override;
+	void artifactMoved(const ArtifactLocation & srcLoc, const ArtifactLocation & destLoc, bool withRedraw) override;
+	void artifactDisassembled(const ArtifactLocation & artLoc) override;
+	void artifactAssembled(const ArtifactLocation & artLoc) override;
+
+private:
+	std::vector<CArtifactsOfHeroPtr> artSets;
+
+	void updateSlots(const ArtifactPosition & slot);
+	std::optional<std::tuple<const CGHeroInstance*, const CArtifactInstance*>> getState();
+	std::optional<CArtifactsOfHeroPtr> findAOHbyRef(CArtifactsOfHeroBase & artsInst);
+};

+ 1 - 1
client/widgets/MiscWidgets.cpp

@@ -200,7 +200,7 @@ void CMinorResDataBar::showAll(SDL_Surface * to)
 {
 	CIntObject::showAll(to);
 
-	for (EGameResID i=EGameResID::WOOD; i<=EGameResID::GOLD; vstd::advance(i, 1))
+	for (GameResID i=EGameResID::WOOD; i<=EGameResID::GOLD; ++i)
 	{
 		std::string text = std::to_string(LOCPLINT->cb->getResourceAmount(i));
 

+ 1 - 1
client/widgets/ObjectLists.h

@@ -25,7 +25,7 @@ class CAnimation;
 class CObjectList : public CIntObject
 {
 public:
-	typedef std::function<std::shared_ptr<CIntObject>(size_t)> CreateFunc;
+	using CreateFunc = std::function<std::shared_ptr<CIntObject>(size_t)>;
 
 private:
 	CreateFunc createObject;

+ 6 - 7
client/widgets/TextControls.cpp

@@ -15,6 +15,7 @@
 
 #include "../CPlayerInterface.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../windows/CMessage.h"
 #include "../adventureMap/CInGameConsole.h"
 #include "../renderSDL/SDL_Extensions.h"
@@ -555,12 +556,12 @@ void CTextInput::clickLeft(tribool down, bool previousState)
 		giveFocus();
 }
 
-void CTextInput::keyPressed(const SDL_Keycode & key)
+void CTextInput::keyPressed(EShortcut key)
 {
 	if(!focus)
 		return;
 
-	if(key == SDLK_TAB)
+	if(key == EShortcut::GLOBAL_MOVE_FOCUS)
 	{
 		moveFocus();
 		GH.breakEventHandling();
@@ -571,9 +572,7 @@ void CTextInput::keyPressed(const SDL_Keycode & key)
 
 	switch(key)
 	{
-	case SDLK_DELETE: // have index > ' ' so it won't be filtered out by default section
-		return;
-	case SDLK_BACKSPACE:
+	case EShortcut::GLOBAL_BACKSPACE:
 		if(!newText.empty())
 		{
 			TextOperations::trimRightUnicode(newText);
@@ -608,9 +607,9 @@ void CTextInput::setText(const std::string & nText, bool callCb)
 		cb(text);
 }
 
-bool CTextInput::captureThisKey(const SDL_Keycode & key)
+bool CTextInput::captureThisKey(EShortcut key)
 {
-	if(key == SDLK_RETURN || key == SDLK_KP_ENTER || key == SDLK_ESCAPE)
+	if(key == EShortcut::GLOBAL_RETURN)
 		return false;
 
 	return true;

+ 2 - 2
client/widgets/TextControls.h

@@ -225,9 +225,9 @@ public:
 	CTextInput(const Rect & Pos, std::shared_ptr<IImage> srf);
 
 	void clickLeft(tribool down, bool previousState) override;
-	void keyPressed(const SDL_Keycode & key) override;
+	void keyPressed(EShortcut key) override;
 
-	bool captureThisKey(const SDL_Keycode & key) override;
+	bool captureThisKey(EShortcut key) override;
 
 	void textInputed(const std::string & enteredText) override;
 	void textEdited(const std::string & enteredText) override;

+ 14 - 17
client/windows/CCastleInterface.cpp

@@ -21,6 +21,7 @@
 #include "../CPlayerInterface.h"
 #include "../PlayerLocalState.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/Buttons.h"
@@ -1184,8 +1185,7 @@ CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInst
 	income = std::make_shared<CLabel>(195, 443, FONT_SMALL, ETextAlignment::CENTER);
 	icon = std::make_shared<CAnimImage>("ITPT", 0, 0, 15, 387);
 
-	exit = std::make_shared<CButton>(Point(744, 544), "TSBTNS", CButton::tooltip(CGI->generaltexth->tcommands[8]), [&](){close();}, SDLK_RETURN);
-	exit->assignedKeys.insert(SDLK_ESCAPE);
+	exit = std::make_shared<CButton>(Point(744, 544), "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), "TSBTNS", CButton::tooltip(CGI->generaltexth->tcommands[3]), [&]()
@@ -1308,20 +1308,20 @@ void CCastleInterface::recreateIcons()
 
 }
 
-void CCastleInterface::keyPressed(const SDL_Keycode & key)
+void CCastleInterface::keyPressed(EShortcut key)
 {
 	switch(key)
 	{
-	case SDLK_UP:
+	case EShortcut::MOVE_UP:
 		townlist->selectPrev();
 		break;
-	case SDLK_DOWN:
+	case EShortcut::MOVE_DOWN:
 		townlist->selectNext();
 		break;
-	case SDLK_SPACE:
+	case EShortcut::TOWN_SWAP_ARMIES:
 		heroes->swapArmies();
 		break;
-	case SDLK_t:
+	case EShortcut::TOWN_OPEN_TAVERN:
 		if(town->hasBuilt(BuildingID::TAVERN))
 			LOCPLINT->showTavernWindow(town);
 		break;
@@ -1420,8 +1420,7 @@ CHallInterface::CHallInterface(const CGTownInstance * Town):
 	statusbar = CGStatusBar::create(statusbarBackground);
 
 	title = std::make_shared<CLabel>(399, 12, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, town->town->buildings.at(BuildingID(town->hallLevel()+BuildingID::VILLAGE_HALL))->getNameTranslated());
-	exit = std::make_shared<CButton>(Point(748, 556), "TPMAGE1.DEF", CButton::tooltip(CGI->generaltexth->hcommands[8]), [&](){close();}, SDLK_RETURN);
-	exit->assignedKeys.insert(SDLK_ESCAPE);
+	exit = std::make_shared<CButton>(Point(748, 556), "TPMAGE1.DEF", CButton::tooltip(CGI->generaltexth->hcommands[8]), [&](){close();}, EShortcut::GLOBAL_RETURN);
 
 	auto & boxList = town->town->clientInfo.hallSlots;
 	boxes.resize(boxList.size());
@@ -1486,11 +1485,11 @@ CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Buildin
 		std::string tooltipYes = boost::str(boost::format(CGI->generaltexth->allTexts[595]) % building->getNameTranslated());
 		std::string tooltipNo  = boost::str(boost::format(CGI->generaltexth->allTexts[596]) % building->getNameTranslated());
 
-		buy = std::make_shared<CButton>(Point(45, 446), "IBUY30", CButton::tooltip(tooltipYes), [&](){ buyFunc(); }, SDLK_RETURN);
+		buy = std::make_shared<CButton>(Point(45, 446), "IBUY30", CButton::tooltip(tooltipYes), [&](){ buyFunc(); }, EShortcut::GLOBAL_ACCEPT);
 		buy->setBorderColor(Colors::METALLIC_GOLD);
 		buy->block(state!=7 || LOCPLINT->playerID != town->tempOwner);
 
-		cancel = std::make_shared<CButton>(Point(290, 445), "ICANCEL", CButton::tooltip(tooltipNo), [&](){ close();}, SDLK_ESCAPE);
+		cancel = std::make_shared<CButton>(Point(290, 445), "ICANCEL", CButton::tooltip(tooltipNo), [&](){ close();}, EShortcut::GLOBAL_CANCEL);
 		cancel->setBorderColor(Colors::METALLIC_GOLD);
 	}
 }
@@ -1596,8 +1595,7 @@ CFortScreen::CFortScreen(const CGTownInstance * town):
 	title = std::make_shared<CLabel>(400, 12, FONT_BIG, ETextAlignment::CENTER, Colors::WHITE, fortBuilding->getNameTranslated());
 
 	std::string text = boost::str(boost::format(CGI->generaltexth->fcommands[6]) % fortBuilding->getNameTranslated());
-	exit = std::make_shared<CButton>(Point(748, 556), "TPMAGE1", CButton::tooltip(text), [&](){ close(); }, SDLK_RETURN);
-	exit->assignedKeys.insert(SDLK_ESCAPE);
+	exit = std::make_shared<CButton>(Point(748, 556), "TPMAGE1", CButton::tooltip(text), [&](){ close(); }, EShortcut::GLOBAL_RETURN);
 
 	std::vector<Point> positions =
 	{
@@ -1785,8 +1783,7 @@ CMageGuildScreen::CMageGuildScreen(CCastleInterface * owner,std::string imagem)
 	auto statusbarBackground = std::make_shared<CPicture>(background->getSurface(), barRect, 7, 556);
 	statusbar = CGStatusBar::create(statusbarBackground);
 
-	exit = std::make_shared<CButton>(Point(748, 556), "TPMAGE1.DEF", CButton::tooltip(CGI->generaltexth->allTexts[593]), [&](){ close(); }, SDLK_RETURN);
-	exit->assignedKeys.insert(SDLK_ESCAPE);
+	exit = std::make_shared<CButton>(Point(748, 556), "TPMAGE1.DEF", CButton::tooltip(CGI->generaltexth->allTexts[593]), [&](){ close(); }, EShortcut::GLOBAL_RETURN);
 
 	static const std::vector<std::vector<Point> > positions =
 	{
@@ -1866,10 +1863,10 @@ CBlacksmithDialog::CBlacksmithDialog(bool possible, CreatureID creMachineID, Art
 	                std::to_string(aid.toArtifact(CGI->artifacts())->getPrice()));
 
 	std::string text = boost::str(boost::format(CGI->generaltexth->allTexts[595]) % creature->getNameSingularTranslated());
-	buy = std::make_shared<CButton>(Point(42, 312), "IBUY30.DEF", CButton::tooltip(text), [&](){ close(); }, SDLK_RETURN);
+	buy = std::make_shared<CButton>(Point(42, 312), "IBUY30.DEF", CButton::tooltip(text), [&](){ close(); }, EShortcut::GLOBAL_ACCEPT);
 
 	text = boost::str(boost::format(CGI->generaltexth->allTexts[596]) % creature->getNameSingularTranslated());
-	cancel = std::make_shared<CButton>(Point(224, 312), "ICANCEL.DEF", CButton::tooltip(text), [&](){ close(); }, SDLK_ESCAPE);
+	cancel = std::make_shared<CButton>(Point(224, 312), "ICANCEL.DEF", CButton::tooltip(text), [&](){ close(); }, EShortcut::GLOBAL_CANCEL);
 
 	if(possible)
 		buy->addCallback([=](){ LOCPLINT->cb->buyArtifact(LOCPLINT->cb->getHero(hid),aid); });

+ 1 - 1
client/windows/CCastleInterface.h

@@ -246,7 +246,7 @@ public:
 
 	void castleTeleport(int where);
 	void townChange();
-	void keyPressed(const SDL_Keycode & key) override;
+	void keyPressed(EShortcut key) override;
 
 	void close();
 	void addBuilding(BuildingID bid);

+ 7 - 9
client/windows/CCreatureWindow.cpp

@@ -22,6 +22,7 @@
 #include "../widgets/TextControls.h"
 #include "../widgets/ObjectLists.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../renderSDL/SDL_Extensions.h"
 
 #include "../../CCallback.h"
@@ -284,7 +285,7 @@ CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset)
 		{
 			LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[12], onDismiss, nullptr);
 		};
-		dismiss = std::make_shared<CButton>(Point(5, 5),"IVIEWCR2.DEF", CGI->generaltexth->zelp[445], onClick, SDLK_d);
+		dismiss = std::make_shared<CButton>(Point(5, 5),"IVIEWCR2.DEF", CGI->generaltexth->zelp[445], onClick, EShortcut::HERO_DISMISS);
 	}
 
 	if(parent->info->upgradeInfo && !parent->info->commander)
@@ -321,14 +322,12 @@ CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset)
 					LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[314], resComps);
 				}
 			};
-			auto upgradeBtn = std::make_shared<CButton>(Point(221 + (int)buttonIndex * 40, 5), "stackWindow/upgradeButton", CGI->generaltexth->zelp[446], onClick, SDLK_1);
+			auto upgradeBtn = std::make_shared<CButton>(Point(221 + (int)buttonIndex * 40, 5), "stackWindow/upgradeButton", CGI->generaltexth->zelp[446], onClick);
 
 			upgradeBtn->addOverlay(std::make_shared<CAnimImage>("CPRSMALL", VLC->creh->objects[upgradeInfo.info.newID[buttonIndex]]->getIconIndex()));
 
 			if(buttonsToCreate == 1) // single upgrade avaialbe
-			{
-				upgradeBtn->assignedKeys.insert(SDLK_u);
-			}
+				upgradeBtn->assignedKey = EShortcut::RECRUITMENT_UPGRADE;
 
 			upgrade[buttonIndex] = upgradeBtn;
 		}
@@ -356,8 +355,7 @@ CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset)
 		parent->switchButtons[parent->activeTab]->disable();
 	}
 
-	exit = std::make_shared<CButton>(Point(382, 5), "hsbtns.def", CGI->generaltexth->zelp[447], [=](){ parent->close(); }, SDLK_RETURN);
-	exit->assignedKeys.insert(SDLK_ESCAPE);
+	exit = std::make_shared<CButton>(Point(382, 5), "hsbtns.def", CGI->generaltexth->zelp[447], [=](){ parent->close(); }, EShortcut::GLOBAL_RETURN);
 }
 
 CStackWindow::CommanderMainSection::CommanderMainSection(CStackWindow * owner, int yOffset)
@@ -457,8 +455,8 @@ CStackWindow::CommanderMainSection::CommanderMainSection(CStackWindow * owner, i
 
 		abilities = std::make_shared<CListBox>(onCreate, Point(38, 3+pos.h), Point(63, 0), 6, abilitiesCount);
 
-		leftBtn = std::make_shared<CButton>(Point(10,  pos.h + 6), "hsbtns3.def", CButton::tooltip(), [=](){ abilities->moveToPrev(); }, SDLK_LEFT);
-		rightBtn = std::make_shared<CButton>(Point(411, pos.h + 6), "hsbtns5.def", CButton::tooltip(), [=](){ abilities->moveToNext(); }, SDLK_RIGHT);
+		leftBtn = std::make_shared<CButton>(Point(10,  pos.h + 6), "hsbtns3.def", CButton::tooltip(), [=](){ abilities->moveToPrev(); }, EShortcut::MOVE_LEFT);
+		rightBtn = std::make_shared<CButton>(Point(411, pos.h + 6), "hsbtns5.def", CButton::tooltip(), [=](){ abilities->moveToNext(); }, EShortcut::MOVE_RIGHT);
 
 		if(abilitiesCount <= 6)
 		{

+ 21 - 22
client/windows/CHeroWindow.cpp

@@ -19,6 +19,7 @@
 
 #include "../gui/CGuiHandler.h"
 #include "../gui/TextAlignment.h"
+#include "../gui/Shortcut.h"
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/TextControls.h"
@@ -42,9 +43,10 @@ TConstBonusListPtr CHeroWithMaybePickedArtifact::getAllBonuses(const CSelector &
 	TConstBonusListPtr heroBonuses = hero->getAllBonuses(selector, limit, hero, cachingStr);
 	TConstBonusListPtr bonusesFromPickedUpArtifact;
 
-	std::shared_ptr<CArtifactsOfHero::SCommonPart> cp = cww->getCommonPart();
-	if(cp && cp->src.art && cp->src.valid() && cp->src.AOH && cp->src.AOH->getHero() == hero)
-		bonusesFromPickedUpArtifact = cp->src.art->getAllBonuses(selector, limit, hero);
+	const auto pickedArtInst = cww->getPickedArtifact();
+
+	if(pickedArtInst)
+		bonusesFromPickedUpArtifact = pickedArtInst->getAllBonuses(selector, limit, hero);
 	else
 		bonusesFromPickedUpArtifact = TBonusListPtr(new BonusList());
 
@@ -118,22 +120,21 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero)
 
 	statusbar = CGStatusBar::create(7, 559, "ADROLLVR.bmp", 660);
 
-	quitButton = std::make_shared<CButton>(Point(609, 516), "hsbtns.def", CButton::tooltip(heroscrn[17]), [=](){ close(); }, SDLK_RETURN);
-	quitButton->assignedKeys.insert(SDLK_ESCAPE);
+	quitButton = std::make_shared<CButton>(Point(609, 516), "hsbtns.def", CButton::tooltip(heroscrn[17]), [=](){ close(); }, EShortcut::GLOBAL_RETURN);
 
 	dismissLabel = std::make_shared<CTextBox>(CGI->generaltexth->jktexts[8], Rect(370, 430, 65, 35), 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
-	dismissButton = std::make_shared<CButton>(Point(454, 429), "hsbtns2.def", CButton::tooltip(heroscrn[28]), [=](){ dismissCurrent(); }, SDLK_d);
+	dismissButton = std::make_shared<CButton>(Point(454, 429), "hsbtns2.def", CButton::tooltip(heroscrn[28]), [=](){ dismissCurrent(); }, EShortcut::HERO_DISMISS);
 
 	questlogLabel = std::make_shared<CTextBox>(CGI->generaltexth->jktexts[9], Rect(510, 430, 65, 35), 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
-	questlogButton = std::make_shared<CButton>(Point(314, 429), "hsbtns4.def", CButton::tooltip(heroscrn[0]), [=](){ LOCPLINT->showQuestLog(); }, SDLK_q);
+	questlogButton = std::make_shared<CButton>(Point(314, 429), "hsbtns4.def", CButton::tooltip(heroscrn[0]), [=](){ LOCPLINT->showQuestLog(); }, EShortcut::ADVENTURE_QUEST_LOG);
 
 	formations = std::make_shared<CToggleGroup>(0);
-	formations->addToggle(0, std::make_shared<CToggleButton>(Point(481, 483), "hsbtns6.def", std::make_pair(heroscrn[23], heroscrn[29]), 0, SDLK_t));
-	formations->addToggle(1, std::make_shared<CToggleButton>(Point(481, 519), "hsbtns7.def", std::make_pair(heroscrn[24], heroscrn[30]), 0, SDLK_l));
+	formations->addToggle(0, std::make_shared<CToggleButton>(Point(481, 483), "hsbtns6.def", std::make_pair(heroscrn[23], heroscrn[29]), 0, EShortcut::HERO_TIGHT_FORMATION));
+	formations->addToggle(1, std::make_shared<CToggleButton>(Point(481, 519), "hsbtns7.def", std::make_pair(heroscrn[24], heroscrn[30]), 0, EShortcut::HERO_LOOSE_FORMATION));
 
 	if(hero->commander)
 	{
-		commanderButton = std::make_shared<CButton>(Point(317, 18), "buttons/commander", CButton::tooltipLocalized("vcmi.heroWindow.openCommander"), [&](){ commanderWindow(); }, SDLK_c);
+		commanderButton = std::make_shared<CButton>(Point(317, 18), "buttons/commander", CButton::tooltipLocalized("vcmi.heroWindow.openCommander"), [&](){ commanderWindow(); }, EShortcut::HERO_COMMANDER);
 	}
 
 	//right list of heroes
@@ -223,7 +224,7 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded)
 	specImage->setFrame(curHero->type->imageIndex);
 	specName->setText(curHero->type->getSpecialtyNameTranslated());
 
-	tacticsButton = std::make_shared<CToggleButton>(Point(539, 483), "hsbtns8.def", std::make_pair(heroscrn[26], heroscrn[31]), 0, SDLK_b);
+	tacticsButton = std::make_shared<CToggleButton>(Point(539, 483), "hsbtns8.def", std::make_pair(heroscrn[26], heroscrn[31]), 0, EShortcut::HERO_TOGGLE_TACTICS);
 	tacticsButton->addHoverText(CButton::HIGHLIGHTED, CGI->generaltexth->heroscrn[25]);
 
 	dismissButton->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->heroscrn[16]) % curHero->getNameTranslated() % curHero->type->heroClass->getNameTranslated()));
@@ -244,7 +245,7 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded)
 		}
 		if(!arts)
 		{
-			arts = std::make_shared<CArtifactsOfHero>(Point(-65, -8), true);
+			arts = std::make_shared<CArtifactsOfHeroMain>(Point(-65, -8));
 			arts->setHero(curHero);
 			addSet(arts);
 		}
@@ -354,25 +355,23 @@ void CHeroWindow::dismissCurrent()
 
 void CHeroWindow::commanderWindow()
 {
-	//bool artSelected = false;
-	std::shared_ptr<CArtifactsOfHero::SCommonPart> commonInfo = getCommonPart();
+	const auto pickedArtInst = getPickedArtifact();
+	const auto hero = getHeroPickedArtifact();
 
-	if(const CArtifactInstance *art = commonInfo->src.art)
+	if(pickedArtInst)
 	{
-		const CGHeroInstance *srcHero = commonInfo->src.AOH->getHero();
-		//artSelected = true;
-		const auto freeSlot = ArtifactUtils::getArtAnyPosition(curHero->commander, art->artType->getId());
+		const auto freeSlot = ArtifactUtils::getArtAnyPosition(curHero->commander, pickedArtInst->getTypeId());
 		if(freeSlot < ArtifactPosition::COMMANDER_AFTER_LAST) //we don't want to put it in commander's backpack!
 		{
-			ArtifactLocation src(srcHero, commonInfo->src.slotID);
+			ArtifactLocation src(hero, ArtifactPosition::TRANSITION_POS);
 			ArtifactLocation dst(curHero->commander.get(), freeSlot);
 
-			if(art->canBePutAt(dst, true))
+			if(pickedArtInst->canBePutAt(dst, true))
 			{	//equip clicked stack
 				if(dst.getArt())
 				{
-					LOCPLINT->cb->swapArtifacts (dst, ArtifactLocation(srcHero,
-						ArtifactUtils::getArtBackpackPosition(srcHero, art->getTypeId())));
+					LOCPLINT->cb->swapArtifacts(dst, ArtifactLocation(hero,
+						ArtifactUtils::getArtBackpackPosition(hero, pickedArtInst->getTypeId())));
 				}
 				LOCPLINT->cb->swapArtifacts(src, dst);
 			}

+ 3 - 3
client/windows/CHeroWindow.h

@@ -10,7 +10,7 @@
 #pragma once
 
 #include "../../lib/HeroBonus.h"
-#include "../widgets/CArtifactHolder.h"
+#include "../widgets/CWindowWithArtifacts.h"
 #include "../widgets/CGarrisonInt.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -25,7 +25,7 @@ class CHeroWindow;
 class LClickableAreaHero;
 class LRClickableAreaWText;
 class LRClickableAreaWTextComp;
-class CArtifactsOfHero;
+class CArtifactsOfHeroMain;
 class MoraleLuckBox;
 class CToggleButton;
 class CToggleGroup;
@@ -105,7 +105,7 @@ class CHeroWindow : public CStatusbarWindow, public CGarrisonHolder, public CWin
 	std::shared_ptr<CToggleGroup> formations;
 
 	std::shared_ptr<CGarrisonInt> garr;
-	std::shared_ptr<CArtifactsOfHero> arts;
+	std::shared_ptr<CArtifactsOfHeroMain> arts;
 
 	std::vector<std::shared_ptr<CLabel>> labels;
 

+ 8 - 8
client/windows/CKingdomInterface.cpp

@@ -17,6 +17,7 @@
 #include "../CPlayerInterface.h"
 #include "../adventureMap/CResDataBar.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/TextControls.h"
 #include "../widgets/MiscWidgets.h"
@@ -613,15 +614,14 @@ void CKingdomInterface::generateButtons()
 
 	//Main control buttons
 	btnHeroes = std::make_shared<CButton>(Point(748, 28+footerPos), "OVBUTN1.DEF", CButton::tooltip(CGI->generaltexth->overview[11], CGI->generaltexth->overview[6]),
-		std::bind(&CKingdomInterface::activateTab, this, 0), SDLK_h);
+		std::bind(&CKingdomInterface::activateTab, this, 0), EShortcut::KINGDOM_HEROES_TAB);
 	btnHeroes->block(true);
 
 	btnTowns = std::make_shared<CButton>(Point(748, 64+footerPos), "OVBUTN6.DEF", CButton::tooltip(CGI->generaltexth->overview[12], CGI->generaltexth->overview[7]),
-		std::bind(&CKingdomInterface::activateTab, this, 1), SDLK_t);
+		std::bind(&CKingdomInterface::activateTab, this, 1), EShortcut::KINGDOM_TOWNS_TAB);
 
 	btnExit = std::make_shared<CButton>(Point(748,99+footerPos), "OVBUTN1.DEF", CButton::tooltip(CGI->generaltexth->allTexts[600]),
-		std::bind(&CKingdomInterface::close, this), SDLK_RETURN);
-	btnExit->assignedKeys.insert(SDLK_ESCAPE);
+		std::bind(&CKingdomInterface::close, this), EShortcut::GLOBAL_RETURN);
 	btnExit->setImageOrder(3, 4, 5, 6);
 
 	//Object list control buttons
@@ -826,7 +826,7 @@ public:
 		background = std::make_shared<CAnimImage>("OVSLOT", 4);
 		pos = background->pos;
 		for(int i=0; i<9; i++)
-			arts.push_back(std::make_shared<CHeroArtPlace>(Point(270+i*48, 65)));
+			arts.push_back(std::make_shared<CHeroArtPlace>(Point(269+i*48, 66)));
 	}
 };
 
@@ -846,7 +846,7 @@ public:
 		btnLeft = std::make_shared<CButton>(Point(269, 66), "HSBTNS3", CButton::tooltip(), 0);
 		btnRight = std::make_shared<CButton>(Point(675, 66), "HSBTNS5", CButton::tooltip(), 0);
 		for(int i=0; i<8; i++)
-			arts.push_back(std::make_shared<CHeroArtPlace>(Point(295+i*48, 65)));
+			arts.push_back(std::make_shared<CHeroArtPlace>(Point(294+i*48, 66)));
 	}
 };
 
@@ -872,7 +872,7 @@ CHeroItem::CHeroItem(const CGHeroInstance * Hero)
 	assert(arts1->arts.size() == 9);
 	assert(arts2->arts.size() == 9);
 
-	CArtifactsOfHero::ArtPlaceMap arts =
+	CArtifactsOfHeroMain::ArtPlaceMap arts =
 	{
 		{ArtifactPosition::HEAD, arts1->arts[0]},
 		{ArtifactPosition::SHOULDERS,arts1->arts[1]},
@@ -896,7 +896,7 @@ CHeroItem::CHeroItem(const CGHeroInstance * Hero)
 	};
 
 
-	heroArts = std::make_shared<CArtifactsOfHero>(arts, backpack->arts, backpack->btnLeft, backpack->btnRight, true);
+	heroArts = std::make_shared<CArtifactsOfHeroKingdom>(arts, backpack->arts, backpack->btnLeft, backpack->btnRight);
 	heroArts->setHero(hero);
 
 	artsTabs = std::make_shared<CTabbedInt>(std::bind(&CHeroItem::onTabSelected, this, _1));

+ 2 - 2
client/windows/CKingdomInterface.h

@@ -9,7 +9,7 @@
  */
 #pragma once
 
-#include "../widgets/CArtifactHolder.h"
+#include "../widgets/CWindowWithArtifacts.h"
 #include "../widgets/CGarrisonInt.h"
 
 class CButton;
@@ -309,7 +309,7 @@ class CHeroItem : public CIntObject, public CGarrisonHolder
 	std::shared_ptr<CIntObject> onTabSelected(size_t index);
 
 public:
-	std::shared_ptr<CArtifactsOfHero> heroArts;
+	std::shared_ptr<CArtifactsOfHeroKingdom> heroArts;
 
 	void updateGarrisons() override;
 

+ 2 - 2
client/windows/CPuzzleWindow.cpp

@@ -16,6 +16,7 @@
 #include "../adventureMap/CResDataBar.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/TextAlignment.h"
+#include "../gui/Shortcut.h"
 #include "../mapView/MapView.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/Images.h"
@@ -35,8 +36,7 @@ CPuzzleWindow::CPuzzleWindow(const int3 & GrailPos, double discoveredRatio)
 
 	CCS->soundh->playSound(soundBase::OBELISK);
 
-	quitb = std::make_shared<CButton>(Point(670, 538), "IOK6432.DEF", CButton::tooltip(CGI->generaltexth->allTexts[599]), std::bind(&CPuzzleWindow::close, this), SDLK_RETURN);
-	quitb->assignedKeys.insert(SDLK_ESCAPE);
+	quitb = std::make_shared<CButton>(Point(670, 538), "IOK6432.DEF", CButton::tooltip(CGI->generaltexth->allTexts[599]), std::bind(&CPuzzleWindow::close, this), EShortcut::GLOBAL_RETURN);
 	quitb->setBorderColor(Colors::METALLIC_GOLD);
 
 	mapView = std::make_shared<PuzzleMapView>(Point(8,9), Point(591, 544), grailPos);

+ 2 - 1
client/windows/CQuestLog.cpp

@@ -14,6 +14,7 @@
 #include "../CPlayerInterface.h"
 
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../widgets/CComponent.h"
 #include "../adventureMap/CAdventureMapInterface.h"
 #include "../widgets/Buttons.h"
@@ -127,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), "IOKAY.DEF", CGI->generaltexth->zelp[445], std::bind(&CQuestLog::close, this), SDLK_RETURN);
+	ok = std::make_shared<CButton>(Point(539, 398), "IOKAY.DEF", CGI->generaltexth->zelp[445], std::bind(&CQuestLog::close, this), EShortcut::GLOBAL_ACCEPT);
 	// 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), "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"));

+ 14 - 42
client/windows/CSpellWindow.cpp

@@ -23,6 +23,7 @@
 
 #include "../battle/BattleInterface.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/TextControls.h"
@@ -409,27 +410,24 @@ void CSpellWindow::turnPageRight()
 		CCS->videoh->openAndPlayVideo("PGTRNRGH.SMK", pos.x+13, pos.y+15);
 }
 
-void CSpellWindow::keyPressed(const SDL_Keycode & key)
+void CSpellWindow::keyPressed(EShortcut key)
 {
-	if(key == SDLK_ESCAPE ||  key == SDLK_RETURN)
+	switch(key)
 	{
-		fexitb();
-		return;
-	}
-	else
-	{
-		switch(key)
-		{
-		case SDLK_LEFT:
+		case EShortcut::GLOBAL_RETURN:
+			fexitb();
+			break;
+
+		case EShortcut::MOVE_LEFT:
 			fLcornerb();
 			break;
-		case SDLK_RIGHT:
+		case EShortcut::MOVE_RIGHT:
 			fRcornerb();
 			break;
-		case SDLK_UP:
-		case SDLK_DOWN:
+		case EShortcut::MOVE_UP:
+		case EShortcut::MOVE_DOWN:
 		{
-			bool down = key == SDLK_DOWN;
+			bool down = key == EShortcut::MOVE_DOWN;
 			static const int schoolsOrder[] = { 0, 3, 1, 2, 4 };
 			int index = -1;
 			while(schoolsOrder[++index] != selectedTab);
@@ -439,38 +437,12 @@ void CSpellWindow::keyPressed(const SDL_Keycode & key)
 				selectSchool(schoolsOrder[index]);
 			break;
 		}
-		case SDLK_c:
+		case EShortcut::SPELLBOOK_TAB_COMBAT:
 			fbattleSpellsb();
 			break;
-		case SDLK_a:
+		case EShortcut::SPELLBOOK_TAB_ADVENTURE:
 			fadvSpellsb();
 			break;
-		default://to get rid of warnings
-			break;
-		}
-
-		//alt + 1234567890-= casts spell from 1 - 12 slot
-		if(GH.isKeyboardAltDown())
-		{
-			SDL_Keycode hlpKey = key;
-			if(CGuiHandler::isNumKey(hlpKey, false))
-			{
-				if(hlpKey == SDLK_KP_PLUS)
-					hlpKey = SDLK_EQUALS;
-				else
-					hlpKey = CGuiHandler::numToDigit(hlpKey);
-			}
-
-			static const SDL_Keycode spellSelectors[] = {SDLK_1, SDLK_2, SDLK_3, SDLK_4, SDLK_5, SDLK_6, SDLK_7, SDLK_8, SDLK_9, SDLK_0, SDLK_MINUS, SDLK_EQUALS};
-
-			int index = -1;
-			while(++index < std::size(spellSelectors) && spellSelectors[index] != hlpKey);
-			if(index >= std::size(spellSelectors))
-				return;
-
-			//try casting spell
-			spellAreas[index]->clickLeft(false, true);
-		}
 	}
 }
 

+ 1 - 1
client/windows/CSpellWindow.h

@@ -112,7 +112,7 @@ public:
 	void selectSchool(int school); //schools: 0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic, 4 - all schools
 	int pagesWithinCurrentTab();
 
-	void keyPressed(const SDL_Keycode & key) override;
+	void keyPressed(EShortcut key) override;
 
 	void show(SDL_Surface * to) override;
 };

+ 71 - 98
client/windows/CTradeWindow.cpp

@@ -15,6 +15,7 @@
 #include "../widgets/Images.h"
 #include "../renderSDL/SDL_Extensions.h"
 #include "../gui/TextAlignment.h"
+#include "../gui/Shortcut.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/TextControls.h"
 #include "../windows/InfoWindows.h"
@@ -179,24 +180,26 @@ void CTradeWindow::CTradeableItem::clickLeft(tribool down, bool previousState)
 		if(type == ARTIFACT_PLACEHOLDER)
 		{
 			CAltarWindow *aw = static_cast<CAltarWindow *>(mw);
-			if(const CArtifactInstance *movedArt = aw->arts->commonInfo->src.art)
+			const auto pickedArtInst = aw->getPickedArtifact();
+
+			auto artifactsOfHero = std::dynamic_pointer_cast<CArtifactsOfHeroAltar>(aw->arts);
+			if(pickedArtInst)
 			{
-				aw->moveFromSlotToAltar(aw->arts->commonInfo->src.slotID, this->shared_from_this(), movedArt);
+				artifactsOfHero->pickedArtMoveToAltar(ArtifactPosition::TRANSITION_POS);
+				aw->moveArtToAltar(this->shared_from_this(), pickedArtInst);
 			}
 			else if(const CArtifactInstance *art = getArtInstance())
 			{
-				aw->arts->commonInfo->src.AOH = aw->arts.get();
-				aw->arts->commonInfo->src.art = art;
-				aw->arts->commonInfo->src.slotID = aw->hero->getArtPos(art);
-				aw->arts->markPossibleSlots(art);
-
-				//aw->arts->commonInfo->dst.AOH = aw->arts;
-				CCS->curh->dragAndDropCursor("artifact", art->artType->getIconIndex());
-
-				aw->arts->artifactsOnAltar.erase(art);
+				const auto hero = artifactsOfHero->getHero();
+				const auto slot = hero->getSlotByInstance(art);
+				assert(slot != ArtifactPosition::PRE_FIRST);
+				LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero, slot),
+					ArtifactLocation(hero, ArtifactPosition::TRANSITION_POS));
+				artifactsOfHero->pickedArtFromSlot = slot;
+				artifactsOfHero->artifactsOnAltar.erase(art);
 				setID(-1);
 				subtitle.clear();
-				aw->deal->block(!aw->arts->artifactsOnAltar.size());
+				aw->deal->block(!artifactsOfHero->artifactsOnAltar.size());
 			}
 
 			aw->calcTotalExp();
@@ -387,18 +390,25 @@ void CTradeWindow::initItems(bool Left)
 		}
 		else //ARTIFACT_EXP
 		{
-			xOffset = -363;
+			xOffset = -365;
 			yOffset = -12;
 		}
 
-		arts = std::make_shared<CArtifactsOfHero>(Point(xOffset, yOffset), true);
-		arts->recActions = 255-DISPOSE;
-		arts->setHero(hero);
-		arts->allowedAssembling = false;
-		addSet(arts);
-
 		if(mode == EMarketMode::ARTIFACT_RESOURCE)
-			arts->highlightModeCallback = std::bind(&CTradeWindow::artifactSelected, this, _1);
+		{
+			auto artifactsOfHero = std::make_shared<CArtifactsOfHeroMarket>(Point(xOffset, yOffset));
+			artifactsOfHero->selectArtCallback = std::bind(&CTradeWindow::artifactSelected, this, _1);
+			artifactsOfHero->setHero(hero);
+			addSet(artifactsOfHero);
+			arts = artifactsOfHero;
+		}
+		else
+		{
+			auto artifactsOfHero = std::make_shared<CArtifactsOfHeroAltar>(Point(xOffset, yOffset));
+			artifactsOfHero->setHero(hero);
+			addSet(artifactsOfHero);
+			arts = artifactsOfHero;
+		}
 	}
 	else
 	{
@@ -630,8 +640,8 @@ void CTradeWindow::setMode(EMarketMode::EMarketMode Mode)
 void CTradeWindow::artifactSelected(CHeroArtPlace *slot)
 {
 	assert(mode == EMarketMode::ARTIFACT_RESOURCE);
-	items[1][0]->setArtInstance(slot->ourArt);
-	if(slot->ourArt && !slot->locked)
+	items[1][0]->setArtInstance(slot->getArt());
+	if(slot->getArt())
 		hLeft = items[1][0];
 	else
 		hLeft = nullptr;
@@ -664,7 +674,7 @@ CMarketplaceWindow::CMarketplaceWindow(const IMarket * Market, const CGHeroInsta
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 
 	madeTransaction = false;
-	bool sliderNeeded = true;
+	bool sliderNeeded = (mode != EMarketMode::RESOURCE_ARTIFACT && mode != EMarketMode::ARTIFACT_RESOURCE);
 
 	statusBar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26));
 
@@ -679,7 +689,6 @@ CMarketplaceWindow::CMarketplaceWindow(const IMarket * Market, const CGHeroInsta
 			break;
 		case EMarketMode::RESOURCE_ARTIFACT:
 			title = (*CGI->townh)[o->subID]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated();
-			sliderNeeded = false;
 			break;
 		case EMarketMode::ARTIFACT_RESOURCE:
 			title = (*CGI->townh)[o->subID]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated();
@@ -687,34 +696,15 @@ CMarketplaceWindow::CMarketplaceWindow(const IMarket * Market, const CGHeroInsta
 			// create image that copies part of background containing slot MISC_1 into position of slot MISC_5
 			// this is workaround for bug in H3 files where this slot for ragdoll on this screen is missing
 			images.push_back(std::make_shared<CPicture>(background->getSurface(), Rect(20, 187, 47, 47), 18, 339 ));
-			sliderNeeded = false;
 			break;
 		default:
 			title = CGI->generaltexth->allTexts[158];
 			break;
 		}
 	}
-	else
+	else if(auto * o = dynamic_cast<const CGMarket *>(market))
 	{
-		if(auto * o = dynamic_cast<const CGObjectInstance *>(market))
-		{
-			switch(o->ID)
-			{
-			case Obj::BLACK_MARKET:
-				title = CGI->generaltexth->allTexts[349];
-				sliderNeeded = false;
-				break;
-			case Obj::TRADING_POST:
-				title = CGI->generaltexth->allTexts[159];
-				break;
-			case Obj::TRADING_POST_SNOW:
-				title = CGI->generaltexth->allTexts[159];
-				break;
-			default:
-				title = o->getObjectName();
-				break;
-			}
-		}
+		title = o->title;
 	}
 
 	titleLabel = std::make_shared<CLabel>(300, 27, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, title);
@@ -722,8 +712,7 @@ CMarketplaceWindow::CMarketplaceWindow(const IMarket * Market, const CGHeroInsta
 	initItems(false);
 	initItems(true);
 
-	ok = std::make_shared<CButton>(Point(516, 520), "IOK6432.DEF", CGI->generaltexth->zelp[600], [&](){ close(); }, SDLK_RETURN);
-	ok->assignedKeys.insert(SDLK_ESCAPE);
+	ok = std::make_shared<CButton>(Point(516, 520), "IOK6432.DEF", CGI->generaltexth->zelp[600], [&](){ close(); }, EShortcut::GLOBAL_RETURN);
 	deal = std::make_shared<CButton>(Point(307, 520), "TPMRKB.DEF", CGI->generaltexth->zelp[595], [&](){ makeDeal(); } );
 	deal->block(true);
 
@@ -861,7 +850,7 @@ void CMarketplaceWindow::selectionChanged(bool side)
 		readyToTrade = readyToTrade && (hLeft->id != hRight->id); //for resource trade, two DIFFERENT resources must be selected
 
 	if(mode == EMarketMode::ARTIFACT_RESOURCE && !hLeft)
-		arts->unmarkSlots(false);
+		arts->unmarkSlots();
 
 	if(readyToTrade)
 	{
@@ -1164,8 +1153,7 @@ CAltarWindow::CAltarWindow(const IMarket * Market, const CGHeroInstance * Hero,
 
 	statusBar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26));
 
-	ok = std::make_shared<CButton>(Point(516, 520), "IOK6432.DEF", CGI->generaltexth->zelp[568], [&](){ close();}, SDLK_RETURN);
-	ok->assignedKeys.insert(SDLK_ESCAPE);
+	ok = std::make_shared<CButton>(Point(516, 520), "IOK6432.DEF", CGI->generaltexth->zelp[568], [&](){ close();}, EShortcut::GLOBAL_RETURN);
 
 	deal = std::make_shared<CButton>(Point(269, 520), "ALTSACR.DEF", CGI->generaltexth->zelp[585], std::bind(&CAltarWindow::makeDeal,this));
 
@@ -1253,13 +1241,15 @@ void CAltarWindow::makeDeal()
 	else
 	{
 		std::vector<ui32> positions;
-		for(const CArtifactInstance *art : arts->artifactsOnAltar) //sacrifice each artifact on the list
+		auto artifactsOfHero = std::dynamic_pointer_cast<CArtifactsOfHeroAltar>(arts);
+		for(const CArtifactInstance * art : artifactsOfHero->artifactsOnAltar)
 		{
-			positions.push_back(hero->getArtPos(art));
+			positions.push_back(hero->getSlotByInstance(art));
 		}
+		std::sort(positions.begin(), positions.end(), std::greater<>());
 
 		LOCPLINT->cb->trade(market, mode, positions, {}, {}, hero);
-		arts->artifactsOnAltar.clear();
+		artifactsOfHero->artifactsOnAltar.clear();
 
 		for(auto item : items[0])
 		{
@@ -1267,7 +1257,6 @@ void CAltarWindow::makeDeal()
 			item->subtitle = "";
 		}
 
-		arts->commonInfo->reset();
 		//arts->scrollBackpack(0);
 		deal->block(true);
 	}
@@ -1297,12 +1286,13 @@ void CAltarWindow::SacrificeAll()
 	}
 	else
 	{
-		for(auto i = hero->artifactsWorn.cbegin(); i != hero->artifactsWorn.cend(); i++)
+		auto artifactsOfHero = std::dynamic_pointer_cast<CArtifactsOfHeroAltar>(arts);
+		for(const auto & aw : artifactsOfHero->visibleArtSet.artifactsWorn)
 		{
-			if(!i->second.locked) //ignore locks from assembled artifacts
-				moveFromSlotToAltar(i->first, nullptr, i->second.artifact);
+			if(!aw.second.locked)
+				moveArtToAltar(nullptr, aw.second.artifact);
 		}
-
+		artifactsOfHero->updateWornSlots();
 		SacrificeBackpack();
 	}
 	redraw();
@@ -1417,7 +1407,8 @@ void CAltarWindow::calcTotalExp()
 	}
 	else
 	{
-		for(const CArtifactInstance *art : arts->artifactsOnAltar)
+		auto artifactsOfHero = std::dynamic_pointer_cast<CArtifactsOfHeroAltar>(arts);
+		for(const CArtifactInstance * art : artifactsOfHero->artifactsOnAltar)
 		{
 			int dmp, valOfArt;
 			market->getOffer(art->artType->getId(), 0, dmp, valOfArt, mode);
@@ -1461,21 +1452,12 @@ int CAltarWindow::firstFreeSlot()
 
 void CAltarWindow::SacrificeBackpack()
 {
-	std::multiset<const CArtifactInstance *> toOmmit = arts->artifactsOnAltar;
-
-	for (auto & elem : hero->artifactsInBackpack)
+	auto artsAltar = std::dynamic_pointer_cast<CArtifactsOfHeroAltar>(arts);
+	while(!artsAltar->visibleArtSet.artifactsInBackpack.empty())
 	{
-
-		if(vstd::contains(toOmmit, elem.artifact))
-		{
-			toOmmit -= elem.artifact;
-			continue;
-		}
-
-		putOnAltar(nullptr, elem.artifact);
-	}
-
-	arts->scrollBackpack(0);
+		if(!putOnAltar(nullptr, artsAltar->visibleArtSet.artifactsInBackpack[0].artifact))
+			break;
+	};
 	calcTotalExp();
 }
 
@@ -1487,15 +1469,18 @@ void CAltarWindow::artifactPicked()
 void CAltarWindow::showAll(SDL_Surface * to)
 {
 	CTradeWindow::showAll(to);
-	if(mode == EMarketMode::ARTIFACT_EXP && arts && arts->commonInfo->src.art)
+	if(mode == EMarketMode::ARTIFACT_EXP && arts)
 	{
-		artIcon->setFrame(arts->commonInfo->src.art->artType->getIconIndex());
-		artIcon->showAll(to);
+		if(auto pickedArt = arts->getPickedArtifact())
+		{
+			artIcon->setFrame(pickedArt->artType->getIconIndex());
+			artIcon->showAll(to);
 
-		int dmp, val;
-		market->getOffer(arts->commonInfo->src.art->artType->getId(), 0, dmp, val, EMarketMode::ARTIFACT_EXP);
-		val = static_cast<int>(hero->calculateXp(val));
-		printAtMiddleLoc(std::to_string(val), 304, 498, FONT_SMALL, Colors::WHITE, to);
+			int dmp, val;
+			market->getOffer(pickedArt->getTypeId(), 0, dmp, val, EMarketMode::ARTIFACT_EXP);
+			val = static_cast<int>(hero->calculateXp(val));
+			printAtMiddleLoc(std::to_string(val), 304, 498, FONT_SMALL, Colors::WHITE, to);
+		}
 	}
 }
 
@@ -1522,7 +1507,9 @@ bool CAltarWindow::putOnAltar(std::shared_ptr<CTradeableItem> altarSlot, const C
 	market->getOffer(art->artType->getId(), 0, dmp, val, EMarketMode::ARTIFACT_EXP);
 	val = static_cast<int>(hero->calculateXp(val));
 
-	arts->artifactsOnAltar.insert(art);
+	auto artsAltar = std::dynamic_pointer_cast<CArtifactsOfHeroAltar>(arts);
+	artsAltar->artifactsOnAltar.insert(art);
+	artsAltar->deleteFromVisible(art);
 	altarSlot->setArtInstance(art);
 	altarSlot->subtitle = std::to_string(val);
 
@@ -1530,25 +1517,11 @@ bool CAltarWindow::putOnAltar(std::shared_ptr<CTradeableItem> altarSlot, const C
 	return true;
 }
 
-void CAltarWindow::moveFromSlotToAltar(ArtifactPosition slotID, std::shared_ptr<CTradeableItem> altarSlot, const CArtifactInstance *art)
+void CAltarWindow::moveArtToAltar(std::shared_ptr<CTradeableItem> altarSlot, const CArtifactInstance *art)
 {
-	auto freeBackpackSlot = ArtifactPosition((si32)hero->artifactsInBackpack.size() + GameConstants::BACKPACK_START);
-	if(arts->commonInfo->src.art)
-	{
-		arts->commonInfo->dst.slotID = freeBackpackSlot;
-		arts->commonInfo->dst.AOH = arts.get();
-	}
-
 	if(putOnAltar(altarSlot, art))
 	{
-		if(slotID < GameConstants::BACKPACK_START)
-			LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero, slotID), ArtifactLocation(hero, freeBackpackSlot));
-		else
-		{
-			arts->commonInfo->src.clear();
-			arts->commonInfo->dst.clear();
-			CCS->curh->dragAndDropCursor(nullptr);
-			arts->unmarkSlots(false);
-		}
+		CCS->curh->dragAndDropCursor(nullptr);
+		arts->unmarkSlots();
 	}
 }

+ 3 - 3
client/windows/CTradeWindow.h

@@ -9,7 +9,7 @@
  */
 #pragma once
 
-#include "../widgets/CArtifactHolder.h"
+#include "../widgets/CWindowWithArtifacts.h"
 #include "CWindowObject.h"
 #include "../../lib/FunctionList.h"
 
@@ -67,7 +67,7 @@ public:
 	const IMarket * market;
 	const CGHeroInstance * hero;
 
-	std::shared_ptr<CArtifactsOfHero> arts;
+	std::shared_ptr<CArtifactsOfHeroBase> arts;
 	//all indexes: 1 = left, 0 = right
 	std::array<std::vector<std::shared_ptr<CTradeableItem>>, 2> items;
 
@@ -186,5 +186,5 @@ public:
 
 	void artifactPicked();
 	int firstFreeSlot();
-	void moveFromSlotToAltar(ArtifactPosition slotID, std::shared_ptr<CTradeableItem>, const CArtifactInstance * art);
+	void moveArtToAltar(std::shared_ptr<CTradeableItem>, const CArtifactInstance * art);
 };

+ 3 - 2
client/windows/CreaturePurchaseCard.cpp

@@ -15,6 +15,7 @@
 #include "CCreatureWindow.h"
 
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../gui/TextAlignment.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/TextControls.h"
@@ -32,12 +33,12 @@ void CreaturePurchaseCard::initButtons()
 
 void CreaturePurchaseCard::initMaxButton()
 {
-	maxButton = std::make_shared<CButton>(Point(pos.x + 52, pos.y + 180), "QuickRecruitmentWindow/QuickRecruitmentAllButton.def", CButton::tooltip(), std::bind(&CSlider::moveToMax,slider), SDLK_LSHIFT);
+	maxButton = std::make_shared<CButton>(Point(pos.x + 52, pos.y + 180), "QuickRecruitmentWindow/QuickRecruitmentAllButton.def", CButton::tooltip(), std::bind(&CSlider::moveToMax,slider), EShortcut::RECRUITMENT_MAX);
 }
 
 void CreaturePurchaseCard::initMinButton()
 {
-	minButton = std::make_shared<CButton>(Point(pos.x, pos.y + 180), "QuickRecruitmentWindow/QuickRecruitmentNoneButton.def", CButton::tooltip(), std::bind(&CSlider::moveToMin,slider), SDLK_LCTRL);
+	minButton = std::make_shared<CButton>(Point(pos.x, pos.y + 180), "QuickRecruitmentWindow/QuickRecruitmentNoneButton.def", CButton::tooltip(), std::bind(&CSlider::moveToMin,slider), EShortcut::RECRUITMENT_MIN);
 }
 
 void CreaturePurchaseCard::initCreatureSwitcherButton()

+ 36 - 41
client/windows/GUIClasses.cpp

@@ -28,6 +28,7 @@
 #include "../gui/CGuiHandler.h"
 #include "../gui/CursorHandler.h"
 #include "../gui/TextAlignment.h"
+#include "../gui/Shortcut.h"
 
 #include "../widgets/CComponent.h"
 #include "../widgets/MiscWidgets.h"
@@ -210,9 +211,9 @@ CRecruitmentWindow::CRecruitmentWindow(const CGDwelling * Dwelling, int Level, c
 
 	slider = std::make_shared<CSlider>(Point(176,279),135,std::bind(&CRecruitmentWindow::sliderMoved,this, _1),0,0,0,true);
 
-	maxButton = std::make_shared<CButton>(Point(134, 313), "IRCBTNS.DEF", CGI->generaltexth->zelp[553], std::bind(&CSlider::moveToMax, slider), SDLK_m);
-	buyButton = std::make_shared<CButton>(Point(212, 313), "IBY6432.DEF", CGI->generaltexth->zelp[554], std::bind(&CRecruitmentWindow::buy, this), SDLK_RETURN);
-	cancelButton = std::make_shared<CButton>(Point(290, 313), "ICN6432.DEF", CGI->generaltexth->zelp[555], std::bind(&CRecruitmentWindow::close, this), SDLK_ESCAPE);
+	maxButton = std::make_shared<CButton>(Point(134, 313), "IRCBTNS.DEF", CGI->generaltexth->zelp[553], std::bind(&CSlider::moveToMax, slider), EShortcut::RECRUITMENT_MAX);
+	buyButton = std::make_shared<CButton>(Point(212, 313), "IBY6432.DEF", CGI->generaltexth->zelp[554], std::bind(&CRecruitmentWindow::buy, this), EShortcut::GLOBAL_ACCEPT);
+	cancelButton = std::make_shared<CButton>(Point(290, 313), "ICN6432.DEF", CGI->generaltexth->zelp[555], std::bind(&CRecruitmentWindow::close, this), EShortcut::GLOBAL_CANCEL);
 
 	title = std::make_shared<CLabel>(243, 32, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW);
 	availableValue = std::make_shared<CLabel>(205, 253, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
@@ -313,8 +314,8 @@ CSplitWindow::CSplitWindow(const CCreature * creature, std::function<void(int, i
 	int leftMax = total - rightMin;
 	int rightMax = total - leftMin;
 
-	ok = std::make_shared<CButton>(Point(20, 263), "IOK6432", CButton::tooltip(), std::bind(&CSplitWindow::apply, this), SDLK_RETURN);
-	cancel = std::make_shared<CButton>(Point(214, 263), "ICN6432", CButton::tooltip(), std::bind(&CSplitWindow::close, this), SDLK_ESCAPE);
+	ok = std::make_shared<CButton>(Point(20, 263), "IOK6432", CButton::tooltip(), std::bind(&CSplitWindow::apply, this), EShortcut::GLOBAL_ACCEPT);
+	cancel = std::make_shared<CButton>(Point(214, 263), "ICN6432", CButton::tooltip(), std::bind(&CSplitWindow::close, this), EShortcut::GLOBAL_CANCEL);
 
 	int sliderPosition = total - leftMin - rightMin;
 
@@ -403,7 +404,7 @@ CLevelWindow::CLevelWindow(const CGHeroInstance * hero, PrimarySkill::PrimarySki
 	}
 
 	portrait = std::make_shared<CAnimImage>("PortraitsLarge", hero->portrait, 0, 170, 66);
-	ok = std::make_shared<CButton>(Point(297, 413), "IOKAY", CButton::tooltip(), std::bind(&CLevelWindow::close, this), SDLK_RETURN);
+	ok = std::make_shared<CButton>(Point(297, 413), "IOKAY", CButton::tooltip(), std::bind(&CLevelWindow::close, this), EShortcut::GLOBAL_ACCEPT);
 
 	//%s has gained a level.
 	mainTitle = std::make_shared<CLabel>(192, 33, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, boost::str(boost::format(CGI->generaltexth->allTexts[444]) % hero->getNameTranslated()));
@@ -459,9 +460,9 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj)
 	rumor = std::make_shared<CTextBox>(rumorText, Rect(32, 190, 330, 68), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
 
 	statusbar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26));
-	cancel = std::make_shared<CButton>(Point(310, 428), "ICANCEL.DEF", CButton::tooltip(CGI->generaltexth->tavernInfo[7]), std::bind(&CTavernWindow::close, this), SDLK_ESCAPE);
-	recruit = std::make_shared<CButton>(Point(272, 355), "TPTAV01.DEF", CButton::tooltip(), std::bind(&CTavernWindow::recruitb, this), SDLK_RETURN);
-	thiefGuild = std::make_shared<CButton>(Point(22, 428), "TPTAV02.DEF", CButton::tooltip(CGI->generaltexth->tavernInfo[5]), std::bind(&CTavernWindow::thievesguildb, this), SDLK_t);
+	cancel = std::make_shared<CButton>(Point(310, 428), "ICANCEL.DEF", CButton::tooltip(CGI->generaltexth->tavernInfo[7]), std::bind(&CTavernWindow::close, this), EShortcut::GLOBAL_CANCEL);
+	recruit = std::make_shared<CButton>(Point(272, 355), "TPTAV01.DEF", CButton::tooltip(), std::bind(&CTavernWindow::recruitb, this), EShortcut::GLOBAL_ACCEPT);
+	thiefGuild = std::make_shared<CButton>(Point(22, 428), "TPTAV02.DEF", CButton::tooltip(CGI->generaltexth->tavernInfo[5]), std::bind(&CTavernWindow::thievesguildb, this), EShortcut::ADVENTURE_THIEVES_GUILD);
 
 	if(LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //not enough gold
 	{
@@ -909,13 +910,9 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2,
 	portraits[0] = std::make_shared<CAnimImage>("PortraitsLarge", heroInst[0]->portrait, 0, 257, 13);
 	portraits[1] = std::make_shared<CAnimImage>("PortraitsLarge", heroInst[1]->portrait, 0, 485, 13);
 
-	artifs[0] = std::make_shared<CArtifactsOfHero>(Point(-334, 150));
-	artifs[0]->commonInfo = std::make_shared<CArtifactsOfHero::SCommonPart>();
-	artifs[0]->commonInfo->participants.insert(artifs[0].get());
+	artifs[0] = std::make_shared<CArtifactsOfHeroMain>(Point(-334, 150));
 	artifs[0]->setHero(heroInst[0]);
-	artifs[1] = std::make_shared<CArtifactsOfHero>(Point(96, 150));
-	artifs[1]->commonInfo = artifs[0]->commonInfo;
-	artifs[1]->commonInfo->participants.insert(artifs[1].get());
+	artifs[1] = std::make_shared<CArtifactsOfHeroMain>(Point(98, 150));
 	artifs[1]->setHero(heroInst[1]);
 
 	addSet(artifs[0]);
@@ -986,7 +983,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2,
 		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), "IOKAY.DEF", CGI->generaltexth->zelp[600], std::bind(&CExchangeWindow::close, this), SDLK_RETURN);
+	quit = std::make_shared<CButton>(Point(732, 567), "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); });
 
@@ -1102,11 +1099,11 @@ CShipyardWindow::CShipyardWindow(const TResources & cost, int state, BoatId boat
 	goldPic = std::make_shared<CAnimImage>("RESOURCE",GameResID(EGameResID::GOLD), 0, 100, 244);
 	woodPic = std::make_shared<CAnimImage>("RESOURCE", GameResID(EGameResID::WOOD), 0, 196, 244);
 
-	quit = std::make_shared<CButton>(Point(224, 312), "ICANCEL", CButton::tooltip(CGI->generaltexth->allTexts[599]), std::bind(&CShipyardWindow::close, this), SDLK_ESCAPE);
-	build = std::make_shared<CButton>(Point(42, 312), "IBUY30", CButton::tooltip(CGI->generaltexth->allTexts[598]), std::bind(&CShipyardWindow::close, this), SDLK_RETURN);
+	quit = std::make_shared<CButton>(Point(224, 312), "ICANCEL", CButton::tooltip(CGI->generaltexth->allTexts[599]), std::bind(&CShipyardWindow::close, this), EShortcut::GLOBAL_CANCEL);
+	build = std::make_shared<CButton>(Point(42, 312), "IBUY30", CButton::tooltip(CGI->generaltexth->allTexts[598]), std::bind(&CShipyardWindow::close, this), EShortcut::GLOBAL_ACCEPT);
 	build->addCallback(onBuy);
 
-	for(auto i = EGameResID::WOOD; i <= EGameResID::GOLD; vstd::advance(i, 1))
+	for(GameResID i = EGameResID::WOOD; i <= EGameResID::GOLD; ++i)
 	{
 		if(cost[i] > LOCPLINT->cb->getResourceAmount(i))
 		{
@@ -1206,9 +1203,9 @@ CTransformerWindow::CTransformerWindow(const IMarket * _market, const CGHeroInst
 		}
 	}
 
-	all = std::make_shared<CButton>(Point(146, 416), "ALTARMY.DEF", CGI->generaltexth->zelp[590], [&](){ addAll(); }, SDLK_a);
-	convert = std::make_shared<CButton>(Point(269, 416), "ALTSACR.DEF", CGI->generaltexth->zelp[591], [&](){ makeDeal(); }, SDLK_RETURN);
-	cancel = std::make_shared<CButton>(Point(392, 416), "ICANCEL.DEF", CGI->generaltexth->zelp[592], [&](){ close(); },SDLK_ESCAPE);
+	all = std::make_shared<CButton>(Point(146, 416), "ALTARMY.DEF", CGI->generaltexth->zelp[590], [&](){ addAll(); }, EShortcut::RECRUITMENT_UPGRADE_ALL);
+	convert = std::make_shared<CButton>(Point(269, 416), "ALTSACR.DEF", CGI->generaltexth->zelp[591], [&](){ makeDeal(); }, EShortcut::GLOBAL_ACCEPT);
+	cancel = std::make_shared<CButton>(Point(392, 416), "ICANCEL.DEF", CGI->generaltexth->zelp[592], [&](){ close(); },EShortcut::GLOBAL_CANCEL);
 	statusbar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26));
 
 	titleLeft = std::make_shared<CLabel>(153, 29,FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[485]);//holding area
@@ -1321,12 +1318,11 @@ CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, const IMarket
 	title = std::make_shared<CLabel>(231, 26, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, titleStr);
 
 	std::vector<int> goods = market->availableItemsIds(EMarketMode::RESOURCE_SKILL);
-	assert(goods.size() == 4);
 
 	for(int i=0; i<goods.size(); i++)//prepare clickable items
 		items.push_back(std::make_shared<CItem>(this, goods[i], 54+i*104, 234));
 
-	cancel = std::make_shared<CButton>(Point(200, 313), "IOKAY.DEF", CGI->generaltexth->zelp[632], [&](){ close(); }, SDLK_RETURN);
+	cancel = std::make_shared<CButton>(Point(200, 313), "IOKAY.DEF", CGI->generaltexth->zelp[632], [&](){ close(); }, EShortcut::GLOBAL_ACCEPT);
 	statusbar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26));
 }
 
@@ -1364,10 +1360,10 @@ CUnivConfirmWindow::CUnivConfirmWindow(CUniversityWindow * owner_, int SKILL, bo
 	boost::replace_first(text, "%s", CGI->skillh->getByIndex(SKILL)->getNameTranslated());
 	boost::replace_first(text, "%d", "2000");
 
-	confirm = std::make_shared<CButton>(Point(148, 299), "IBY6432.DEF", CButton::tooltip(hoverText, text), [=](){makeDeal(SKILL);}, SDLK_RETURN);
+	confirm = std::make_shared<CButton>(Point(148, 299), "IBY6432.DEF", CButton::tooltip(hoverText, text), [=](){makeDeal(SKILL);}, EShortcut::GLOBAL_ACCEPT);
 	confirm->block(!available);
 
-	cancel = std::make_shared<CButton>(Point(252,299), "ICANCEL.DEF", CGI->generaltexth->zelp[631], [&](){ close(); }, SDLK_ESCAPE);
+	cancel = std::make_shared<CButton>(Point(252,299), "ICANCEL.DEF", CGI->generaltexth->zelp[631], [&](){ close(); }, EShortcut::GLOBAL_CANCEL);
 	statusbar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26));
 }
 
@@ -1387,7 +1383,7 @@ CGarrisonWindow::CGarrisonWindow(const CArmedInstance * up, const CGHeroInstance
 		auto split = std::make_shared<CButton>(Point(88, 314), "IDV6432.DEF", CButton::tooltip(CGI->generaltexth->tcommands[3], ""), [&](){ garr->splitClick(); } );
 		garr->addSplitBtn(split);
 	}
-	quit = std::make_shared<CButton>(Point(399, 314), "IOK6432.DEF", CButton::tooltip(CGI->generaltexth->tcommands[8], ""), [&](){ close(); }, SDLK_RETURN);
+	quit = std::make_shared<CButton>(Point(399, 314), "IOK6432.DEF", CButton::tooltip(CGI->generaltexth->tcommands[8], ""), [&](){ close(); }, EShortcut::GLOBAL_ACCEPT);
 
 	std::string titleText;
 	if(down->tempOwner == up->tempOwner)
@@ -1437,7 +1433,7 @@ CHillFortWindow::CHillFortWindow(const CGHeroInstance * visitor, const CGObjectI
 
 	for(int i = 0; i < slotsCount; i++)
 	{
-		upgrade[i] = std::make_shared<CButton>(Point(107 + i * 76, 171), "", CButton::tooltip(getTextForSlot(SlotID(i))), [=](){ makeDeal(SlotID(i)); }, SDLK_1 + i);
+		upgrade[i] = std::make_shared<CButton>(Point(107 + i * 76, 171), "", CButton::tooltip(getTextForSlot(SlotID(i))), [=](){ makeDeal(SlotID(i)); }, vstd::next(EShortcut::SELECT_INDEX_1, i));
 		for(auto image : { "APHLF1R.DEF", "APHLF1Y.DEF", "APHLF1G.DEF" })
 			upgrade[i]->addImage(image);
 
@@ -1448,11 +1444,11 @@ CHillFortWindow::CHillFortWindow(const CGHeroInstance * visitor, const CGObjectI
 		}
 	}
 
-	upgradeAll = std::make_shared<CButton>(Point(30, 231), "", CButton::tooltip(CGI->generaltexth->allTexts[432]), [&](){ makeDeal(SlotID(slotsCount));}, SDLK_0);
+	upgradeAll = std::make_shared<CButton>(Point(30, 231), "", CButton::tooltip(CGI->generaltexth->allTexts[432]), [&](){ makeDeal(SlotID(slotsCount));}, EShortcut::RECRUITMENT_UPGRADE_ALL);
 	for(auto image : { "APHLF4R.DEF", "APHLF4Y.DEF", "APHLF4G.DEF" })
 		upgradeAll->addImage(image);
 
-	quit = std::make_shared<CButton>(Point(294, 275), "IOKAY.DEF", CButton::tooltip(), std::bind(&CHillFortWindow::close, this), SDLK_RETURN);
+	quit = std::make_shared<CButton>(Point(294, 275), "IOKAY.DEF", CButton::tooltip(), std::bind(&CHillFortWindow::close, this), EShortcut::GLOBAL_ACCEPT);
 	statusbar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26));
 
 	garr = std::make_shared<CGarrisonInt>(108, 60, 18, Point(), hero, nullptr);
@@ -1628,8 +1624,7 @@ CThievesGuildWindow::CThievesGuildWindow(const CGObjectInstance * _owner):
 	SThievesGuildInfo tgi; //info to be displayed
 	LOCPLINT->cb->getThievesGuildInfo(tgi, owner);
 
-	exitb = std::make_shared<CButton>(Point(748, 556), "TPMAGE1", CButton::tooltip(CGI->generaltexth->allTexts[600]), [&](){ close();}, SDLK_RETURN);
-	exitb->assignedKeys.insert(SDLK_ESCAPE);
+	exitb = std::make_shared<CButton>(Point(748, 556), "TPMAGE1", CButton::tooltip(CGI->generaltexth->allTexts[600]), [&](){ close();}, EShortcut::GLOBAL_RETURN);
 	statusbar = CGStatusBar::create(3, 555, "TStatBar.bmp", 742);
 
 	resdatabar = std::make_shared<CMinorResDataBar>();
@@ -1825,7 +1820,7 @@ void CObjectListWindow::init(std::shared_ptr<CIntObject> titleWidget_, std::stri
 
 	title = std::make_shared<CLabel>(152, 27, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, _title);
 	descr = std::make_shared<CLabel>(145, 133, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, _descr);
-	exit = std::make_shared<CButton>( Point(228, 402), "ICANCEL.DEF", CButton::tooltip(), std::bind(&CObjectListWindow::exitPressed, this), SDLK_ESCAPE);
+	exit = std::make_shared<CButton>( Point(228, 402), "ICANCEL.DEF", CButton::tooltip(), std::bind(&CObjectListWindow::exitPressed, this), EShortcut::GLOBAL_CANCEL);
 
 	if(titleWidget)
 	{
@@ -1838,7 +1833,7 @@ void CObjectListWindow::init(std::shared_ptr<CIntObject> titleWidget_, std::stri
 		Point(14, 151), Point(0, 25), 9, items.size(), 0, 1, Rect(262, -32, 256, 256) );
 	list->type |= REDRAW_PARENT;
 
-	ok = std::make_shared<CButton>(Point(15, 402), "IOKAY.DEF", CButton::tooltip(), std::bind(&CObjectListWindow::elementSelected, this), SDLK_RETURN);
+	ok = std::make_shared<CButton>(Point(15, 402), "IOKAY.DEF", CButton::tooltip(), std::bind(&CObjectListWindow::elementSelected, this), EShortcut::GLOBAL_ACCEPT);
 	ok->block(!list->size());
 }
 
@@ -1886,28 +1881,28 @@ void CObjectListWindow::changeSelection(size_t which)
 	selected = which;
 }
 
-void CObjectListWindow::keyPressed (const SDL_Keycode & key)
+void CObjectListWindow::keyPressed (EShortcut key)
 {
 	int sel = static_cast<int>(selected);
 
 	switch(key)
 	{
-	break; case SDLK_UP:
+	break; case EShortcut::MOVE_UP:
 		sel -=1;
 
-	break; case SDLK_DOWN:
+	break; case EShortcut::MOVE_DOWN:
 		sel +=1;
 
-	break; case SDLK_PAGEUP:
+	break; case EShortcut::MOVE_PAGE_UP:
 		sel -=9;
 
-	break; case SDLK_PAGEDOWN:
+	break; case EShortcut::MOVE_PAGE_DOWN:
 		sel +=9;
 
-	break; case SDLK_HOME:
+	break; case EShortcut::MOVE_FIRST:
 		sel = 0;
 
-	break; case SDLK_END:
+	break; case EShortcut::MOVE_LAST:
 		sel = static_cast<int>(items.size());
 
 	break; default:

+ 3 - 3
client/windows/GUIClasses.h

@@ -14,7 +14,7 @@
 #include "../lib/ResourceSet.h"
 #include "../lib/CConfigHandler.h"
 #include "../lib/int3.h"
-#include "../widgets/CArtifactHolder.h"
+#include "../widgets/CWindowWithArtifacts.h"
 #include "../widgets/CGarrisonInt.h"
 #include "../widgets/Images.h"
 
@@ -194,7 +194,7 @@ public:
 	std::shared_ptr<CIntObject> genItem(size_t index);
 	void elementSelected();//call callback and close this window
 	void changeSelection(size_t which);
-	void keyPressed(const SDL_Keycode & key) override;
+	void keyPressed(EShortcut key) override;
 };
 
 class CTavernWindow : public CStatusbarWindow
@@ -327,7 +327,7 @@ class CExchangeWindow : public CStatusbarWindow, public CGarrisonHolder, public
 
 public:
 	std::array<const CGHeroInstance *, 2> heroInst;
-	std::array<std::shared_ptr<CArtifactsOfHero>, 2> artifs;
+	std::array<std::shared_ptr<CArtifactsOfHeroMain>, 2> artifs;
 
 	void updateGarrisons() override;
 

+ 9 - 8
client/windows/InfoWindows.cpp

@@ -26,6 +26,7 @@
 #include "../windows/CMessage.h"
 #include "../renderSDL/SDL_Extensions.h"
 #include "../gui/CursorHandler.h"
+#include "../gui/Shortcut.h"
 
 #include "../../CCallback.h"
 
@@ -79,9 +80,6 @@ CSelWindow::CSelWindow(const std::string &Text, PlayerColor player, int charperl
 
 	text = std::make_shared<CTextBox>(Text, Rect(0, 0, 250, 100), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE);
 
-	buttons.front()->assignedKeys.insert(SDLK_RETURN); //first button - reacts on enter
-	buttons.back()->assignedKeys.insert(SDLK_ESCAPE); //last button - reacts on escape
-
 	if (buttons.size() > 1 && askID.getNum() >= 0) //cancel button functionality
 	{
 		buttons.back()->addCallback([askID]() {
@@ -96,8 +94,8 @@ CSelWindow::CSelWindow(const std::string &Text, PlayerColor player, int charperl
 		addChild(comps[i].get());
 		components.push_back(comps[i]);
 		comps[i]->onSelect = std::bind(&CSelWindow::selectionChange,this,i);
-		if(i<9)
-			comps[i]->assignedKeys.insert(SDLK_1+i);
+		if(i<8)
+			comps[i]->assignedKey = vstd::next(EShortcut::SELECT_INDEX_1,i);
 	}
 	CMessage::drawIWindow(this, Text, player);
 }
@@ -137,10 +135,13 @@ CInfoWindow::CInfoWindow(std::string Text, PlayerColor player, const TCompsInfo
 		text->resize(text->label->textSize);
 	}
 
-	if(buttons.size())
+	if(buttons.size() == 1)
+		buttons.front()->assignedKey = EShortcut::GLOBAL_RETURN;
+
+	if(buttons.size() == 2)
 	{
-		buttons.front()->assignedKeys.insert(SDLK_RETURN); //first button - reacts on enter
-		buttons.back()->assignedKeys.insert(SDLK_ESCAPE); //last button - reacts on escape
+		buttons.front()->assignedKey = EShortcut::GLOBAL_ACCEPT;
+		buttons.back()->assignedKey = EShortcut::GLOBAL_CANCEL;
 	}
 
 	for(auto & comp : comps)

+ 2 - 2
client/windows/InfoWindows.h

@@ -45,8 +45,8 @@ public:
 class CInfoWindow : public CSimpleWindow
 {
 public:
-	typedef std::vector<std::pair<std::string, CFunctionList<void()> > > TButtonsInfo;
-	typedef std::vector<std::shared_ptr<CComponent>> TCompsInfo;
+	using TButtonsInfo = std::vector<std::pair<std::string, CFunctionList<void()>>>;
+	using TCompsInfo = std::vector<std::shared_ptr<CComponent>>;
 	QueryID ID; //for identification
 	std::shared_ptr<CTextBox> text;
 	std::vector<std::shared_ptr<CButton>> buttons;

+ 4 - 4
client/windows/QuickRecruitmentWindow.cpp

@@ -14,6 +14,7 @@
 #include "../widgets/Buttons.h"
 #include "../widgets/CreatureCostBox.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../../CCallback.h"
 #include "../../lib/ResourceSet.h"
 #include "../../lib/CCreatureHandler.h"
@@ -29,20 +30,19 @@ void QuickRecruitmentWindow::setButtons()
 
 void QuickRecruitmentWindow::setCancelButton()
 {
-	cancelButton = std::make_shared<CButton>(Point((pos.w / 2) + 48, 418), "ICN6432.DEF", CButton::tooltip(), [&](){ close(); }, SDLK_ESCAPE);
+	cancelButton = std::make_shared<CButton>(Point((pos.w / 2) + 48, 418), "ICN6432.DEF", CButton::tooltip(), [&](){ close(); }, EShortcut::GLOBAL_CANCEL);
 	cancelButton->setImageOrder(0, 1, 2, 3);
 }
 
 void QuickRecruitmentWindow::setBuyButton()
 {
-	buyButton = std::make_shared<CButton>(Point((pos.w / 2) - 32, 418), "IBY6432.DEF", CButton::tooltip(), [&](){ purchaseUnits(); }, SDLK_RETURN);
-	cancelButton->assignedKeys.insert(SDLK_ESCAPE);
+	buyButton = std::make_shared<CButton>(Point((pos.w / 2) - 32, 418), "IBY6432.DEF", CButton::tooltip(), [&](){ purchaseUnits(); }, EShortcut::GLOBAL_ACCEPT);
 	buyButton->setImageOrder(0, 1, 2, 3);
 }
 
 void QuickRecruitmentWindow::setMaxButton()
 {
-	maxButton = std::make_shared<CButton>(Point((pos.w/2)-112, 418), "IRCBTNS.DEF", CButton::tooltip(), [&](){ maxAllCards(cards); }, SDLK_m);
+	maxButton = std::make_shared<CButton>(Point((pos.w/2)-112, 418), "IRCBTNS.DEF", CButton::tooltip(), [&](){ maxAllCards(cards); }, EShortcut::RECRUITMENT_MAX);
 	maxButton->setImageOrder(0, 1, 2, 3);
 }
 

+ 16 - 16
config/campaignSets.json

@@ -2,7 +2,7 @@
 	"roe" :
 	{
 		"images" : [ {"x": 0, "y": 0, "name":"CAMPBACK"} ],
-		"exitbutton" : {"x": 658, "y": 482, "name":"CMPSCAN", "hotkey" : 27},
+		"exitbutton" : {"x": 658, "y": 482, "name":"CMPSCAN" },
 		"items":
 		[
 			{ "x":90,  "y":72,  "file":"DATA/GOOD1.H3C",    "image":"CAMPGD1S", "video":"CGOOD1",   "open": true },
@@ -22,7 +22,7 @@
 			{"x": 34,  "y": 417, "name":"CAMP1FWX"},//one campaign have special inactive image
 			{"x": 385, "y": 401, "name":"CAMPNOSC"},//and the last one is not present
 		],
-		"exitbutton" : {"x": 658, "y": 482, "name":"CMPSCAN", "hotkey" : 27},
+		"exitbutton" : {"x": 658, "y": 482, "name":"CMPSCAN" },
 		"items":
 		[
 			{ "x":90,  "y":72,  "file":"DATA/AB.H3C",       "image":"CAMP1AB7", "video":"C1ab7", "open": true },
@@ -36,7 +36,7 @@
 	"sod":
 	{
 		"images" : [ {"x": 0, "y": 0, "name":"CAMPBKX2"} ],
-		"exitbutton" : {"x": 658, "y": 482, "name":"CMPSCAN", "hotkey" : 27},
+		"exitbutton" : {"x": 658, "y": 482, "name":"CMPSCAN" },
 		"items":
 		[
 			{ "x":90,  "y":72,  "file":"DATA/GEM.H3C",      "image":"CAMPNB1", "video":"NEW",     "open": true },
@@ -47,18 +47,18 @@
 			{ "x":34,  "y":417, "file":"DATA/FINAL.H3C",    "image":"CAMPUA1", "video":"UNHOLY",  "open": true },
 			{ "x":404, "y":414, "file":"DATA/SECRET.H3C",  "image":"CAMPSP1", "video":"SPECTRE", "open": true }
 		]
-	},
-	"wog" : 
-	{
-		/// wog campaigns, currently has no assigned button in campaign screen and thus unused
-		"images" : [ {"x": 0, "y": 0, "name":"CAMPZALL"} ],
-		"exitbutton" : {"x": 658, "y": 482, "name":"CMPSCAN", "hotkey" : 27},
-		"items":
-		[
-			{ "x":90,  "y":72,  "file":"DATA/ZC1.H3C", "image":"CAMPZ01", "open": true},
-			{ "x":539, "y":72,  "file":"DATA/ZC2.H3C", "image":"CAMPZ02", "open": true},
-			{ "x":43,  "y":245, "file":"DATA/ZC3.H3C", "image":"CAMPZ03", "open": true},
-			{ "x":311, "y":242, "file":"DATA/ZC4.H3C", "image":"CAMPZ04", "open": true}
-		]
 	}
+//	"wog" : 
+//	{
+//		/// wog campaigns, currently has no assigned button in campaign screen and thus unused
+//		"images" : [ {"x": 0, "y": 0, "name":"CAMPZALL"} ],
+//		"exitbutton" : {"x": 658, "y": 482, "name":"CMPSCAN", "hotkey" : 27},
+//		"items":
+//		[
+//			{ "x":90,  "y":72,  "file":"DATA/ZC1.H3C", "image":"CAMPZ01", "open": true},
+//			{ "x":539, "y":72,  "file":"DATA/ZC2.H3C", "image":"CAMPZ02", "open": true},
+//			{ "x":43,  "y":245, "file":"DATA/ZC3.H3C", "image":"CAMPZ03", "open": true},
+//			{ "x":311, "y":242, "file":"DATA/ZC4.H3C", "image":"CAMPZ04", "open": true}
+//		]
+//	}
 }

+ 20 - 20
config/mainmenu.json

@@ -17,22 +17,22 @@
 				"name" : "main",
 				"buttons":
 				[
-					{"x": 644, "y":  70, "center" : true, "name":"MMENUNG", "hotkey" : 110, "help": 3, "command": "to new"},
-					{"x": 645, "y": 192, "center" : true, "name":"MMENULG", "hotkey" : 108, "help": 4, "command": "to load"},
-					{"x": 643, "y": 296, "center" : true, "name":"MMENUHS", "hotkey" : 104, "help": 5, "command": "highscores"},
-					{"x": 643, "y": 414, "center" : true, "name":"MMENUCR", "hotkey" : 99,  "help": 6, "command": "to credits"},
-					{"x": 643, "y": 520, "center" : true, "name":"MMENUQT", "hotkey" : 27,  "help": 7, "command": "exit"}
+					{"x": 644, "y":  70, "center" : true, "name":"MMENUNG", "shortcut" : "mainMenuNew", "help": 3, "command": "to new"},
+					{"x": 645, "y": 192, "center" : true, "name":"MMENULG", "shortcut" : "mainMenuLoad", "help": 4, "command": "to load"},
+					{"x": 643, "y": 296, "center" : true, "name":"MMENUHS", "shortcut" : "mainMenuScores", "help": 5, "command": "highscores"},
+					{"x": 643, "y": 414, "center" : true, "name":"MMENUCR", "shortcut" : "mainMenuCredits", "help": 6, "command": "to credits"},
+					{"x": 643, "y": 520, "center" : true, "name":"MMENUQT", "shortcut" : "mainMenuQuit", "help": 7, "command": "exit"}
 				]
 			},
 			{
 				"name" : "new",
 				"buttons":
 				[
-					{"x": 649, "y":  65, "center" : true, "name":"GTSINGL", "hotkey" : 115, "help": 10, "command": "start single"},
-					{"x": 649, "y": 180, "center" : true, "name":"GTMULTI", "hotkey" : 109, "help": 12, "command": "start multi"},
-					{"x": 646, "y": 298, "center" : true, "name":"GTCAMPN", "hotkey" : 99,  "help": 11, "command": "to campaign"},
-					{"x": 647, "y": 412, "center" : true, "name":"GTTUTOR", "hotkey" : 116, "help": 13, "command": "start tutorial"},
-					{"x": 645, "y": 517, "center" : true, "name":"GTBACK",  "hotkey" : 27,  "help": 14, "command": "to main"}
+					{"x": 649, "y":  65, "center" : true, "name":"GTSINGL", "shortcut" : "mainMenuSingleplayer", "help": 10, "command": "start single"},
+					{"x": 649, "y": 180, "center" : true, "name":"GTMULTI", "shortcut" : "mainMenuMultiplayer", "help": 12, "command": "start multi"},
+					{"x": 646, "y": 298, "center" : true, "name":"GTCAMPN", "shortcut" : "mainMenuCampaign", "help": 11, "command": "to campaign"},
+					{"x": 647, "y": 412, "center" : true, "name":"GTTUTOR", "shortcut" : "mainMenuTutorial", "help": 13, "command": "start tutorial"},
+					{"x": 645, "y": 517, "center" : true, "name":"GTBACK",  "shortcut" : "mainMenuBack", "help": 14, "command": "to main"}
 				],
 				"images": [ {"x": 114, "y": 312, "name":"NEWGAME"} ]
 			},
@@ -40,11 +40,11 @@
 				"name" : "load",
 				"buttons":
 				[
-					{"x": 649, "y":  65, "center" : true, "name":"GTSINGL", "hotkey" : 115, "help": 10, "command": "load single"},
-					{"x": 649, "y": 180, "center" : true, "name":"GTMULTI", "hotkey" : 109, "help": 12, "command": "load multi"},
-					{"x": 646, "y": 298, "center" : true, "name":"GTCAMPN", "hotkey" : 99,  "help": 11, "command": "load campaign"},
-					{"x": 647, "y": 412, "center" : true, "name":"GTTUTOR", "hotkey" : 116, "help": 13, "command": "load tutorial"},
-					{"x": 645, "y": 517, "center" : true, "name":"GTBACK",  "hotkey" : 27,  "help": 14, "command": "to main"}
+					{"x": 649, "y":  65, "center" : true, "name":"GTSINGL", "shortcut" : "mainMenuSingleplayer", "help": 10, "command": "load single"},
+					{"x": 649, "y": 180, "center" : true, "name":"GTMULTI", "shortcut" : "mainMenuMultiplayer", "help": 12, "command": "load multi"},
+					{"x": 646, "y": 298, "center" : true, "name":"GTCAMPN", "shortcut" : "mainMenuCampaign", "help": 11, "command": "load campaign"},
+					{"x": 647, "y": 412, "center" : true, "name":"GTTUTOR", "shortcut" : "mainMenuTutorial", "help": 13, "command": "load tutorial"},
+					{"x": 645, "y": 517, "center" : true, "name":"GTBACK",  "shortcut" : "mainMenuBack", "help": 14, "command": "to main"}
 				],
 				"images": [ {"x": 114, "y": 312, "name":"LOADGAME"} ]
 			},
@@ -52,11 +52,11 @@
 				"name" : "campaign",
 				"buttons":
 				[
-					{"x": 634, "y":  67, "center" : true, "name":"CSSSOD", "hotkey" : 119, "command": "campaigns sod"},
-					{"x": 637, "y": 181, "center" : true, "name":"CSSROE", "hotkey" : 114, "command": "campaigns roe"},
-					{"x": 638, "y": 301, "center" : true, "name":"CSSARM", "hotkey" : 97,  "command": "campaigns ab"},
-					{"x": 638, "y": 413, "center" : true, "name":"CSSCUS", "hotkey" : 99,  "command": "start campaign"},
-					{"x": 639, "y": 518, "center" : true, "name":"CSSEXIT", "hotkey" : 27,  "command": "to new"}
+					{"x": 634, "y":  67, "center" : true, "name":"CSSSOD",  "shortcut" : "mainMenuCampaignSod", "command": "campaigns sod"},
+					{"x": 637, "y": 181, "center" : true, "name":"CSSROE",  "shortcut" : "mainMenuCampaignRoe", "command": "campaigns roe"},
+					{"x": 638, "y": 301, "center" : true, "name":"CSSARM",  "shortcut" : "mainMenuCampaignAb", "command": "campaigns ab"},
+					{"x": 638, "y": 413, "center" : true, "name":"CSSCUS",  "shortcut" : "mainMenuCampaign", "command": "start campaign"},
+					{"x": 639, "y": 518, "center" : true, "name":"CSSEXIT", "shortcut" : "mainMenuBack",  "command": "to new"}
 				],
 			}
 		]

+ 20 - 10
config/objects/generic.json

@@ -15,7 +15,7 @@
 	},
 
 	"altarOfSacrifice" : {
-		"index" :2,
+		"index" :2, 
 		"handler" : "market",
 		"base" : {
 			"sounds" : {
@@ -36,7 +36,7 @@
 		}
 	},
 	"tradingPost" : {
-		"index" :221,
+		"index" :221, 
 		"handler" : "market",
 		"base" : {
 			"sounds" : {
@@ -54,12 +54,13 @@
 					"rarity"	: 100
 				},
 				"modes" : ["resource-resource", "resource-player"],
-				"efficacy" : 5
+				"efficiency" : 5,
+				"title" : "core.genrltxt.159"
 			}
 		}
 	},
 	"tradingPostDUPLICATE"		: {
-		"index" :99,
+		"index" :99, 
 		"handler" : "market",
 		"base" : {
 			"sounds" : {
@@ -77,12 +78,13 @@
 					"rarity"	: 100
 				},
 				"modes" : ["resource-resource", "resource-player"],
-				"efficacy" : 5
+				"efficiency" : 5,
+				"title" : "core.genrltxt.159"
 			}
 		}
 	},
 	"freelancersGuild" : {
-		"index" :213,
+		"index" :213, 
 		"handler" : "market",
 		"types" : {
 			"object" : {
@@ -99,7 +101,7 @@
 	},
 
 	"blackMarket" : {
-		"index" :7,
+		"index" :7, 
 		"handler" : "market",
 		"base" : {
 			"sounds" : {
@@ -115,7 +117,8 @@
 					"value"		: 8000,
 					"rarity"	: 20
 				},
-				"modes" : ["resource-artifact"]
+				"modes" : ["resource-artifact"],
+				"title" : "core.genrltxt.349"
 			}
 		}
 	},
@@ -542,7 +545,7 @@
 		}
 	},
 	"university" : {
-		"index" :104,
+		"index" :104, 
 		"handler" : "market",
 		"base" : {
 			"sounds" : {
@@ -559,7 +562,14 @@
 				},
 				"modes" : ["resource-skill"],
 				"title" : "core.genrltxt.602",
-				"speech" : "core.genrltxt.603"
+				"speech" : "core.genrltxt.603",
+				"offer": 
+				[
+					{ "noneOf" : ["necromancy"] },
+					{ "noneOf" : ["necromancy"] },
+					{ "noneOf" : ["necromancy"] },
+					{ "noneOf" : ["necromancy"] }
+				]
 			}
 		}
 	},

+ 11 - 22
config/widgets/battleWindow.json

@@ -21,8 +21,7 @@
 			"position": {"x": 4, "y": 560},
 			"image": "icm003",
 			"help": "core.help.381",
-			"callback": "options",
-			"hotkey": "o"
+			"hotkey": "globalOptions"
 		},
 
 		{
@@ -31,8 +30,7 @@
 			"position": {"x": 55, "y": 560},
 			"image": "icm001",
 			"help": "core.help.379",
-			"callback": "surrender",
-			"hotkey": "s"
+			"hotkey": "battleSurrender"
 		},
 
 		{
@@ -41,8 +39,7 @@
 			"position": {"x": 106, "y": 560},
 			"image": "icm002",
 			"help": "core.help.380",
-			"callback": "flee",
-			"hotkey": "r"
+			"hotkey": "battleRetreat"
 		},
 
 		{
@@ -51,8 +48,7 @@
 			"position": {"x": 157, "y": 560},
 			"image": "icm004",
 			"help": "core.help.382",
-			"callback": "autofight",
-			"hotkey": "a"
+			"hotkey": "battleAutocombat"
 		},
 
 		{
@@ -61,8 +57,7 @@
 			"position": {"x": 646, "y": 560},
 			"image": "icm005",
 			"help": "core.help.385",
-			"callback": "spellbook",
-			"hotkey": "c"
+			"hotkey": "battleCastSpell"
 		},
 
 		{
@@ -71,8 +66,7 @@
 			"position": {"x": 697, "y": 560},
 			"image": "icm006",
 			"help": "core.help.386",
-			"callback": "wait",
-			"hotkey": "w"
+			"hotkey": "battleWait"
 		},
 
 		{
@@ -81,8 +75,7 @@
 			"position": {"x": 748, "y": 560},
 			"image": "icm007",
 			"help": "core.help.387",
-			"callback": "defence",
-			"hotkey": ["d", "space"]
+			"hotkey": "battleDefend"
 		},
 
 		{
@@ -90,9 +83,8 @@
 			"name": "consoleUp",
 			"position": {"x": 625, "y": 560},
 			"image": "ComSlide",
-			"callback": "consoleUp",
 			"imageOrder": [0, 1, 0, 0],
-			"hotkey": "up"
+			"hotkey": "battleConsoleUp"
 		},
 
 		{
@@ -100,9 +92,8 @@
 			"name": "consoleDown",
 			"position": {"x": 625, "y": 579},
 			"image": "ComSlide",
-			"callback": "consoleDown",
 			"imageOrder": [2, 3, 2, 2],
-			"hotkey": "down"
+			"hotkey": "battleConsoleDown"
 		},
 
 		{
@@ -117,8 +108,7 @@
 			"name": "tacticNext",
 			"position": {"x": 213, "y": 560},
 			"image": "icm011",
-			"callback": "tacticNext",
-			"hotkey": "space"
+			"hotkey": "battleTacticsNext"
 		},
 		
 		{
@@ -126,8 +116,7 @@
 			"name": "tacticEnd",
 			"position": {"x": 419, "y": 560},
 			"image": "icm012",
-			"callback": "tacticEnd",
-			"hotkey": "enter"
+			"hotkey": "battleTacticsEnd"
 		}
 	]
 }

+ 0 - 1
config/widgets/settings/generalOptionsTab.json

@@ -47,7 +47,6 @@
 			"image": "settingsWindow/button32",
 			"help": "vcmi.systemOptions.resolutionButton",
 			"callback": "setGameResolution",
-			"hotkey": "g",			
 			"items":
 			[
 				{

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно