Преглед изворни кода

Merge branch 'develop' into rmg-split-enum-monster-strength

Warzyw647 пре 2 година
родитељ
комит
bc4755a89a
100 измењених фајлова са 4647 додато и 3871 уклоњено
  1. 16 1
      AI/BattleAI/AttackPossibility.cpp
  2. 7 0
      AI/BattleAI/BattleExchangeVariant.cpp
  3. 1 1
      AI/Nullkiller/AIUtility.cpp
  4. 1 1
      AI/Nullkiller/Analyzers/ObjectClusterizer.cpp
  5. 23 2
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  6. 2 0
      AI/Nullkiller/Pathfinding/AINodeStorage.h
  7. 63 0
      AI/Nullkiller/Pathfinding/Actions/SpecialAction.cpp
  8. 33 1
      AI/Nullkiller/Pathfinding/Actions/SpecialAction.h
  9. 1 1
      AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp
  10. 7 2
      AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp
  11. BIN
      Mods/vcmi/Data/settingsWindow/frameAudio.png
  12. BIN
      Mods/vcmi/Data/settingsWindow/frameMovement.png
  13. BIN
      Mods/vcmi/Data/settingsWindow/frameStackQueue.png
  14. BIN
      Mods/vcmi/Data/settingsWindow/gear.png
  15. 0 2
      Mods/vcmi/config/vcmi/chinese.json
  16. 18 6
      Mods/vcmi/config/vcmi/english.json
  17. 25 9
      Mods/vcmi/config/vcmi/german.json
  18. 0 5
      Mods/vcmi/config/vcmi/polish.json
  19. 0 5
      Mods/vcmi/config/vcmi/russian.json
  20. 0 5
      Mods/vcmi/config/vcmi/spanish.json
  21. 6 5
      Mods/vcmi/config/vcmi/ukrainian.json
  22. 1 1
      android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityMods.java
  23. 21 655
      client/CMT.cpp
  24. 37 9
      client/CMakeLists.txt
  25. 2 2
      client/CMusicHandler.cpp
  26. 87 137
      client/CPlayerInterface.cpp
  27. 1 2
      client/CPlayerInterface.h
  28. 7 4
      client/CServerHandler.cpp
  29. 19 22
      client/CVideoHandler.cpp
  30. 3 6
      client/Client.cpp
  31. 5 49
      client/ClientCommandManager.cpp
  32. 0 4
      client/ClientCommandManager.h
  33. 0 9
      client/DPIaware.manifest
  34. 2 3
      client/NetPacksClient.cpp
  35. 6 5
      client/NetPacksLobbyClient.cpp
  36. 1 1
      client/PlayerLocalState.cpp
  37. 827 0
      client/adventureMap/AdventureMapInterface.cpp
  38. 41 86
      client/adventureMap/AdventureMapInterface.h
  39. 455 0
      client/adventureMap/AdventureMapShortcuts.cpp
  40. 88 0
      client/adventureMap/AdventureMapShortcuts.h
  41. 455 0
      client/adventureMap/AdventureMapWidget.cpp
  42. 109 0
      client/adventureMap/AdventureMapWidget.h
  43. 8 7
      client/adventureMap/AdventureOptions.cpp
  44. 2 2
      client/adventureMap/AdventureOptions.h
  45. 20 0
      client/adventureMap/AdventureState.h
  46. 0 94
      client/adventureMap/CAdvMapPanel.cpp
  47. 0 60
      client/adventureMap/CAdvMapPanel.h
  48. 0 1346
      client/adventureMap/CAdventureMapInterface.cpp
  49. 19 24
      client/adventureMap/CInGameConsole.cpp
  50. 6 6
      client/adventureMap/CInfoBar.cpp
  51. 47 20
      client/adventureMap/CList.cpp
  52. 23 31
      client/adventureMap/CList.h
  53. 8 6
      client/adventureMap/CMinimap.cpp
  54. 25 33
      client/adventureMap/CResDataBar.cpp
  55. 15 3
      client/adventureMap/CResDataBar.h
  56. 9 8
      client/battle/BattleActionsController.cpp
  57. 17 17
      client/battle/BattleAnimationClasses.cpp
  58. 10 10
      client/battle/BattleAnimationClasses.h
  59. 15 6
      client/battle/BattleFieldController.cpp
  60. 2 0
      client/battle/BattleFieldController.h
  61. 5 4
      client/battle/BattleInterface.cpp
  62. 28 19
      client/battle/BattleInterfaceClasses.cpp
  63. 1 0
      client/battle/BattleInterfaceClasses.h
  64. 2 2
      client/battle/BattleObstacleController.cpp
  65. 1 1
      client/battle/BattleObstacleController.h
  66. 28 10
      client/battle/BattleProjectileController.cpp
  67. 9 2
      client/battle/BattleProjectileController.h
  68. 15 7
      client/battle/BattleStacksController.cpp
  69. 3 3
      client/battle/BattleStacksController.h
  70. 15 9
      client/battle/BattleWindow.cpp
  71. 1 0
      client/battle/BattleWindow.h
  72. 269 0
      client/eventsSDL/InputHandler.cpp
  73. 77 0
      client/eventsSDL/InputHandler.h
  74. 85 0
      client/eventsSDL/InputSourceKeyboard.cpp
  75. 23 0
      client/eventsSDL/InputSourceKeyboard.h
  76. 72 0
      client/eventsSDL/InputSourceMouse.cpp
  77. 25 0
      client/eventsSDL/InputSourceMouse.h
  78. 92 0
      client/eventsSDL/InputSourceText.cpp
  79. 29 0
      client/eventsSDL/InputSourceText.h
  80. 135 0
      client/eventsSDL/InputSourceTouch.cpp
  81. 37 0
      client/eventsSDL/InputSourceTouch.h
  82. 2 2
      client/eventsSDL/NotificationHandler.cpp
  83. 0 0
      client/eventsSDL/NotificationHandler.h
  84. 87 0
      client/eventsSDL/UserEventHandler.cpp
  85. 20 0
      client/eventsSDL/UserEventHandler.h
  86. 65 656
      client/gui/CGuiHandler.cpp
  87. 40 107
      client/gui/CGuiHandler.h
  88. 54 103
      client/gui/CIntObject.cpp
  89. 44 100
      client/gui/CIntObject.h
  90. 2 1
      client/gui/CursorHandler.cpp
  91. 244 0
      client/gui/EventDispatcher.cpp
  92. 68 0
      client/gui/EventDispatcher.h
  93. 74 0
      client/gui/EventsReceiver.cpp
  94. 77 0
      client/gui/EventsReceiver.h
  95. 57 0
      client/gui/FramerateManager.cpp
  96. 40 0
      client/gui/FramerateManager.h
  97. 183 42
      client/gui/InterfaceObjectConfigurable.cpp
  98. 13 0
      client/gui/InterfaceObjectConfigurable.h
  99. 10 3
      client/gui/Shortcut.h
  100. 19 86
      client/gui/ShortcutHandler.cpp

+ 16 - 1
AI/BattleAI/AttackPossibility.cpp

@@ -50,7 +50,22 @@ int64_t AttackPossibility::calculateDamageReduce(
 	vstd::amin(damageDealt, defender->getAvailableHealth());
 
 	// FIXME: provide distance info for Jousting bonus
-	auto enemyDamageBeforeAttack = cb.battleEstimateDamage(defender, attacker, 0);
+	auto attackerUnitForMeasurement = attacker;
+
+	if(attackerUnitForMeasurement->isTurret())
+	{
+		auto ourUnits = cb.battleGetUnitsIf([&](const battle::Unit * u) -> bool
+			{
+				return u->unitSide() == attacker->unitSide() && !u->isTurret();
+			});
+
+		if(ourUnits.empty())
+			attackerUnitForMeasurement = defender;
+		else
+			attackerUnitForMeasurement = ourUnits.front();
+	}
+
+	auto enemyDamageBeforeAttack = cb.battleEstimateDamage(defender, attackerUnitForMeasurement, 0);
 	auto enemiesKilled = damageDealt / defender->getMaxHealth() + (damageDealt % defender->getMaxHealth() >= defender->getFirstHPleft() ? 1 : 0);
 	auto enemyDamage = averageDmg(enemyDamageBeforeAttack.damage);
 	auto damagePerEnemy = enemyDamage / (double)defender->getCount();

+ 7 - 0
AI/BattleAI/BattleExchangeVariant.cpp

@@ -386,9 +386,13 @@ int64_t BattleExchangeEvaluator::calculateExchange(
 
 	for(auto unit : exchangeUnits)
 	{
+		if(unit->isTurret())
+			continue;
+
 		bool isOur = cb->battleMatchOwner(ap.attack.attacker, unit, true);
 		auto & attackerQueue = isOur ? ourStacks : enemyStacks;
 
+
 		if(!vstd::contains(attackerQueue, unit))
 		{
 			attackerQueue.push_back(unit);
@@ -593,6 +597,9 @@ void BattleExchangeEvaluator::updateReachabilityMap(HypotheticBattle & hb)
 
 		for(const battle::Unit * unit : turnQueue)
 		{
+			if(unit->isTurret())
+				continue;
+
 			if(turnBattle.battleCanShoot(unit))
 			{
 				for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1)

+ 1 - 1
AI/Nullkiller/AIUtility.cpp

@@ -255,7 +255,7 @@ bool isObjectPassable(const CGObjectInstance * obj, PlayerColor playerColor, Pla
 	{
 		auto quest = dynamic_cast<const CGKeys *>(obj);
 
-		if(quest->passableFor(playerColor))
+		if(quest->wasMyColorVisited(playerColor))
 			return true;
 	}
 

+ 1 - 1
AI/Nullkiller/Analyzers/ObjectClusterizer.cpp

@@ -114,7 +114,7 @@ const CGObjectInstance * ObjectClusterizer::getBlocker(const AIPath & path) cons
 
 			if(blockerObject)
 			{
-				blockers.push_back(blockerObject);
+				blockers.insert(blockers.begin(), blockerObject);
 			}
 		}
 

+ 23 - 2
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -52,6 +52,27 @@ AISharedStorage::~AISharedStorage()
 	}
 }
 
+void AIPathNode::addSpecialAction(std::shared_ptr<const SpecialAction> action)
+{
+	if(!specialAction)
+	{
+		specialAction = action;
+	}
+	else
+	{
+		auto parts = specialAction->getParts();
+
+		if(parts.empty())
+		{
+			parts.push_back(specialAction);
+		}
+
+		parts.push_back(action);
+
+		specialAction = std::make_shared<CompositeAction>(parts);
+	}
+}
+
 AINodeStorage::AINodeStorage(const Nullkiller * ai, const int3 & Sizes)
 	: sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes)
 {
@@ -765,7 +786,7 @@ void HeroChainCalculationTask::addHeroChain(const std::vector<ExchangeCandidate>
 		if(exchangeNode->actor->actorAction)
 		{
 			exchangeNode->theNodeBefore = carrier;
-			exchangeNode->specialAction = exchangeNode->actor->actorAction;
+			exchangeNode->addSpecialAction(exchangeNode->actor->actorAction);
 		}
 
 		exchangeNode->chainOther = other;
@@ -1045,7 +1066,7 @@ struct TowmPortalFinder
 				movementCost);
 
 			node->theNodeBefore = bestNode;
-			node->specialAction.reset(new AIPathfinding::TownPortalAction(targetTown));
+			node->addSpecialAction(std::make_shared<AIPathfinding::TownPortalAction>(targetTown));
 		}
 
 		return nodeOptional;

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

@@ -55,6 +55,8 @@ struct AIPathNode : public CGPathNode
 		return accessible == CGPathNode::EAccessibility::NOT_SET
 			|| accessible == CGPathNode::EAccessibility::BLOCKED;
 	}
+
+	void addSpecialAction(std::shared_ptr<const SpecialAction> action);
 };
 
 struct AIPathNodeInfo

+ 63 - 0
AI/Nullkiller/Pathfinding/Actions/SpecialAction.cpp

@@ -27,4 +27,67 @@ void SpecialAction::execute(const CGHeroInstance * hero) const
 	throw cannotFulfillGoalException("Can not execute " + toString());
 }
 
+bool CompositeAction::canAct(const AIPathNode * source) const
+{
+	for(auto part : parts)
+	{
+		if(!part->canAct(source)) return false;
+	}
+
+	return true;
+}
+
+Goals::TSubgoal CompositeAction::decompose(const CGHeroInstance * hero) const
+{
+	for(auto part : parts)
+	{
+		auto goal = part->decompose(hero);
+
+		if(!goal->invalid()) return goal;
+	}
+
+	return SpecialAction::decompose(hero);
+}
+
+void CompositeAction::execute(const CGHeroInstance * hero) const
+{
+	for(auto part : parts)
+	{
+		part->execute(hero);
+	}
+}
+
+void CompositeAction::applyOnDestination(
+	const CGHeroInstance * hero,
+	CDestinationNodeInfo & destination,
+	const PathNodeInfo & source,
+	AIPathNode * dstNode,
+	const AIPathNode * srcNode) const
+{
+	for(auto part : parts)
+	{
+		part->applyOnDestination(hero, destination, source, dstNode, srcNode);
+	}
+}
+
+std::string CompositeAction::toString() const
+{
+	std::string result = "";
+
+	for(auto part : parts)
+	{
+		result += ", " + part->toString();
+	}
+
+	return result;
+}
+
+const CGObjectInstance * CompositeAction::targetObject() const
+{
+	if(parts.empty())
+		return nullptr;
+
+	return parts.front()->targetObject();
+}
+
 }

+ 33 - 1
AI/Nullkiller/Pathfinding/Actions/SpecialAction.h

@@ -36,7 +36,7 @@ public:
 		const CGHeroInstance * hero,
 		CDestinationNodeInfo & destination,
 		const PathNodeInfo & source,
-		AIPathNode * dstMode,
+		AIPathNode * dstNode,
 		const AIPathNode * srcNode) const
 	{
 	}
@@ -44,6 +44,38 @@ public:
 	virtual std::string toString() const = 0;
 
 	virtual const CGObjectInstance * targetObject() const { return nullptr; }
+
+	virtual std::vector<std::shared_ptr<const SpecialAction>> getParts() const
+	{
+		return {};
+	}
+};
+
+class CompositeAction : public SpecialAction
+{
+private:
+	std::vector<std::shared_ptr<const SpecialAction>> parts;
+
+public:
+	CompositeAction(std::vector<std::shared_ptr<const SpecialAction>> parts) : parts(parts) {}
+
+	bool canAct(const AIPathNode * source) const override;
+	void execute(const CGHeroInstance * hero) const override;
+	std::string toString() const override;
+	const CGObjectInstance * targetObject() const override;
+	Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;
+
+	std::vector<std::shared_ptr<const SpecialAction>> getParts() const override
+	{
+		return parts;
+	}
+
+	void applyOnDestination(
+		const CGHeroInstance * hero,
+		CDestinationNodeInfo & destination,
+		const PathNodeInfo & source,
+		AIPathNode * dstNode,
+		const AIPathNode * srcNode) const override;
 };
 
 }

+ 1 - 1
AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp

@@ -133,7 +133,7 @@ namespace AIPathfinding
 
 				if(boatNode->action == CGPathNode::UNKNOWN)
 				{
-					boatNode->specialAction = virtualBoat;
+					boatNode->addSpecialAction(virtualBoat);
 					destination.blocked = false;
 					destination.action = CGPathNode::ENodeAction::EMBARK;
 					destination.node = boatNode;

+ 7 - 2
AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp

@@ -157,7 +157,7 @@ namespace AIPathfinding
 
 			nodeStorage->updateAINode(destination.node, [&](AIPathNode * node)
 			{
-				node->specialAction.reset(new QuestAction(questAction));
+				node->addSpecialAction(std::make_shared<QuestAction>(questAction));
 			});
 		}
 
@@ -279,6 +279,11 @@ namespace AIPathfinding
 
 		if(loss < actualArmyValue)
 		{
+			if(destNode->specialAction)
+			{
+				battleNode->specialAction = destNode->specialAction;
+			}
+
 			destination.node = battleNode;
 			nodeStorage->commit(destination, source);
 
@@ -288,7 +293,7 @@ namespace AIPathfinding
 
 			AIPreviousNodeRule(nodeStorage).process(source, destination, pathfinderConfig, pathfinderHelper);
 
-			battleNode->specialAction = std::make_shared<BattleAction>(destination.coord);
+			battleNode->addSpecialAction(std::make_shared<BattleAction>(destination.coord));
 
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 1
 			logAi->trace(

BIN
Mods/vcmi/Data/settingsWindow/frameAudio.png


BIN
Mods/vcmi/Data/settingsWindow/frameMovement.png


BIN
Mods/vcmi/Data/settingsWindow/frameStackQueue.png


BIN
Mods/vcmi/Data/settingsWindow/gear.png


+ 0 - 2
Mods/vcmi/config/vcmi/chinese.json

@@ -201,8 +201,6 @@
 	"core.bonus.DOUBLE_DAMAGE_CHANCE.description": "${val}%几率造成双倍基础伤害",
 	"core.bonus.DRAGON_NATURE.name": "龙",
 	"core.bonus.DRAGON_NATURE.description": "生物拥有龙的特性",
-	"core.bonus.DIRECT_DAMAGE_IMMUNITY.name": "魔法直伤免疫",
-	"core.bonus.DIRECT_DAMAGE_IMMUNITY.description": "免疫直接造成伤害的魔法",
 	"core.bonus.EARTH_IMMUNITY.name": "土系免疫",
 	"core.bonus.EARTH_IMMUNITY.description": "免疫所有土系魔法",
 	"core.bonus.ENCHANTER.name": "强化师",

+ 18 - 6
Mods/vcmi/config/vcmi/english.json

@@ -30,6 +30,15 @@
 	"vcmi.capitalColors.6" : "Teal",
 	"vcmi.capitalColors.7" : "Pink",
 
+	"vcmi.mainMenu.tutorialNotImplemented" : "Sorry, tutorial is not implemented yet\n",
+	"vcmi.mainMenu.highscoresNotImplemented" : "Sorry, high scores menu is not implemented yet\n",
+	"vcmi.mainMenu.serverConnecting" : "Connecting...",
+	"vcmi.mainMenu.serverAddressEnter" : "Enter address:",
+	"vcmi.mainMenu.serverClosing" : "Closing...",
+	"vcmi.mainMenu.hostTCP" : "Host TCP/IP game",
+	"vcmi.mainMenu.joinTCP" : "Join TCP/IP game",
+	"vcmi.mainMenu.playerName" : "Player",
+
 	"vcmi.server.errors.existingProcess"     : "Another VCMI server process is running. Please terminate it before starting a new game.",
 	"vcmi.server.errors.modsIncompatibility" : "The following mods are required to load the game:",
 	"vcmi.server.confirmReconnect"           : "Do you want to reconnect to the last session?",
@@ -46,13 +55,18 @@
 	"vcmi.systemOptions.otherGroup" : "Other Settings", // unused right now
 	"vcmi.systemOptions.townsGroup" : "Town Screen",
 
-	"vcmi.systemOptions.fullscreenButton.hover" : "Fullscreen",
-	"vcmi.systemOptions.fullscreenButton.help"  : "{Fullscreen}\n\nIf selected, VCMI will run in fullscreen mode, otherwise it will run in windowed mode",
+	"vcmi.systemOptions.fullscreenBorderless.hover" : "Fullscreen (borderless)",
+	"vcmi.systemOptions.fullscreenBorderless.help"  : "{Borderless Fullscreen}\n\nIf selected, VCMI will run in borderless fullscreen mode. In this mode, game will always use same resolution as desktop, ignoring selected resolution.",
+	"vcmi.systemOptions.fullscreenExclusive.hover"  : "Fullscreen (exclusive)",
+	"vcmi.systemOptions.fullscreenExclusive.help"   : "{Fullscreen}\n\nIf selected, VCMI will run in exclusive fullscreen mode. In this mode, game will change resolution of monitor to selected resolution.",
 	"vcmi.systemOptions.resolutionButton.hover" : "Resolution: %wx%h",
-	"vcmi.systemOptions.resolutionButton.help"  : "{Select Resolution}\n\nChange in-game screen resolution. A game restart is required to apply the new resolution.",
+	"vcmi.systemOptions.resolutionButton.help"  : "{Select Resolution}\n\nChange in-game screen resolution.",
 	"vcmi.systemOptions.resolutionMenu.hover"   : "Select Resolution",
 	"vcmi.systemOptions.resolutionMenu.help"    : "Change in-game screen resolution.",
-	"vcmi.systemOptions.fullscreenFailed"       : "{Fullscreen}\n\nFailed to switch to fullscreen mode! The current resolution is not supported by the display!",
+	"vcmi.systemOptions.scalingButton.hover"   : "Interface Scaling: %p%",
+	"vcmi.systemOptions.scalingButton.help"    : "{Interface Scaling}\n\nChanges scaling of in-game interface",
+	"vcmi.systemOptions.scalingMenu.hover"     : "Select Interface Scaling",
+	"vcmi.systemOptions.scalingMenu.help"      : "Change in-game interface scaling.",
 	"vcmi.systemOptions.framerateButton.hover"  : "Show FPS",
 	"vcmi.systemOptions.framerateButton.help"   : "{Show FPS}\n\nToggle the visibility of the Frames Per Second counter in the corner of the game window",
 
@@ -201,8 +215,6 @@
 	"core.bonus.DOUBLE_DAMAGE_CHANCE.description": "Has a ${val}% chance of dealing double base damage when attacking",
 	"core.bonus.DRAGON_NATURE.name": "Dragon",
 	"core.bonus.DRAGON_NATURE.description": "Creature has a Dragon Nature",
-	"core.bonus.DIRECT_DAMAGE_IMMUNITY.name": "Direct Damage Immunity",
-	"core.bonus.DIRECT_DAMAGE_IMMUNITY.description": "Immune to direct damage spells",
 	"core.bonus.EARTH_IMMUNITY.name": "Earth immunity",
 	"core.bonus.EARTH_IMMUNITY.description": "Immune to all spells from the school of Earth magic",
 	"core.bonus.ENCHANTER.name": "Enchanter",

+ 25 - 9
Mods/vcmi/config/vcmi/german.json

@@ -30,6 +30,15 @@
 	"vcmi.capitalColors.6" : "Türkis",
 	"vcmi.capitalColors.7" : "Rosa",
 
+	"vcmi.mainMenu.tutorialNotImplemented" : "Das Tutorial ist aktuell noch nicht implementiert\n",
+	"vcmi.mainMenu.highscoresNotImplemented" : "Die Highscores sind aktuell noch nicht implementiert\n",
+	"vcmi.mainMenu.serverConnecting" : "Verbinde...",
+	"vcmi.mainMenu.serverAddressEnter" : "Addresse eingeben:",
+	"vcmi.mainMenu.serverClosing" : "Trenne...",
+	"vcmi.mainMenu.hostTCP" : "Hoste TCP/IP Spiel",
+	"vcmi.mainMenu.joinTCP" : "Trete TCP/IP Spiel bei",
+	"vcmi.mainMenu.playerName" : "Spieler",
+
 	"vcmi.server.errors.existingProcess"     : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst",
 	"vcmi.server.errors.modsIncompatibility" : "Erforderliche Mods um das Spiel zu laden:",
 	"vcmi.server.confirmReconnect"           : "Mit der letzten Sitzung verbinden?",
@@ -46,13 +55,18 @@
 	"vcmi.systemOptions.otherGroup" : "Andere Einstellungen", // unused right now
 	"vcmi.systemOptions.townsGroup" : "Stadt-Bildschirm",
 
-	"vcmi.systemOptions.fullscreenButton.hover" : "Vollbild",
-	"vcmi.systemOptions.fullscreenButton.help"  : "{Vollbild}\n\n Wenn ausgewählt wird VCMI im Vollbildmodus laufen, ansonsten im Fenstermodus",
+	"vcmi.systemOptions.fullscreenBorderless.hover" : "Vollbild (randlos)",
+	"vcmi.systemOptions.fullscreenBorderless.help"  : "{Randloses Vollbild}\n\nWenn diese Option ausgewählt ist, wird VCMI im randlosen Vollbildmodus ausgeführt. In diesem Modus wird das Spiel immer dieselbe Auflösung wie der Desktop verwenden und die gewählte Auflösung ignorieren.",
+	"vcmi.systemOptions.fullscreenExclusive.hover"  : "Vollbild (exklusiv)",
+	"vcmi.systemOptions.fullscreenExclusive.help"   : "{Vollbild}\n\nWenn diese Option ausgewählt ist, wird VCMI im exklusiven Vollbildmodus ausgeführt. In diesem Modus ändert das Spiel die Auflösung des Monitors auf die ausgewählte Auflösung.",
 	"vcmi.systemOptions.resolutionButton.hover" : "Auflösung: %wx%h",
-	"vcmi.systemOptions.resolutionButton.help"  : "{Wähle Auflösung}\n\n Ändert die Spielauflösung. Spielneustart ist erforderlich um neue Auflösung zu übernehmen.",
+	"vcmi.systemOptions.resolutionButton.help"  : "{Wähle Auflösung}\n\n Ändert die Spielauflösung.",
 	"vcmi.systemOptions.resolutionMenu.hover"   : "Wähle Auflösung",
 	"vcmi.systemOptions.resolutionMenu.help"    : "Ändere die Spielauflösung.",
-	"vcmi.systemOptions.fullscreenFailed"       : "{Vollbild}\n\n Der Wechsel in den Vollbildmodus ist fehlgeschlagen! Die aktuelle Auflösung wird von der Anzeige nicht unterstützt!",
+	"vcmi.systemOptions.scalingButton.hover"   : "Interface-Skalierung: %p%",
+	"vcmi.systemOptions.scalingButton.help"    : "{Interface-Skalierung}\n\nÄndern der Skalierung des Interfaces im Spiel",
+	"vcmi.systemOptions.scalingMenu.hover"     : "Skalierung des Interfaces auswählen",
+	"vcmi.systemOptions.scalingMenu.help"      : "Ändern der Skalierung des Interfaces im Spiel.",
 	"vcmi.systemOptions.framerateButton.hover"  : "FPS anzeigen",
 	"vcmi.systemOptions.framerateButton.help"   : "{FPS anzeigen}\n\n Schaltet die Sichtbarkeit des Zählers für die Bilder pro Sekunde in der Ecke des Spielfensters um.",
 
@@ -88,9 +102,12 @@
 	"vcmi.battleOptions.animationsSpeed1.help": "Setzt die Animationsgeschwindigkeit auf sehr langsam",
 	"vcmi.battleOptions.animationsSpeed5.help": "Setzt die Animationsgeschwindigkeit auf sehr schnell",
 	"vcmi.battleOptions.animationsSpeed6.help": "Setzt die Animationsgeschwindigkeit auf sofort",
+	"vcmi.battleOptions.touchscreenMode.hover": "Touchscreen-Modus",
+	"vcmi.battleOptions.touchscreenMode.help": "{Touchscreen-Modus}\n\nFalls aktiviert, ist ein zweiter Klick erforderlich, um die Aktion zu bestätigen und auszuführen. Dies ist besser für Touchscreen-Geräte geeignet.",
+	"vcmi.battleOptions.movementHighlightOnHover.hover": "Hervorhebung der Bewegung bei Hover",
+	"vcmi.battleOptions.movementHighlightOnHover.help": "{Hervorhebung der Bewegung bei Hover}\n\nHebt die Bewegungsreichweite der Einheit hervor, wenn man mit dem Mauszeiger über sie fährt.",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Intro-Musik überspringen",
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Intro-Musik überspringen}\n\n Überspringe die kurze Musik, die zu Beginn eines jeden Kampfes gespielt wird, bevor die Action beginnt. Kann auch durch Drücken der ESC-Taste übersprungen werden.",
-	
 	"vcmi.battleWindow.pressKeyToSkipIntro" : "Beliebige Taste drücken, um das Kampf-Intro zu überspringen",
 
 	"vcmi.battleWindow.damageEstimation.melee" : "Angriff auf %CREATURE (%DAMAGE).",
@@ -103,10 +120,11 @@
 	"vcmi.battleWindow.damageEstimation.damage.1" : "%d Schaden",
 	"vcmi.battleWindow.damageEstimation.kills" : "%d werden verenden",
 	"vcmi.battleWindow.damageEstimation.kills.1" : "%d werden verenden",
+	"vcmi.battleResultsWindow.applyResultsLabel" : "Kampfergebnis übernehmen",
 
 	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Verfügbare Kreaturen anzeigen",
 	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Verfügbare Kreaturen anzeigen}\n\n Zeigt in der Stadtübersicht (linke untere Ecke) die zum Kauf verfügbaren Kreaturen anstelle ihres Wachstums an.",
-	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Wöchentliches Wachstum der Kreaturen anzeigen",
+	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Wöchentl. Wachstum der Kreaturen anz.",
 	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Wöchentliches Wachstum der Kreaturen anzeigen}\n\n Zeigt das wöchentliche Wachstum der Kreaturen anstelle der verfügbaren Menge in der Stadtübersicht (unten links).",
 	"vcmi.otherOptions.compactTownCreatureInfo.hover": "Kompakte Kreatur-Infos",
 	"vcmi.otherOptions.compactTownCreatureInfo.help": "{Kompakte Kreatur-Infos}\n\n Kleinere Stadt-Kreaturen Informationen in der Stadtübersicht.",
@@ -196,8 +214,6 @@
 	"core.bonus.DOUBLE_DAMAGE_CHANCE.description": "${val}% Chance auf doppelten Schaden",
 	"core.bonus.DRAGON_NATURE.name": "Drache",
 	"core.bonus.DRAGON_NATURE.description": "Kreatur hat eine Drachennatur",
-	"core.bonus.DIRECT_DAMAGE_IMMUNITY.name": "Direkte Schadensimmunität",
-	"core.bonus.DIRECT_DAMAGE_IMMUNITY.description": "Immun gegen Direktschadenszauber",
 	"core.bonus.EARTH_IMMUNITY.name": "Erdimmunität",
 	"core.bonus.EARTH_IMMUNITY.description": "Immun gegen alle Zauber der Erdschule",
 	"core.bonus.ENCHANTER.name": "Verzauberer",
@@ -294,7 +310,7 @@
 	"core.bonus.SUMMON_GUARDIANS.description": "Beschwört bei Kampfbeginn ${subtype.creature} (${val}%)",
 	"core.bonus.SYNERGY_TARGET.name": "Synergierbar",
 	"core.bonus.SYNERGY_TARGET.description": "Diese Kreatur ist anfällig für Synergieeffekte",
-	"core.bonus.TWO_HEX_ATTACK_BREATH.name": "Breath",
+	"core.bonus.TWO_HEX_ATTACK_BREATH.name": "Atem",
 	"core.bonus.TWO_HEX_ATTACK_BREATH.description": "Atem-Angriff (2-Hex-Bereich)",
 	"core.bonus.THREE_HEADED_ATTACK.name": "Dreiköpfiger Angriff",
 	"core.bonus.THREE_HEADED_ATTACK.description": "Greift drei benachbarte Einheiten an",

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

@@ -37,13 +37,10 @@
 	"vcmi.systemOptions.otherGroup" : "Inne ustawienia", // unused right now
 	"vcmi.systemOptions.townsGroup" : "Ekran miasta",
 
-	"vcmi.systemOptions.fullscreenButton.hover" : "Pełny ekran",
-	"vcmi.systemOptions.fullscreenButton.help"  : "{Pełny ekran}\n\n Po wybraniu VCMI uruchomi się w trybie pełnoekranowym, w przeciwnym wypadku uruchomi się w oknie",
 	"vcmi.systemOptions.resolutionButton.hover" : "Rozdzielczość: %wx%h",
 	"vcmi.systemOptions.resolutionButton.help"  : "{Wybierz rozdzielczość}\n\n Zmień rozdzielczość ekranu w grze. Restart gry jest wymagany, by zmiany zostały uwzględnione.",
 	"vcmi.systemOptions.resolutionMenu.hover"   : "Wybierz rozdzielczość",
 	"vcmi.systemOptions.resolutionMenu.help"    : "Zmień rozdzielczość ekranu w grze.",
-	"vcmi.systemOptions.fullscreenFailed"       : "{Pełny ekran}\n\n Nieudane przełączenie w tryb pełnoekranowy! Obecna rozdzielczość nie jest wspierana przez wyświetlacz!",
 	"vcmi.systemOptions.framerateButton.hover"  : "Pokaż FPS",
 	"vcmi.systemOptions.framerateButton.help"   : "{Pokaż FPS}\n\n Przełącza widoczność licznika klatek na sekundę (FPS) w rogu okna gry.",
 
@@ -175,8 +172,6 @@
 	"core.bonus.DOUBLE_DAMAGE_CHANCE.description": "${val}% szans na podwójne obrażenia",
 	"core.bonus.DRAGON_NATURE.name": "Smok",
 	"core.bonus.DRAGON_NATURE.description": "Stworzenie posiada smoczą naturę",
-	"core.bonus.DIRECT_DAMAGE_IMMUNITY.name": "Odporność na bezpośrednie obrażenia",
-	"core.bonus.DIRECT_DAMAGE_IMMUNITY.description": "Odporny na czary zadające bezpośrednie obrażenia",
 	"core.bonus.EARTH_IMMUNITY.name": "Odporność na ziemię",
 	"core.bonus.EARTH_IMMUNITY.description": "Odporny na wszystkie czary szkoły ziemi",
 	"core.bonus.ENCHANTER.name": "Czarodziej",

+ 0 - 5
Mods/vcmi/config/vcmi/russian.json

@@ -37,13 +37,10 @@
 	"vcmi.systemOptions.otherGroup" : "Иное", // unused right now
 	"vcmi.systemOptions.townsGroup" : "Экран города",
 
-	"vcmi.systemOptions.fullscreenButton.hover" : "Полный экран",
-	"vcmi.systemOptions.fullscreenButton.help"  : "{Полный экран}\n\n Если выбрано, то VCMI будет работать в полноэкранном режиме, если нет - в окне",
 	"vcmi.systemOptions.resolutionButton.hover" : "Разрешение %wx%h",
 	"vcmi.systemOptions.resolutionButton.help"  : "{Разрешение экрана}\n\n Изменение разрешения экрана. Для применения нового разрешения требуется перезапуск игры.",
 	"vcmi.systemOptions.resolutionMenu.hover"   : "Выбрать разрешения экрана",
 	"vcmi.systemOptions.resolutionMenu.help"    : "Изменение разрешения экрана в игре.",
-	"vcmi.systemOptions.fullscreenFailed"       : "{Полный экран}\n\n Невозможно переключиться в полноэкранный режим - выбранное разрешение не поддерживается дисплеем!",
 	"vcmi.systemOptions.framerateButton.hover"  : "Показывать частоту кадров",
 	"vcmi.systemOptions.framerateButton.help"   : "{Показывать частоту кадров}\n\n Включить счетчик частоты кадров в углу игрового клиента",
 
@@ -199,8 +196,6 @@
 	"core.bonus.DOUBLE_DAMAGE_CHANCE.description": "Шанс ${val}% на двойной урон",
 	"core.bonus.DRAGON_NATURE.name": "Дракон",
 	"core.bonus.DRAGON_NATURE.description": "Это существо - дракон",
-	"core.bonus.DIRECT_DAMAGE_IMMUNITY.name": "Иммунитет к магии прямого урона",
-	"core.bonus.DIRECT_DAMAGE_IMMUNITY.description": "Заклинания прямого урона не могут быть применены",
 	"core.bonus.EARTH_IMMUNITY.name": "Иммунитет к земле",
 	"core.bonus.EARTH_IMMUNITY.description": "Иммунитет ко всем заклинаниям Магии Земли",
 	"core.bonus.ENCHANTER.name": "Заклинатель (массовое)",

+ 0 - 5
Mods/vcmi/config/vcmi/spanish.json

@@ -46,13 +46,10 @@
 	"vcmi.systemOptions.otherGroup" : "Otras configuraciones", // actualmente no utilizada
 	"vcmi.systemOptions.townsGroup" : "Pantalla de la ciudad",
 
-	"vcmi.systemOptions.fullscreenButton.hover" : "Pantalla completa",
-	"vcmi.systemOptions.fullscreenButton.help"  : "{Pantalla completa}\n\n Si se selecciona, VCMI se ejecutará en modo de pantalla completa, de lo contrario se ejecutará en ventana",
 	"vcmi.systemOptions.resolutionButton.hover" : "Resolución: %wx%h",
 	"vcmi.systemOptions.resolutionButton.help"  : "{Seleccionar resolución}\n\n Cambia la resolución de la pantalla del juego. Se requiere reiniciar el juego para aplicar la nueva resolución.",
 	"vcmi.systemOptions.resolutionMenu.hover"   : "Seleccionar resolución",
 	"vcmi.systemOptions.resolutionMenu.help"    : "Cambia la resolución de la pantalla del juego.",
-	"vcmi.systemOptions.fullscreenFailed"       : "{Pantalla completa}\n\n ¡Fallo al cambiar a modo de pantalla completa! ¡La resolución actual no es compatible con la pantalla!",
 	"vcmi.systemOptions.framerateButton.hover"  : "Mostrar FPS",
 	"vcmi.systemOptions.framerateButton.help"   : "{Mostrar FPS}\n\n Muestra el contador de Frames Por Segundo en la esquina de la ventana del juego.",
 
@@ -201,8 +198,6 @@
 	"core.bonus.DOUBLE_DAMAGE_CHANCE.description": "${val}% de probabilidad de doble daño",
 	"core.bonus.DRAGON_NATURE.name": "Dragón",
 	"core.bonus.DRAGON_NATURE.description": "La criatura tiene la naturaleza de dragón",
-	"core.bonus.DIRECT_DAMAGE_IMMUNITY.name": "Inmunidad al Daño Directo",
-	"core.bonus.DIRECT_DAMAGE_IMMUNITY.description": "Inmune a hechizos de daño directo",
 	"core.bonus.EARTH_IMMUNITY.name": "Inmunidad a la Tierra",
 	"core.bonus.EARTH_IMMUNITY.description": "Inmune a todos los hechizos de la escuela de tierra",
 	"core.bonus.ENCHANTER.name": "Encantador",

+ 6 - 5
Mods/vcmi/config/vcmi/ukrainian.json

@@ -38,15 +38,18 @@
 	"vcmi.systemOptions.otherGroup" : "Інші налаштування",
 	"vcmi.systemOptions.townsGroup" : "Екран міста",
 
-    "vcmi.adventureOptions.infoBarPick.help" : "{Повідомлення у панелі статусу}\n\nЗа можливості, повідомлення про відвідування об'єктів карти пригод будуть відображені у панелі статусу замість окремого вікна",
-    "vcmi.adventureOptions.infoBarPick.hover" : "Повідомлення у панелі статусу",
+	"vcmi.systemOptions.fullscreenBorderless.hover" : "На весь екран (безрамкове вікно)",
+	"vcmi.systemOptions.fullscreenBorderless.help"  : "{На весь екран (безрамкове вікно)}\n\nЯкщо обрано, VCMI працюватиме у режимі безрамкового вікна на весь екран. У цьому режимі гра завжди використовує ту саму роздільну здатність, що й робочий стіл, ігноруючи вибрану роздільну здатність",
+	"vcmi.systemOptions.fullscreenExclusive.hover"  : "На весь екран (ексклюзивний режим)",
+	"vcmi.systemOptions.fullscreenExclusive.help"   : "{На весь екран (ексклюзивний режим)}\n\nnЯкщо вибрано, VCMI працюватиме у ексклюзивному повноекранному режимі. У цьому режимі гра змінюватиме роздільну здатність монітора на вибрану роздільну здатність",
+	"vcmi.adventureOptions.infoBarPick.help" : "{Повідомлення у панелі статусу}\n\nЗа можливості, повідомлення про відвідування об'єктів карти пригод будуть відображені у панелі статусу замість окремого вікна",
+	"vcmi.adventureOptions.infoBarPick.hover" : "Повідомлення у панелі статусу",
 	"vcmi.systemOptions.fullscreenButton.hover" : "Повноекранний режим",
 	"vcmi.systemOptions.fullscreenButton.help"  : "{Повноекранний режим}\n\n Якщо обрано, VCMI буде запускатися в режимі на весь екран, інакше — віконний режим",
 	"vcmi.systemOptions.resolutionButton.hover" : "Роздільна здатність: %wx%h",
 	"vcmi.systemOptions.resolutionButton.help"  : "{Роздільна здатність}\n\n Зміна розширення екрану в грі. Аби зміни набули чинності необхідно перезавантажити гру.",
 	"vcmi.systemOptions.resolutionMenu.hover"   : "Обрати роздільну здатність",
 	"vcmi.systemOptions.resolutionMenu.help"    : "Змінити роздільну здатність екрану в грі.",
-	"vcmi.systemOptions.fullscreenFailed"       : "{Повноекранний режим}\n\n Не вдалося перейти в повноекранний режим! Поточна роздільна здатність не підтримується дисплеєм!",
 	"vcmi.systemOptions.framerateButton.hover"  : "Лічильник кадрів",
 	"vcmi.systemOptions.framerateButton.help"   : "{Лічильник кадрів}\n\n Перемикає видимість лічильника кадрів на секунду у кутку ігрового вікна",
 
@@ -175,8 +178,6 @@
 	"core.bonus.DOUBLE_DAMAGE_CHANCE.description" : "${val}% шанс нанести подвійної шкоди",
 	"core.bonus.DRAGON_NATURE.name" : "Дракон",
 	"core.bonus.DRAGON_NATURE.description" : "Істота має драконячу природу",
-	"core.bonus.DIRECT_DAMAGE_IMMUNITY.name" : "Імунітет до прямої шкоди",
-	"core.bonus.DIRECT_DAMAGE_IMMUNITY.description" : "Імунітет до заклять, що завдають прямої шкоди",
 	"core.bonus.EARTH_IMMUNITY.name" : "Імунітет Землі",
 	"core.bonus.EARTH_IMMUNITY.description" : "Імунітет до всіх заклять школи Землі",
 	"core.bonus.ENCHANTER.name" : "Чарівник",

+ 1 - 1
android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityMods.java

@@ -50,7 +50,7 @@ import eu.vcmi.vcmi.util.ServerResponse;
 public class ActivityMods extends ActivityWithToolbar
 {
     private static final boolean ENABLE_REPO_DOWNLOADING = true;
-    private static final String REPO_URL = "https://raw.githubusercontent.com/vcmi/vcmi-mods-repository/develop/vcmi-1.2.json";
+    private static final String REPO_URL = "https://raw.githubusercontent.com/vcmi/vcmi-mods-repository/develop/vcmi-1.3.json";
     private VCMIModsRepo mRepo;
     private RecyclerView mRecycler;
 

+ 21 - 655
client/CMT.cpp

@@ -7,61 +7,42 @@
  * Full text of license available in license.txt file, in main folder
  *
  */
+
 // CMT.cpp : Defines the entry point for the console application.
-//
 #include "StdInc.h"
+#include "CMT.h"
 
 #include "CGameInfo.h"
 #include "mainmenu/CMainMenu.h"
-#include "lobby/CSelectionBase.h"
-#include "windows/CCastleInterface.h"
 #include "gui/CursorHandler.h"
+#include "eventsSDL/InputHandler.h"
 #include "CPlayerInterface.h"
 #include "CVideoHandler.h"
 #include "CMusicHandler.h"
-#include "Client.h"
 #include "gui/CGuiHandler.h"
+#include "gui/WindowHandler.h"
 #include "CServerHandler.h"
-#include "gui/NotificationHandler.h"
 #include "ClientCommandManager.h"
 #include "windows/CMessage.h"
-#include "renderSDL/SDL_Extensions.h"
+#include "render/IScreenHandler.h"
 
 #include "../lib/filesystem/Filesystem.h"
-#include "../lib/filesystem/FileStream.h"
-#include "../lib/CConsoleHandler.h"
-#include "../lib/CGameState.h"
-#include "../lib/CBuildingHandler.h"
-#include "../CCallback.h"
-#include "../lib/CHeroHandler.h"
-#include "../lib/spells/CSpellHandler.h"
 #include "../lib/CGeneralTextHandler.h"
-#include "../lib/serializer/BinaryDeserializer.h"
-#include "../lib/serializer/BinarySerializer.h"
 #include "../lib/VCMIDirs.h"
-#include "../lib/NetPacks.h"
-#include "../lib/CModHandler.h"
-#include "../lib/CTownHandler.h"
+#include "../lib/CConfigHandler.h"
+
 #include "../lib/logging/CBasicLogConfigurator.h"
-#include "../lib/CPlayerState.h"
-#include "../lib/serializer/Connection.h"
 
-#include <boost/asio.hpp>
 #include <boost/program_options.hpp>
-
-#include "mainmenu/CPrologEpilogVideo.h"
 #include <vstd/StringUtils.h>
-#include <SDL.h>
 
-#ifdef VCMI_WINDOWS
-#include <SDL_syswm.h>
-#endif
+#include <SDL_main.h>
+
 #ifdef VCMI_ANDROID
 #include "../lib/CAndroidVMHelper.h"
+#include <SDL_system.h>
 #endif
 
-#include "CMT.h"
-
 #if __MINGW32__
 #undef main
 #endif
@@ -70,44 +51,18 @@ namespace po = boost::program_options;
 namespace po_style = boost::program_options::command_line_style;
 namespace bfs = boost::filesystem;
 
-std::string NAME_AFFIX = "client";
-std::string NAME = GameConstants::VCMI_VERSION + std::string(" (") + NAME_AFFIX + ')'; //application name
-CGuiHandler GH;
-
-int preferredDriverIndex = -1;
-SDL_Window * mainWindow = nullptr;
-SDL_Renderer * mainRenderer = nullptr;
-SDL_Texture * screenTexture = nullptr;
-
 extern boost::thread_specific_ptr<bool> inGuiThread;
 
-SDL_Surface *screen = nullptr, //main screen surface
-	*screen2 = nullptr, //and hlp surface (used to store not-active interfaces layer)
-	*screenBuf = screen; //points to screen (if only advmapint is present) or screen2 (else) - should be used when updating controls which are not regularly redrawed
-
-std::queue<SDL_Event> SDLEventsQueue;
-boost::mutex eventsM;
-
 static po::variables_map vm;
 
-//static bool setResolution = false; //set by event handling thread after resolution is adjusted
-
 #ifndef VCMI_IOS
 void processCommand(const std::string &message);
 #endif
-static void setScreenRes(int w, int h, int bpp, bool fullscreen, int displayIndex, bool resetVideo=true);
 void playIntro();
 static void mainLoop();
 
 static CBasicLogConfigurator *logConfig;
 
-#ifndef VCMI_WINDOWS
-#ifndef _GNU_SOURCE
-#define _GNU_SOURCE
-#endif
-#include <getopt.h>
-#endif
-
 void init()
 {
 	CStopWatch tmh;
@@ -139,17 +94,6 @@ static void prog_help(const po::options_description &opts)
 	std::cout << opts;
 }
 
-static void SDLLogCallback(void*           userdata,
-						   int             category,
-						   SDL_LogPriority priority,
-						   const char*     message)
-{
-	//todo: convert SDL log priority to vcmi log priority
-	//todo: make separate log domain for SDL
-
-	logGlobal->debug("SDL(category %d; priority %d) %s", category, priority, message);
-}
-
 #if defined(VCMI_WINDOWS) && !defined(__GNUC__) && defined(VCMI_WITH_DEBUG_CONSOLE)
 int wmain(int argc, wchar_t* argv[])
 #elif defined(VCMI_MOBILE)
@@ -255,7 +199,7 @@ int main(int argc, char * argv[])
 	const bfs::path logPath = VCMIDirs::get().userLogsPath() / "VCMI_Client_log.txt";
 	logConfig = new CBasicLogConfigurator(logPath, console);
 	logConfig->configureDefault();
-	logGlobal->info(NAME);
+	logGlobal->info("Starting client of '%s'", GameConstants::VCMI_VERSION);
 	logGlobal->info("Creating console and configuring logger: %d ms", pomtime.getDiff());
 	logGlobal->info("The log file will be saved to %s", logPath);
 
@@ -338,73 +282,11 @@ int main(int argc, char * argv[])
 	testFile("VIDEO/GOOD1A.SMK", "campaign movies");
 	testFile("SOUNDS/G1A.WAV", "campaign music"); //technically not a music but voiced intro sounds
 
-	conf.init();
-	logGlobal->info("Loading settings: %d ms", pomtime.getDiff());
-
 	srand ( (unsigned int)time(nullptr) );
 
-
-	const JsonNode& video = settings["video"];
-	const JsonNode& res = video["screenRes"];
-
-	//something is really wrong...
-	if (res["width"].Float() < 100 || res["height"].Float() < 100)
-	{
-		logGlobal->error("Fatal error: failed to load settings!");
-		logGlobal->error("Possible reasons:");
-		logGlobal->error("\tCorrupted local configuration file at %s/settings.json", VCMIDirs::get().userConfigPath());
-		logGlobal->error("\tMissing or corrupted global configuration file at %s/schemas/settings.json", VCMIDirs::get().userConfigPath());
-		logGlobal->error("VCMI will now exit...");
-		exit(EXIT_FAILURE);
-	}
-
 	if(!settings["session"]["headless"].Bool())
-	{
-		if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_AUDIO|SDL_INIT_NOPARACHUTE))
-		{
-			logGlobal->error("Something was wrong: %s", SDL_GetError());
-			exit(-1);
-		}
-
-		#ifdef VCMI_ANDROID
-		// manually setting egl pixel format, as a possible solution for sdl2<->android problem
-		// https://bugzilla.libsdl.org/show_bug.cgi?id=2291
-		SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5);
-		SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 6);
-		SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5);
-		SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 0);
-		#endif // VCMI_ANDROID
-
-		//(!)init here AFTER SDL_Init() while using SDL for FPS management
 		GH.init();
 
-		SDL_LogSetOutputFunction(&SDLLogCallback, nullptr);
-
-		int driversCount = SDL_GetNumRenderDrivers();
-		std::string preferredDriverName = video["driver"].String();
-
-		logGlobal->info("Found %d render drivers", driversCount);
-
-		for(int it = 0; it < driversCount; it++)
-		{
-			SDL_RendererInfo info;
-			SDL_GetRenderDriverInfo(it,&info);
-
-			std::string driverName(info.name);
-
-			if(!preferredDriverName.empty() && driverName == preferredDriverName)
-			{
-				preferredDriverIndex = it;
-				logGlobal->info("\t%s (active)", driverName);
-			}
-			else
-				logGlobal->info("\t%s", driverName);
-		}
-
-		setScreenRes((int)res["width"].Float(), (int)res["height"].Float(), (int)video["bitsPerPixel"].Float(), video["fullscreen"].Bool(), (int)video["displayIndex"].Float());
-		logGlobal->info("\tInitializing screen: %d ms", pomtime.getDiff());
-	}
-
 	CCS = new CClientState();
 	CGI = new CGameInfo(); //contains all global informations about game (texts, lodHandlers, map handler etc.)
 	CSH = new CServerHandler();
@@ -433,19 +315,6 @@ int main(int argc, char * argv[])
 		logGlobal->info("Initializing screen and sound handling: %d ms", pomtime.getDiff());
 	}
 
-#ifdef VCMI_MAC
-	// Ctrl+click should be treated as a right click on Mac OS X
-	SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, "1");
-#endif
-
-#ifdef SDL_HINT_MOUSE_TOUCH_EVENTS
-	if(GH.isPointerRelativeMode)
-	{
-		SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0");
-		SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0");
-	}
-#endif
-
 #ifndef VCMI_NO_THREADED_LOAD
 	//we can properly play intro only in the main thread, so we have to move loading to the separate thread
 	boost::thread loading(init);
@@ -457,9 +326,7 @@ int main(int argc, char * argv[])
 	{
 		if(!vm.count("battle") && !vm.count("nointro") && settings["video"]["showIntro"].Bool())
 			playIntro();
-		SDL_SetRenderDrawColor(mainRenderer, 0, 0, 0, 255);
-		SDL_RenderClear(mainRenderer);
-		SDL_RenderPresent(mainRenderer);
+		GH.screenHandler().clearScreen();
 	}
 
 
@@ -490,7 +357,6 @@ int main(int argc, char * argv[])
 		CCS->curh->show();
 	}
 
-
 	logGlobal->info("Initialization of VCMI (together): %d ms", total.getDiff());
 
 	session["autoSkip"].Bool()  = vm.count("autoSkip");
@@ -580,490 +446,20 @@ void playIntro()
 	}
 }
 
-#if !defined(VCMI_MOBILE)
-static bool checkVideoMode(int monitorIndex, int w, int h)
-{
-	//we only check that our desired window size fits on screen
-	SDL_DisplayMode mode;
-
-	if (0 != SDL_GetDesktopDisplayMode(monitorIndex, &mode))
-	{
-		logGlobal->error("SDL_GetDesktopDisplayMode failed");
-		logGlobal->error(SDL_GetError());
-		return false;
-	}
-
-	logGlobal->info("Check display mode: requested %d x %d; available up to %d x %d ", w, h, mode.w, mode.h);
-
-	if (!mode.w || !mode.h || (w <= mode.w && h <= mode.h))
-	{
-		return true;
-	}
-
-	return false;
-}
-#endif
-
-static void cleanupRenderer()
-{
-	screenBuf = nullptr; //it`s a link - just nullify
-
-	if(nullptr != screen2)
-	{
-		SDL_FreeSurface(screen2);
-		screen2 = nullptr;
-	}
-
-	if(nullptr != screen)
-	{
-		SDL_FreeSurface(screen);
-		screen = nullptr;
-	}
-
-	if(nullptr != screenTexture)
-	{
-		SDL_DestroyTexture(screenTexture);
-		screenTexture = nullptr;
-	}
-}
-
-static bool recreateWindow(int w, int h, int bpp, bool fullscreen, int displayIndex)
-{
-	// VCMI will only work with 2 or 4 bytes per pixel
-	vstd::amax(bpp, 16);
-	vstd::amin(bpp, 32);
-	if(bpp>16)
-		bpp = 32;
-
-	if(displayIndex < 0)
-	{
-		if (mainWindow != nullptr)
-			displayIndex = SDL_GetWindowDisplayIndex(mainWindow);
-		if (displayIndex < 0)
-			displayIndex = 0;
-	}
-
-#if defined(VCMI_MOBILE)
-	SDL_GetWindowSize(mainWindow, &w, &h);
-#else
-	if(!checkVideoMode(displayIndex, w, h))
-	{
-		logGlobal->error("Error: SDL says that %dx%d resolution is not available!", w, h);
-	}
-#endif
-
-	bool bufOnScreen = (screenBuf == screen);
-	bool realFullscreen = settings["video"]["realFullscreen"].Bool();
-
-	/* match best rendering resolution */
-	int renderWidth = 0, renderHeight = 0;
-	auto aspectRatio = (float)w / (float)h;
-	auto minDiff = 10.f;
-	for (const auto& pair : conf.guiOptions)
-	{
-		int pWidth, pHeight;
-		std::tie(pWidth, pHeight) = pair.first;
-		/* filter out resolution which is larger than window */
-		if (pWidth > w || pHeight > h)
-		{
-			continue;
-		}
-		auto ratio = (float)pWidth / (float)pHeight;
-		auto diff = fabs(aspectRatio - ratio);
-		/* select closest aspect ratio */
-		if (diff < minDiff)
-		{
-			renderWidth = pWidth;
-			renderHeight = pHeight;
-			minDiff = diff;
-		}
-		/* select largest resolution meets prior conditions.
-		 * since there are resolutions like 1366x768(not exactly 16:9), a deviation of 0.005 is allowed. */
-		else if (fabs(diff - minDiff) < 0.005f && pWidth > renderWidth)
-		{
-			renderWidth = pWidth;
-			renderHeight = pHeight;
-		}
-	}
-	if (renderWidth == 0)
-	{
-		// no matching resolution for upscaling - complain & fallback to default resolution.
-		logGlobal->error("Failed to match rendering resolution for %dx%d!", w, h);
-		Settings newRes = settings.write["video"]["screenRes"];
-		std::tie(w, h) = conf.guiOptions.begin()->first;
-		newRes["width"].Float() = w;
-		newRes["height"].Float() = h;
-		conf.SetResolution(w, h);
-		logGlobal->error("Falling back to %dx%d", w, h);
-		renderWidth = w;
-		renderHeight = h;
-	}
-	else
-	{
-		logGlobal->info("Set logical rendering resolution to %dx%d", renderWidth, renderHeight);
-	}
-
-	cleanupRenderer();
-
-	if(nullptr == mainWindow)
-	{
-#if defined(VCMI_MOBILE)
-		auto createWindow = [displayIndex](uint32_t extraFlags) -> bool {
-			mainWindow = SDL_CreateWindow(NAME.c_str(), SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex), SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex), 0, 0, SDL_WINDOW_FULLSCREEN | extraFlags);
-			return mainWindow != nullptr;
-		};
-
-# ifdef VCMI_IOS
-		SDL_SetHint(SDL_HINT_IOS_HIDE_HOME_INDICATOR, "1");
-		SDL_SetHint(SDL_HINT_RETURN_KEY_HIDES_IME, "1");
-		SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best");
-
-		uint32_t windowFlags = SDL_WINDOW_BORDERLESS | SDL_WINDOW_ALLOW_HIGHDPI;
-		if(!createWindow(windowFlags | SDL_WINDOW_METAL))
-		{
-			logGlobal->warn("Metal unavailable, using OpenGLES");
-			createWindow(windowFlags);
-		}
-# else
-		createWindow(0);
-# endif // VCMI_IOS
-
-		// SDL on mobile doesn't do proper letterboxing, and will show an annoying flickering in the blank space in case you're not using the full screen estate
-		// That's why we need to make sure our width and height we'll use below have the same aspect ratio as the screen itself to ensure we fill the full screen estate
-
-		SDL_Rect screenRect;
-
-		if(SDL_GetDisplayBounds(0, &screenRect) == 0)
-		{
-			const auto screenWidth = screenRect.w;
-			const auto screenHeight = screenRect.h;
-
-			const auto aspect = static_cast<double>(screenWidth) / screenHeight;
-
-			logGlobal->info("Screen size and aspect ratio: %dx%d (%lf)", screenWidth, screenHeight, aspect);
-
-			if((double)w / aspect > (double)h)
-			{
-				h = (int)round((double)w / aspect);
-			}
-			else
-			{
-				w = (int)round((double)h * aspect);
-			}
-
-			logGlobal->info("Changing logical screen size to %dx%d", w, h);
-		}
-		else
-		{
-			logGlobal->error("Can't fix aspect ratio for screen");
-		}
-#else
-		if(fullscreen)
-		{
-			if(realFullscreen)
-				mainWindow = SDL_CreateWindow(NAME.c_str(), SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex), SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex), renderWidth, renderHeight, SDL_WINDOW_FULLSCREEN);
-			else //in windowed full-screen mode use desktop resolution
-				mainWindow = SDL_CreateWindow(NAME.c_str(), SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex),SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex), 0, 0, SDL_WINDOW_FULLSCREEN_DESKTOP);
-			SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best");
-		}
-		else
-		{
-			mainWindow = SDL_CreateWindow(NAME.c_str(), SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex),SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex), w, h, 0);
-		}
-#endif // defined(VCMI_MOBILE)
-
-		if(nullptr == mainWindow)
-		{
-			throw std::runtime_error("Unable to create window\n");
-		}
-
-		//create first available renderer if preferred not set. Use no flags, so HW accelerated will be preferred but SW renderer also will possible
-		mainRenderer = SDL_CreateRenderer(mainWindow,preferredDriverIndex,0);
-
-		if(nullptr == mainRenderer)
-		{
-			throw std::runtime_error("Unable to create renderer\n");
-		}
-
-
-		SDL_RendererInfo info;
-		SDL_GetRendererInfo(mainRenderer, &info);
-		logGlobal->info("Created renderer %s", info.name);
-
-	}
-	else
-	{
-#if !defined(VCMI_MOBILE)
-
-		if(fullscreen)
-		{
-			if(realFullscreen)
-			{
-				SDL_SetWindowFullscreen(mainWindow, SDL_WINDOW_FULLSCREEN);
-
-				SDL_DisplayMode mode;
-				SDL_GetDesktopDisplayMode(displayIndex, &mode);
-				mode.w = renderWidth;
-				mode.h = renderHeight;
-
-				SDL_SetWindowDisplayMode(mainWindow, &mode);
-			}
-			else
-			{
-				SDL_SetWindowFullscreen(mainWindow, SDL_WINDOW_FULLSCREEN_DESKTOP);
-			}
-
-			SDL_SetWindowPosition(mainWindow, SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex), SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex));
-
-			SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best");
-		}
-		else
-		{
-			SDL_SetWindowFullscreen(mainWindow, 0);
-			SDL_SetWindowSize(mainWindow, w, h);
-			SDL_SetWindowPosition(mainWindow, SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex), SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex));
-		}
-#endif
-	}
-
-	if(!(fullscreen && realFullscreen))
-	{
-		SDL_RenderSetLogicalSize(mainRenderer, renderWidth, renderHeight);
-
-//following line is bugged not only on android, do not re-enable without checking
-//#ifndef VCMI_ANDROID
-//		// on android this stretches the game to fit the screen, not preserving aspect and apparently this also breaks coordinates scaling in mouse events
-//		SDL_RenderSetViewport(mainRenderer, nullptr);
-//#endif
-
-	}
-
-
-	#ifdef VCMI_ENDIAN_BIG
-		int bmask = 0xff000000;
-		int gmask = 0x00ff0000;
-		int rmask = 0x0000ff00;
-		int amask = 0x000000ff;
-	#else
-		int bmask = 0x000000ff;
-		int gmask = 0x0000ff00;
-		int rmask = 0x00ff0000;
-		int amask = 0xFF000000;
-	#endif
-
-	screen = SDL_CreateRGBSurface(0,renderWidth,renderHeight,bpp,rmask,gmask,bmask,amask);
-	if(nullptr == screen)
-	{
-		logGlobal->error("Unable to create surface %dx%d with %d bpp: %s", renderWidth, renderHeight, bpp, SDL_GetError());
-		throw std::runtime_error("Unable to create surface");
-	}
-	//No blending for screen itself. Required for proper cursor rendering.
-	SDL_SetSurfaceBlendMode(screen, SDL_BLENDMODE_NONE);
-
-	screenTexture = SDL_CreateTexture(mainRenderer,
-											SDL_PIXELFORMAT_ARGB8888,
-											SDL_TEXTUREACCESS_STREAMING,
-											renderWidth, renderHeight);
-
-	if(nullptr == screenTexture)
-	{
-		logGlobal->error("Unable to create screen texture");
-		logGlobal->error(SDL_GetError());
-		throw std::runtime_error("Unable to create screen texture");
-	}
-
-	screen2 = CSDL_Ext::copySurface(screen);
-
-
-	if(nullptr == screen2)
-	{
-		throw std::runtime_error("Unable to copy surface\n");
-	}
-
-	screenBuf = bufOnScreen ? screen : screen2;
-
-	SDL_SetRenderDrawColor(mainRenderer, 0, 0, 0, 0);
-	SDL_RenderClear(mainRenderer);
-	SDL_RenderPresent(mainRenderer);
-
-	if(!settings["session"]["headless"].Bool() && settings["general"]["notifications"].Bool())
-	{
-		NotificationHandler::init(mainWindow);
-	}
-
-	return true;
-}
-
-//used only once during initialization
-static void setScreenRes(int w, int h, int bpp, bool fullscreen, int displayIndex, bool resetVideo)
-{
-	if(!recreateWindow(w, h, bpp, fullscreen, displayIndex))
-	{
-		throw std::runtime_error("Requested screen resolution is not available\n");
-	}
-}
-
-static void fullScreenChanged()
-{
-	boost::unique_lock<boost::recursive_mutex> lock(*CPlayerInterface::pim);
-
-	Settings full = settings.write["video"]["fullscreen"];
-	const bool toFullscreen = full->Bool();
-
-	auto bitsPerPixel = screen->format->BitsPerPixel;
-
-	auto w = screen->w;
-	auto h = screen->h;
-
-	if(!recreateWindow(w, h, bitsPerPixel, toFullscreen, -1))
-	{
-		//will return false and report error if video mode is not supported
-		return;
-	}
-
-	GH.totalRedraw();
-}
-
-static void handleEvent(SDL_Event & ev)
-{
-	if((ev.type==SDL_QUIT) ||(ev.type == SDL_KEYDOWN && ev.key.keysym.sym==SDLK_F4 && (ev.key.keysym.mod & KMOD_ALT)))
-	{
-#ifdef VCMI_ANDROID
-		handleQuit(false);
-#else
-		handleQuit();
-#endif
-		return;
-	}
-#ifdef VCMI_ANDROID
-	else if (ev.type == SDL_KEYDOWN && ev.key.keysym.scancode == SDL_SCANCODE_AC_BACK)
-	{
-		handleQuit(true);
-	}
-#endif
-	else if(ev.type == SDL_KEYDOWN && ev.key.keysym.sym==SDLK_F4)
-	{
-		Settings full = settings.write["video"]["fullscreen"];
-		full->Bool() = !full->Bool();
-		return;
-	}
-	else if(ev.type == SDL_USEREVENT)
-	{
-		switch(static_cast<EUserEvent>(ev.user.code))
-		{
-		case EUserEvent::FORCE_QUIT:
-			{
-				handleQuit(false);
-				return;
-			}
-			break;
-		case EUserEvent::RETURN_TO_MAIN_MENU:
-			{
-				CSH->endGameplay();
-				GH.defActionsDef = 63;
-				CMM->menu->switchToTab("main");
-			}
-			break;
-		case EUserEvent::RESTART_GAME:
-			{
-				CSH->sendRestartGame();
-			}
-			break;
-		case EUserEvent::CAMPAIGN_START_SCENARIO:
-			{
-				CSH->campaignServerRestartLock.set(true);
-				CSH->endGameplay();
-				auto ourCampaign = std::shared_ptr<CCampaignState>(reinterpret_cast<CCampaignState *>(ev.user.data1));
-				auto & epilogue = ourCampaign->camp->scenarios[ourCampaign->mapsConquered.back()].epilog;
-				auto finisher = [=]()
-				{
-					if(ourCampaign->mapsRemaining.size())
-					{
-						GH.pushInt(CMM);
-						GH.pushInt(CMM->menu);
-						CMM->openCampaignLobby(ourCampaign);
-					}
-				};
-				if(epilogue.hasPrologEpilog)
-				{
-					GH.pushIntT<CPrologEpilogVideo>(epilogue, finisher);
-				}
-				else
-				{
-					CSH->campaignServerRestartLock.waitUntil(false);
-					finisher();
-				}
-			}
-			break;
-		case EUserEvent::RETURN_TO_MENU_LOAD:
-			CSH->endGameplay();
-			GH.defActionsDef = 63;
-			CMM->menu->switchToTab("load");
-			break;
-		case EUserEvent::FULLSCREEN_TOGGLED:
-			fullScreenChanged();
-			break;
-		default:
-			logGlobal->error("Unknown user event. Code %d", ev.user.code);
-			break;
-		}
-
-		return;
-	}
-	else if(ev.type == SDL_WINDOWEVENT)
-	{
-		switch (ev.window.event) {
-		case SDL_WINDOWEVENT_RESTORED:
-#ifndef VCMI_IOS
-			fullScreenChanged();
-#endif
-			break;
-		}
-		return;
-	}
-	else if(ev.type == SDL_SYSWMEVENT)
-	{
-		if(!settings["session"]["headless"].Bool() && settings["general"]["notifications"].Bool())
-		{
-			NotificationHandler::handleSdlEvent(ev);
-		}
-	}
-
-	//preprocessing
-	if(ev.type == SDL_MOUSEMOTION)
-	{
-		CCS->curh->cursorMove(ev.motion.x, ev.motion.y);
-	}
-
-	{
-		boost::unique_lock<boost::mutex> lock(eventsM);
-		SDLEventsQueue.push(ev);
-	}
-
-}
-
-
 static void mainLoop()
 {
-	SettingsListener resChanged = settings.listen["video"]["fullscreen"];
-	resChanged([](const JsonNode &newState){  CGuiHandler::pushUserEvent(EUserEvent::FULLSCREEN_TOGGLED); });
+	SettingsListener resChanged = settings.listen["video"]["resolution"];
+	SettingsListener fsChanged = settings.listen["video"]["fullscreen"];
+	resChanged([](const JsonNode &newState){  GH.pushUserEvent(EUserEvent::FULLSCREEN_TOGGLED); });
+	fsChanged([](const JsonNode &newState){  GH.pushUserEvent(EUserEvent::FULLSCREEN_TOGGLED); });
 
 	inGuiThread.reset(new bool(true));
-	assert(GH.mainFPSmng);
-	GH.mainFPSmng->init(settings["video"]["targetfps"].Integer());
 
 	while(1) //main SDL events loop
 	{
-		SDL_Event ev;
-
-		while(1 == SDL_PollEvent(&ev))
-		{
-			handleEvent(ev);
-		}
-
+		GH.input().fetchEvents();
 		CSH->applyPacksOnLobbyScreen();
 		GH.renderFrame();
-
 	}
 }
 
@@ -1075,8 +471,7 @@ static void quitApplication()
 			CSH->endGameplay();
 	}
 
-	GH.listInt.clear();
-	GH.objsToBlit.clear();
+	GH.windows().clear();
 
 	CMM.reset();
 
@@ -1100,29 +495,9 @@ static void quitApplication()
 	vstd::clear_pointer(console);// should be removed after everything else since used by logging
 
 	boost::this_thread::sleep(boost::posix_time::milliseconds(750));//???
-	if(!settings["session"]["headless"].Bool())
-	{
-		if(settings["general"]["notifications"].Bool())
-		{
-			NotificationHandler::destroy();
-		}
-
-		cleanupRenderer();
 
-		if(nullptr != mainRenderer)
-		{
-			SDL_DestroyRenderer(mainRenderer);
-			mainRenderer = nullptr;
-		}
-
-		if(nullptr != mainWindow)
-		{
-			SDL_DestroyWindow(mainWindow);
-			mainWindow = nullptr;
-		}
-
-		SDL_Quit();
-	}
+	if(!settings["session"]["headless"].Bool())
+		GH.screenHandler().close();
 
 	if(logConfig != nullptr)
 	{
@@ -1137,19 +512,10 @@ static void quitApplication()
 
 void handleQuit(bool ask)
 {
-
 	if(CSH->client && LOCPLINT && ask)
 	{
 		CCS->curh->set(Cursor::Map::POINTER);
-		LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[69], [](){
-			// Workaround for assertion failure on exit:
-			// handleQuit() is alway called during SDL event processing
-			// during which, eventsM is kept locked
-			// this leads to assertion failure if boost::mutex is in locked state
-			eventsM.unlock();
-
-			quitApplication();
-		}, nullptr);
+		LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[69], quitApplication, nullptr);
 	}
 	else
 	{

+ 37 - 9
client/CMakeLists.txt

@@ -2,9 +2,10 @@ set(client_SRCS
 	StdInc.cpp
 	../CCallback.cpp
 
-	adventureMap/CAdvMapPanel.cpp
-	adventureMap/CAdventureMapInterface.cpp
-	adventureMap/CAdventureOptions.cpp
+	adventureMap/AdventureMapInterface.cpp
+	adventureMap/AdventureMapShortcuts.cpp
+	adventureMap/AdventureMapWidget.cpp
+	adventureMap/AdventureOptions.cpp
 	adventureMap/CInGameConsole.cpp
 	adventureMap/CInfoBar.cpp
 	adventureMap/CList.cpp
@@ -26,12 +27,23 @@ set(client_SRCS
 	battle/BattleWindow.cpp
 	battle/CreatureAnimation.cpp
 
+	eventsSDL/NotificationHandler.cpp
+	eventsSDL/InputHandler.cpp
+	eventsSDL/UserEventHandler.cpp
+	eventsSDL/InputSourceKeyboard.cpp
+	eventsSDL/InputSourceMouse.cpp
+	eventsSDL/InputSourceText.cpp
+	eventsSDL/InputSourceTouch.cpp
+
 	gui/CGuiHandler.cpp
 	gui/CIntObject.cpp
 	gui/CursorHandler.cpp
+	gui/EventDispatcher.cpp
+	gui/EventsReceiver.cpp
 	gui/InterfaceObjectConfigurable.cpp
-	gui/NotificationHandler.cpp
+	gui/FramerateManager.cpp
 	gui/ShortcutHandler.cpp
+	gui/WindowHandler.cpp
 
 	lobby/CBonusSelection.cpp
 	lobby/CCampaignInfoScreen.cpp
@@ -75,6 +87,7 @@ set(client_SRCS
 	renderSDL/SDLImage.cpp
 	renderSDL/SDLImageLoader.cpp
 	renderSDL/SDLRWwrapper.cpp
+	renderSDL/ScreenHandler.cpp
 	renderSDL/SDL_Extensions.cpp
 
 	widgets/Buttons.cpp
@@ -129,9 +142,11 @@ set(client_SRCS
 set(client_HEADERS
 	StdInc.h
 
-	adventureMap/CAdvMapPanel.h
-	adventureMap/CAdventureMapInterface.h
-	adventureMap/CAdventureOptions.h
+	adventureMap/AdventureMapInterface.h
+	adventureMap/AdventureMapShortcuts.h
+	adventureMap/AdventureMapWidget.h
+	adventureMap/AdventureState.h
+	adventureMap/AdventureOptions.h
 	adventureMap/CInGameConsole.h
 	adventureMap/CInfoBar.h
 	adventureMap/CList.h
@@ -154,15 +169,26 @@ set(client_HEADERS
 	battle/BattleWindow.h
 	battle/CreatureAnimation.h
 
+	eventsSDL/NotificationHandler.h
+	eventsSDL/InputHandler.h
+	eventsSDL/UserEventHandler.h
+	eventsSDL/InputSourceKeyboard.h
+	eventsSDL/InputSourceMouse.h
+	eventsSDL/InputSourceText.h
+	eventsSDL/InputSourceTouch.h
+
 	gui/CGuiHandler.h
 	gui/CIntObject.h
 	gui/CursorHandler.h
+	gui/EventDispatcher.h
+	gui/EventsReceiver.h
 	gui/InterfaceObjectConfigurable.h
+	gui/FramerateManager.h
 	gui/MouseButton.h
-	gui/NotificationHandler.h
 	gui/Shortcut.h
 	gui/ShortcutHandler.h
 	gui/TextAlignment.h
+	gui/WindowHandler.h
 
 	lobby/CBonusSelection.h
 	lobby/CCampaignInfoScreen.h
@@ -202,6 +228,7 @@ set(client_HEADERS
 	render/IFont.h
 	render/IImage.h
 	render/IImageLoader.h
+	render/IScreenHandler.h
 
 	renderSDL/CBitmapFont.h
 	renderSDL/CBitmapHanFont.h
@@ -211,6 +238,7 @@ set(client_HEADERS
 	renderSDL/SDLImage.h
 	renderSDL/SDLImageLoader.h
 	renderSDL/SDLRWwrapper.h
+	renderSDL/ScreenHandler.h
 	renderSDL/SDL_Extensions.h
 	renderSDL/SDL_PixelAccess.h
 
@@ -323,7 +351,7 @@ if(WIN32)
 	endif()
 	target_compile_definitions(vcmiclient PRIVATE WINDOWS_IGNORE_PACKING_MISMATCH)
 
-	# TODO: very hacky, find proper solution to copy AI dlls into bin dir
+# TODO: very hacky, find proper solution to copy AI dlls into bin dir
 	if(MSVC)
 		add_custom_command(TARGET vcmiclient POST_BUILD
 			WORKING_DIRECTORY "$<TARGET_FILE_DIR:vcmiclient>"

+ 2 - 2
client/CMusicHandler.cpp

@@ -335,7 +335,7 @@ CMusicHandler::CMusicHandler():
 
 	auto mp3files = CResourceHandler::get()->getFilteredFiles([](const ResourceID & id) ->  bool
 	{
-		if(id.getType() != EResType::MUSIC)
+		if(id.getType() != EResType::SOUND)
 			return false;
 
 		if(!boost::algorithm::istarts_with(id.getName(), "MUSIC/"))
@@ -561,7 +561,7 @@ void MusicEntry::load(std::string musicURI)
 
 	try
 	{
-		auto musicFile = MakeSDLRWops(CResourceHandler::get()->load(ResourceID(std::move(musicURI), EResType::MUSIC)));
+		auto musicFile = MakeSDLRWops(CResourceHandler::get()->load(ResourceID(std::move(musicURI), EResType::SOUND)));
 		music = Mix_LoadMUS_RW(musicFile, SDL_TRUE);
 	}
 	catch(std::exception &e)

+ 87 - 137
client/CPlayerInterface.cpp

@@ -12,10 +12,9 @@
 
 #include <vcmi/Artifact.h>
 
-#include "adventureMap/CAdventureMapInterface.h"
+#include "adventureMap/AdventureMapInterface.h"
 #include "mapView/mapHandler.h"
 #include "adventureMap/CList.h"
-#include "adventureMap/CInfoBar.h"
 #include "battle/BattleInterface.h"
 #include "battle/BattleEffectsController.h"
 #include "battle/BattleFieldController.h"
@@ -23,6 +22,7 @@
 #include "battle/BattleWindow.h"
 #include "../CCallback.h"
 #include "windows/CCastleInterface.h"
+#include "eventsSDL/InputHandler.h"
 #include "gui/CursorHandler.h"
 #include "windows/CKingdomInterface.h"
 #include "CGameInfo.h"
@@ -66,6 +66,7 @@
 #include "../lib/CPlayerState.h"
 #include "../lib/GameConstants.h"
 #include "gui/CGuiHandler.h"
+#include "gui/WindowHandler.h"
 #include "windows/InfoWindows.h"
 #include "../lib/UnlockGuard.h"
 #include "../lib/CPathfinder.h"
@@ -74,11 +75,9 @@
 #include "CServerHandler.h"
 // FIXME: only needed for CGameState::mutex
 #include "../lib/CGameState.h"
-#include "gui/NotificationHandler.h"
+#include "eventsSDL/NotificationHandler.h"
 #include "adventureMap/CInGameConsole.h"
 
-#include <SDL_events.h>
-
 // The macro below is used to mark functions that are called by client when game state changes.
 // They all assume that CPlayerInterface::pim mutex is locked.
 #define EVENT_HANDLER_CALLED_BY_CLIENT
@@ -96,8 +95,6 @@
 		return;						\
 	RETURN_IF_QUICK_COMBAT
 
-extern std::queue<SDL_Event> SDLEventsQueue;
-extern boost::mutex eventsM;
 boost::recursive_mutex * CPlayerInterface::pim = new boost::recursive_mutex;
 
 CPlayerInterface * LOCPLINT;
@@ -163,22 +160,23 @@ void CPlayerInterface::initGameInterface(std::shared_ptr<Environment> ENV, std::
 	initializeHeroTownList();
 
 	// always recreate advmap interface to avoid possible memory-corruption bugs
-	adventureInt.reset(new CAdventureMapInterface());
+	adventureInt.reset(new AdventureMapInterface());
 }
 
 void CPlayerInterface::playerStartsTurn(PlayerColor player)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	if (!vstd::contains (GH.listInt, adventureInt))
+
+	if(GH.windows().findWindows<AdventureMapInterface>().empty())
 	{
 		// after map load - remove all active windows and replace them with adventure map
-		GH.popInts ((int)GH.listInt.size());
-		GH.pushInt (adventureInt);
+		GH.windows().clear();
+		GH.windows().pushWindow(adventureInt);
 	}
 
 	// remove all dialogs that do not expect query answer
-	while (GH.listInt.front() != adventureInt && !dynamic_cast<CInfoWindow*>(GH.listInt.front().get()))
-		GH.popInts(1);
+	while (!GH.windows().topWindow<AdventureMapInterface>() && !GH.windows().topWindow<CInfoWindow>())
+		GH.windows().popWindows(1);
 
 	if (player != playerID && LOCPLINT == this)
 	{
@@ -205,7 +203,7 @@ void CPlayerInterface::performAutosave()
 	}
 	else if(frequency > 0 && cb->getDate() % frequency == 0)
 	{
-		LOCPLINT->cb->save("Saves/" + prefix + "Autosave_" + std::to_string(autosaveCount++ + 1));
+		cb->save("Saves/" + prefix + "Autosave_" + std::to_string(autosaveCount++ + 1));
 		autosaveCount %= 5;
 	}
 }
@@ -214,8 +212,6 @@ void CPlayerInterface::yourTurn()
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	{
-		boost::unique_lock<boost::mutex> lock(eventsM); //block handling events until interface is ready
-
 		LOCPLINT = this;
 		GH.curInt = this;
 
@@ -246,7 +242,7 @@ void CPlayerInterface::acceptTurn()
 {
 	if (settings["session"]["autoSkip"].Bool())
 	{
-		while(CInfoWindow *iw = dynamic_cast<CInfoWindow *>(GH.topInt().get()))
+		while(auto iw = GH.windows().topWindow<CInfoWindow>())
 			iw->close();
 	}
 
@@ -371,22 +367,8 @@ void CPlayerInterface::heroMoved(const TryMoveHero & details, bool verbose)
 
 	//check if user cancelled movement
 	{
-		boost::unique_lock<boost::mutex> un(eventsM);
-		while(!SDLEventsQueue.empty())
-		{
-			SDL_Event ev = SDLEventsQueue.front();
-			SDLEventsQueue.pop();
-			switch(ev.type)
-			{
-			case SDL_MOUSEBUTTONDOWN:
-				stillMoveHero.setn(STOP_MOVE);
-				break;
-			case SDL_KEYDOWN:
-				if (ev.key.keysym.sym < SDLK_F1  ||  ev.key.keysym.sym > SDLK_F15)
-					stillMoveHero.setn(STOP_MOVE);
-				break;
-			}
-		}
+		if (GH.input().ignoreEventsUntilInput())
+			stillMoveHero.setn(STOP_MOVE);
 	}
 
 	if (stillMoveHero.get() == WAITING_MOVE)
@@ -440,7 +422,7 @@ void CPlayerInterface::openTownWindow(const CGTownInstance * town)
 
 	auto newCastleInt = std::make_shared<CCastleInterface>(town);
 
-	GH.pushInt(newCastleInt);
+	GH.windows().pushWindow(newCastleInt);
 }
 
 void CPlayerInterface::heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val)
@@ -448,7 +430,7 @@ void CPlayerInterface::heroPrimarySkillChanged(const CGHeroInstance * hero, int
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	if (which == 4)
 	{
-		if (CAltarWindow *ctw = dynamic_cast<CAltarWindow *>(GH.topInt().get()))
+		for (auto ctw : GH.windows().findWindows<CAltarWindow>())
 			ctw->setExpToLevel();
 	}
 	else
@@ -458,11 +440,8 @@ void CPlayerInterface::heroPrimarySkillChanged(const CGHeroInstance * hero, int
 void CPlayerInterface::heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	CUniversityWindow* cuw = dynamic_cast<CUniversityWindow*>(GH.topInt().get());
-	if (cuw) //university window is open
-	{
-		GH.totalRedraw();
-	}
+	for (auto cuw : GH.windows().findWindows<CUniversityWindow>())
+		cuw->redraw();
 }
 
 void CPlayerInterface::heroManaPointsChanged(const CGHeroInstance * hero)
@@ -481,10 +460,10 @@ void CPlayerInterface::heroMovePointsChanged(const CGHeroInstance * hero)
 void CPlayerInterface::receivedResource()
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	if (CMarketplaceWindow *mw = dynamic_cast<CMarketplaceWindow *>(GH.topInt().get()))
+	for (auto mw : GH.windows().findWindows<CMarketplaceWindow>())
 		mw->resourceChanged();
 
-	GH.totalRedraw();
+	GH.windows().totalRedraw();
 }
 
 void CPlayerInterface::heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector<SecondarySkill>& skills, QueryID queryID)
@@ -492,7 +471,7 @@ void CPlayerInterface::heroGotLevel(const CGHeroInstance *hero, PrimarySkill::Pr
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	waitWhileDialog();
 	CCS->soundh->playSound(soundBase::heroNewLevel);
-	GH.pushIntT<CLevelWindow>(hero, pskill, skills, [=](ui32 selection)
+	GH.windows().createAndPushWindow<CLevelWindow>(hero, pskill, skills, [=](ui32 selection)
 	{
 		cb->selectionMade(selection, queryID);
 	});
@@ -503,7 +482,7 @@ void CPlayerInterface::commanderGotLevel (const CCommanderInstance * commander,
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	waitWhileDialog();
 	CCS->soundh->playSound(soundBase::heroNewLevel);
-	GH.pushIntT<CStackWindow>(commander, skills, [=](ui32 selection)
+	GH.windows().createAndPushWindow<CStackWindow>(commander, skills, [=](ui32 selection)
 	{
 		cb->selectionMade(selection, queryID);
 	});
@@ -538,17 +517,15 @@ void CPlayerInterface::heroInGarrisonChange(const CGTownInstance *town)
 		castleInt->heroes->update();
 		castleInt->redraw();
 	}
-	for (auto isa : GH.listInt)
+
+	for (auto ki : GH.windows().findWindows<CKingdomInterface>())
 	{
-		CKingdomInterface *ki = dynamic_cast<CKingdomInterface*>(isa.get());
-		if (ki)
-		{
-			ki->townChanged(town);
-			ki->updateGarrisons();
-			ki->redraw();
-		}
+		ki->townChanged(town);
+		ki->updateGarrisons();
+		ki->redraw();
 	}
 }
+
 void CPlayerInterface::heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
@@ -590,20 +567,16 @@ void CPlayerInterface::garrisonsChanged(std::vector<const CGObjectInstance *> ob
 			adventureInt->onTownChanged(town);
 	}
 
-	for (auto & elem : GH.listInt)
-	{
-		CGarrisonHolder *cgh = dynamic_cast<CGarrisonHolder*>(elem.get());
-		if (cgh)
-			cgh->updateGarrisons();
+	for (auto cgh : GH.windows().findWindows<CGarrisonHolder>())
+		cgh->updateGarrisons();
 
-		if (CTradeWindow *cmw = dynamic_cast<CTradeWindow*>(elem.get()))
-		{
-			if (vstd::contains(objs, cmw->hero))
-				cmw->garrisonChanged();
-		}
+	for (auto cmw : GH.windows().findWindows<CTradeWindow>())
+	{
+		if (vstd::contains(objs, cmw->hero))
+			cmw->garrisonChanged();
 	}
 
-	GH.totalRedraw();
+	GH.windows().totalRedraw();
 }
 
 void CPlayerInterface::buildChanged(const CGTownInstance *town, BuildingID buildingID, int what) //what: 1 - built, 2 - demolished
@@ -862,7 +835,7 @@ void CPlayerInterface::battleEnd(const BattleResult *br, QueryID queryID)
 			{
 				cb->selectionMade(selection, queryID);
 			};
-			GH.pushInt(wnd);
+			GH.windows().pushWindow(wnd);
 			// #1490 - during AI turn when quick combat is on, we need to display the message and wait for user to close it.
 			// Otherwise NewTurn causes freeze.
 			waitWhileDialog();
@@ -1016,7 +989,7 @@ void CPlayerInterface::showInfoDialog(EInfoWindowMode type, const std::string &t
 		waitWhileDialog(); //Fix for mantis #98
 		adventureInt->showInfoBoxMessage(components, text, timer);
 
-		if (makingTurn && GH.listInt.size() && LOCPLINT == this)
+		if (makingTurn && GH.windows().count() > 0 && LOCPLINT == this)
 			CCS->soundh->playSound(static_cast<soundBase::soundID>(soundID));
 		return;
 	}
@@ -1057,12 +1030,12 @@ void CPlayerInterface::showInfoDialog(const std::string &text, const std::vector
 	}
 	std::shared_ptr<CInfoWindow> temp = CInfoWindow::create(text, playerID, components);
 
-	if (makingTurn && GH.listInt.size() && LOCPLINT == this)
+	if (makingTurn && GH.windows().count() > 0 && LOCPLINT == this)
 	{
 		CCS->soundh->playSound(static_cast<soundBase::soundID>(soundID));
 		showingDialog->set(true);
 		stopMovement(); // interrupt movement to show dialog
-		GH.pushInt(temp);
+		GH.windows().pushWindow(temp);
 	}
 	else
 	{
@@ -1122,7 +1095,7 @@ void CPlayerInterface::showBlockingDialog( const std::string &text, const std::v
 		int charperline = 35;
 		if (pom.size() > 1)
 			charperline = 50;
-		GH.pushIntT<CSelWindow>(text, playerID, charperline, intComps, pom, askID);
+		GH.windows().createAndPushWindow<CSelWindow>(text, playerID, charperline, intComps, pom, askID);
 		intComps[0]->clickLeft(true, false);
 	}
 }
@@ -1171,7 +1144,7 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component
 
 	std::shared_ptr<CObjectListWindow> wnd = std::make_shared<CObjectListWindow>(tempList, localIcon, localTitle, localDescription, selectCallback);
 	wnd->onExit = cancelCallback;
-	GH.pushInt(wnd);
+	GH.windows().pushWindow(wnd);
 }
 
 void CPlayerInterface::tileRevealed(const std::unordered_set<int3> &pos)
@@ -1190,7 +1163,7 @@ void CPlayerInterface::tileHidden(const std::unordered_set<int3> &pos)
 void CPlayerInterface::openHeroWindow(const CGHeroInstance *hero)
 {
 	boost::unique_lock<boost::recursive_mutex> un(*pim);
-	GH.pushIntT<CHeroWindow>(hero);
+	GH.windows().createAndPushWindow<CHeroWindow>(hero);
 }
 
 void CPlayerInterface::availableCreaturesChanged( const CGDwelling *town )
@@ -1198,27 +1171,22 @@ void CPlayerInterface::availableCreaturesChanged( const CGDwelling *town )
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	if (const CGTownInstance * townObj = dynamic_cast<const CGTownInstance*>(town))
 	{
-		CFortScreen * fortScreen = dynamic_cast<CFortScreen*>(GH.topInt().get());
-		CCastleInterface * castleInterface = dynamic_cast<CCastleInterface*>(GH.topInt().get());
-
-		if (fortScreen)
+		for (auto fortScreen : GH.windows().findWindows<CFortScreen>())
 			fortScreen->creaturesChangedEventHandler();
-		else if(castleInterface)
+
+		for (auto castleInterface : GH.windows().findWindows<CCastleInterface>())
 			castleInterface->creaturesChangedEventHandler();
 
-		for(auto isa : GH.listInt)
-		{
-			CKingdomInterface *ki = dynamic_cast<CKingdomInterface*>(isa.get());
-			if (ki && townObj)
+		if (townObj)
+			for (auto ki : GH.windows().findWindows<CKingdomInterface>())
 				ki->townChanged(townObj);
-		}
 	}
-	else if(town && GH.listInt.size() && (town->ID == Obj::CREATURE_GENERATOR1
+	else if(town && GH.windows().count() > 0 && (town->ID == Obj::CREATURE_GENERATOR1
 		||  town->ID == Obj::CREATURE_GENERATOR4  ||  town->ID == Obj::WAR_MACHINE_FACTORY))
 	{
-		CRecruitmentWindow *crw = dynamic_cast<CRecruitmentWindow*>(GH.topInt().get());
-		if (crw && crw->dwelling == town)
-			crw->availableCreaturesChanged();
+		for (auto crw : GH.windows().findWindows<CRecruitmentWindow>())
+			if (crw->dwelling == town)
+				crw->availableCreaturesChanged();
 	}
 }
 
@@ -1284,7 +1252,7 @@ void CPlayerInterface::showGarrisonDialog( const CArmedInstance *up, const CGHer
 
 	auto cgw = std::make_shared<CGarrisonWindow>(up, down, removableUnits);
 	cgw->quit->addCallback(onEnd);
-	GH.pushInt(cgw);
+	GH.windows().pushWindow(cgw);
 }
 
 /**
@@ -1342,7 +1310,7 @@ void CPlayerInterface::showHeroExchange(ObjectInstanceID hero1, ObjectInstanceID
 void CPlayerInterface::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	GH.pushIntT<CExchangeWindow>(hero1, hero2, query);
+	GH.windows().createAndPushWindow<CExchangeWindow>(hero1, hero2, query);
 }
 
 void CPlayerInterface::objectPropertyChanged(const SetObjectProperty * sop)
@@ -1403,7 +1371,7 @@ void CPlayerInterface::showRecruitmentDialog(const CGDwelling *dwelling, const C
 	{
 		LOCPLINT->cb->recruitCreatures(dwelling, dst, id, count, -1);
 	};
-	GH.pushIntT<CRecruitmentWindow>(dwelling, level, dst, recruitCb);
+	GH.windows().createAndPushWindow<CRecruitmentWindow>(dwelling, level, dst, recruitCb);
 }
 
 void CPlayerInterface::waitWhileDialog(bool unlockPim)
@@ -1426,7 +1394,7 @@ void CPlayerInterface::showShipyardDialog(const IShipyard *obj)
 	auto state = obj->shipyardStatus();
 	TResources cost;
 	obj->getBoatCost(cost);
-	GH.pushIntT<CShipyardWindow>(cost, state, obj->getBoatType(), [=](){ cb->buildBoat(obj); });
+	GH.windows().createAndPushWindow<CShipyardWindow>(cost, state, obj->getBoatType(), [=](){ cb->buildBoat(obj); });
 }
 
 void CPlayerInterface::newObject( const CGObjectInstance * obj )
@@ -1450,7 +1418,7 @@ void CPlayerInterface::centerView (int3 pos, int focusTime)
 	adventureInt->centerOnTile(pos);
 	if (focusTime)
 	{
-		GH.totalRedraw();
+		GH.windows().totalRedraw();
 		{
 			auto unlockPim = vstd::makeUnlockGuard(*pim);
 			IgnoreEvents ignore(*this);
@@ -1491,7 +1459,6 @@ void CPlayerInterface::playerBlocked(int reason, bool start)
 		if(CSH->howManyPlayerInterfaces() > 1 && LOCPLINT != this && LOCPLINT->makingTurn == false)
 		{
 			//one of our players who isn't last in order got attacked not by our another player (happens for example in hotseat mode)
-			boost::unique_lock<boost::mutex> lock(eventsM); //TODO: copied from yourTurn, no idea if it's needed
 			LOCPLINT = this;
 			GH.curInt = this;
 			adventureInt->onCurrentPlayerChanged(playerID);
@@ -1519,16 +1486,15 @@ void CPlayerInterface::update()
 	if ((CSH->howManyPlayerInterfaces() <= 1 || makingTurn) && !dialogs.empty() && !showingDialog->get())
 	{
 		showingDialog->set(true);
-		GH.pushInt(dialogs.front());
+		GH.windows().pushWindow(dialogs.front());
 		dialogs.pop_front();
 	}
 
 	assert(adventureInt);
 
 	// Handles mouse and key input
-	GH.updateTime();
 	GH.handleEvents();
-	GH.simpleRedraw();
+	GH.windows().simpleRedraw();
 }
 
 int CPlayerInterface::getLastIndex( std::string namePrefix)
@@ -1594,7 +1560,7 @@ void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResul
 			if(adventureInt)
 			{
 				GH.terminate_cond->setn(true);
-				GH.popInts(GH.listInt.size());
+				GH.windows().popWindows(GH.windows().count());
 				adventureInt.reset();
 			}
 		}
@@ -1640,7 +1606,7 @@ void CPlayerInterface::showPuzzleMap()
 	double ratio = 0;
 	int3 grailPos = cb->getGrailPos(&ratio);
 
-	GH.pushIntT<CPuzzleWindow>(grailPos, ratio);
+	GH.windows().createAndPushWindow<CPuzzleWindow>(grailPos, ratio);
 }
 
 void CPlayerInterface::viewWorldMap()
@@ -1652,8 +1618,8 @@ void CPlayerInterface::advmapSpellCast(const CGHeroInstance * caster, int spellI
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 
-	if(dynamic_cast<CSpellWindow *>(GH.topInt().get()))
-		GH.popInts(1);
+	if(GH.windows().topWindow<CSpellWindow>())
+		GH.windows().popWindows(1);
 
 	if(spellID == SpellID::FLY || spellID == SpellID::WATER_WALK)
 		localState->erasePath(caster);
@@ -1715,50 +1681,50 @@ void CPlayerInterface::showMarketWindow(const IMarket *market, const CGHeroInsta
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 
 	if(market->allowsTrade(EMarketMode::ARTIFACT_EXP) && visitor->getAlignment() != EAlignment::EVIL)
-		GH.pushIntT<CAltarWindow>(market, visitor, EMarketMode::ARTIFACT_EXP);
+		GH.windows().createAndPushWindow<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);
+		GH.windows().createAndPushWindow<CAltarWindow>(market, visitor, EMarketMode::CREATURE_EXP);
 	else if(market->allowsTrade(EMarketMode::CREATURE_UNDEAD))
-		GH.pushIntT<CTransformerWindow>(market, visitor);
+		GH.windows().createAndPushWindow<CTransformerWindow>(market, visitor);
 	else if(!market->availableModes().empty())
-		GH.pushIntT<CMarketplaceWindow>(market, visitor, market->availableModes().front());
+		GH.windows().createAndPushWindow<CMarketplaceWindow>(market, visitor, market->availableModes().front());
 }
 
 void CPlayerInterface::showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	GH.pushIntT<CUniversityWindow>(visitor, market);
+	GH.windows().createAndPushWindow<CUniversityWindow>(visitor, market);
 }
 
 void CPlayerInterface::showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	GH.pushIntT<CHillFortWindow>(visitor, object);
+	GH.windows().createAndPushWindow<CHillFortWindow>(visitor, object);
 }
 
 void CPlayerInterface::availableArtifactsChanged(const CGBlackMarket * bm)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	if (CMarketplaceWindow *cmw = dynamic_cast<CMarketplaceWindow*>(GH.topInt().get()))
+	for (auto cmw : GH.windows().findWindows<CMarketplaceWindow>())
 		cmw->artifactsChanged(false);
 }
 
 void CPlayerInterface::showTavernWindow(const CGObjectInstance *townOrTavern)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	GH.pushIntT<CTavernWindow>(townOrTavern);
+	GH.windows().createAndPushWindow<CTavernWindow>(townOrTavern);
 }
 
 void CPlayerInterface::showThievesGuildWindow (const CGObjectInstance * obj)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	GH.pushIntT<CThievesGuildWindow>(obj);
+	GH.windows().createAndPushWindow<CThievesGuildWindow>(obj);
 }
 
 void CPlayerInterface::showQuestLog()
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	GH.pushIntT<CQuestLog>(LOCPLINT->cb->getMyQuests());
+	GH.windows().createAndPushWindow<CQuestLog>(LOCPLINT->cb->getMyQuests());
 }
 
 void CPlayerInterface::showShipyardDialogOrProblemPopup(const IShipyard *obj)
@@ -1809,12 +1775,9 @@ void CPlayerInterface::artifactRemoved(const ArtifactLocation &al)
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	auto hero = std::visit(HeroObjectRetriever(), al.artHolder);
 	adventureInt->onHeroChanged(hero);
-	for(auto isa : GH.listInt)
-	{
-		auto artWin = dynamic_cast<CArtifactHolder*>(isa.get());
-		if (artWin)
-			artWin->artifactRemoved(al);
-	}
+
+	for(auto artWin : GH.windows().findWindows<CArtifactHolder>())
+		artWin->artifactRemoved(al);
 
 	waitWhileDialog();
 }
@@ -1834,12 +1797,9 @@ void CPlayerInterface::artifactMoved(const ArtifactLocation &src, const Artifact
 			redraw = false;
 	}
 
-	for(auto isa : GH.listInt)
-	{
-		auto artWin = dynamic_cast<CArtifactHolder*>(isa.get());
-		if (artWin)
-			artWin->artifactMoved(src, dst, redraw);
-	}
+	for(auto artWin : GH.windows().findWindows<CArtifactHolder>())
+		artWin->artifactMoved(src, dst, redraw);
+
 	waitWhileDialog();
 }
 
@@ -1853,12 +1813,9 @@ void CPlayerInterface::artifactAssembled(const ArtifactLocation &al)
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	auto hero = std::visit(HeroObjectRetriever(), al.artHolder);
 	adventureInt->onHeroChanged(hero);
-	for(auto isa : GH.listInt)
-	{
-		auto artWin = dynamic_cast<CArtifactHolder*>(isa.get());
-		if (artWin)
-			artWin->artifactAssembled(al);
-	}
+
+	for(auto artWin : GH.windows().findWindows<CArtifactHolder>())
+		artWin->artifactAssembled(al);
 }
 
 void CPlayerInterface::artifactDisassembled(const ArtifactLocation &al)
@@ -1866,12 +1823,9 @@ void CPlayerInterface::artifactDisassembled(const ArtifactLocation &al)
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	auto hero = std::visit(HeroObjectRetriever(), al.artHolder);
 	adventureInt->onHeroChanged(hero);
-	for(auto isa : GH.listInt)
-	{
-		auto artWin = dynamic_cast<CArtifactHolder*>(isa.get());
-		if (artWin)
-			artWin->artifactDisassembled(al);
-	}
+
+	for(auto artWin : GH.windows().findWindows<CArtifactHolder>())
+		artWin->artifactDisassembled(al);
 }
 
 void CPlayerInterface::waitForAllDialogs(bool unlockPim)
@@ -1897,15 +1851,11 @@ bool CPlayerInterface::capturedAllEvents()
 		return true;
 	}
 
-	bool needToLockAdventureMap = adventureInt && adventureInt->active && CGI->mh->hasOngoingAnimations();
+	bool needToLockAdventureMap = adventureInt && adventureInt->isActive() && CGI->mh->hasOngoingAnimations();
 
 	if (ignoreEvents || needToLockAdventureMap || isAutoFightOn)
 	{
-		boost::unique_lock<boost::mutex> un(eventsM);
-		while(!SDLEventsQueue.empty())
-		{
-			SDLEventsQueue.pop();
-		}
+		GH.input().ignoreEventsUntilInput();
 		return true;
 	}
 

+ 1 - 2
client/CPlayerInterface.h

@@ -31,7 +31,7 @@ struct CPathsInfo;
 VCMI_LIB_NAMESPACE_END
 
 class CButton;
-class CAdventureMapInterface;
+class AdventureMapInterface;
 class CCastleInterface;
 class BattleInterface;
 class CComponent;
@@ -47,7 +47,6 @@ class KeyInterested;
 class MotionInterested;
 class PlayerLocalState;
 class TimeInterested;
-class IShowable;
 
 namespace boost
 {

+ 7 - 4
client/CServerHandler.cpp

@@ -14,6 +14,7 @@
 #include "CGameInfo.h"
 #include "CPlayerInterface.h"
 #include "gui/CGuiHandler.h"
+#include "gui/WindowHandler.h"
 
 #include "lobby/CSelectionBase.h"
 #include "lobby/CLobbyScreen.h"
@@ -123,7 +124,8 @@ public:
 	}
 };
 
-extern std::string NAME;
+static const std::string NAME_AFFIX = "client";
+static const std::string NAME = GameConstants::VCMI_VERSION + std::string(" (") + NAME_AFFIX + ')'; //application name
 
 CServerHandler::CServerHandler()
 	: state(EClientState::NONE), mx(std::make_shared<boost::recursive_mutex>()), client(nullptr), loadMode(0), campaignStateToSend(nullptr), campaignServerRestartLock(false)
@@ -324,7 +326,7 @@ void CServerHandler::applyPacksOnLobbyScreen()
 		packsForLobbyScreen.pop_front();
 		CBaseForLobbyApply * apply = applier->getApplier(typeList.getTypeID(pack)); //find the applier
 		apply->applyOnLobbyScreen(static_cast<CLobbyScreen *>(SEL), this, pack);
-		GH.totalRedraw();
+		GH.windows().totalRedraw();
 		delete pack;
 	}
 }
@@ -748,8 +750,9 @@ void CServerHandler::debugStartTest(std::string filename, bool save)
 
 	boost::this_thread::sleep(boost::posix_time::milliseconds(100));
 
-	while(!settings["session"]["headless"].Bool() && !dynamic_cast<CLobbyScreen *>(GH.topInt().get()))
+	while(!settings["session"]["headless"].Bool() && !GH.windows().topWindow<CLobbyScreen>())
 		boost::this_thread::sleep(boost::posix_time::milliseconds(50));
+
 	while(!mi || mapInfo->fileURI != CSH->mi->fileURI)
 	{
 		setMapInfo(mapInfo);
@@ -840,7 +843,7 @@ void CServerHandler::threadHandleConnection()
 			if(client)
 			{
 				state = EClientState::DISCONNECTING;
-				CGuiHandler::pushUserEvent(EUserEvent::RETURN_TO_MAIN_MENU);
+				GH.pushUserEvent(EUserEvent::RETURN_TO_MAIN_MENU);
 			}
 			else
 			{

+ 19 - 22
client/CVideoHandler.cpp

@@ -12,14 +12,13 @@
 
 #include "CMT.h"
 #include "gui/CGuiHandler.h"
+#include "eventsSDL/InputHandler.h"
+#include "gui/FramerateManager.h"
 #include "renderSDL/SDL_Extensions.h"
 #include "CPlayerInterface.h"
 #include "../lib/filesystem/Filesystem.h"
 
 #include <SDL_render.h>
-#include <SDL_events.h>
-
-extern CGuiHandler GH; //global gui handler
 
 #ifndef DISABLE_VIDEO
 
@@ -30,18 +29,6 @@ extern "C" {
 #include <libswscale/swscale.h>
 }
 
-//reads events and returns true on key down
-static bool keyDown()
-{
-	SDL_Event ev;
-	while(SDL_PollEvent(&ev))
-	{
-		if(ev.type == SDL_KEYDOWN || ev.type == SDL_MOUSEBUTTONDOWN)
-			return true;
-	}
-	return false;
-}
-
 #ifdef _MSC_VER
 #pragma comment(lib, "avcodec.lib")
 #pragma comment(lib, "avutil.lib")
@@ -370,7 +357,7 @@ void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, boo
 	auto packet_duration = frame->duration;
 #endif
 	double frameEndTime = (frame->pts + packet_duration) * av_q2d(format->streams[stream]->time_base);
-	frameTime += GH.mainFPSmng->getElapsedMilliseconds() / 1000.0;
+	frameTime += GH.framerate().getElapsedMilliseconds() / 1000.0;
 
 	if (frameTime >= frameEndTime )
 	{
@@ -450,21 +437,31 @@ bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey)
 
 	pos.x = x;
 	pos.y = y;
+	frameTime = 0.0;
 
 	while(nextFrame())
 	{
-		if(stopOnKey && keyDown())
-			return false;
+		if(stopOnKey)
+		{
+			GH.input().fetchEvents();
+			if(GH.input().ignoreEventsUntilInput())
+				return false;
+		}
 
 		SDL_Rect rect = CSDL_Ext::toSDL(pos);
 
 		SDL_RenderCopy(mainRenderer, texture, nullptr, &rect);
 		SDL_RenderPresent(mainRenderer);
 
-		// Wait 3 frames
-		GH.mainFPSmng->framerateDelay();
-		GH.mainFPSmng->framerateDelay();
-		GH.mainFPSmng->framerateDelay();
+#if (LIBAVUTIL_VERSION_MAJOR < 58)
+		auto packet_duration = frame->pkt_duration;
+#else
+		auto packet_duration = frame->duration;
+#endif
+		double frameDurationSec = packet_duration * av_q2d(format->streams[stream]->time_base);
+		uint32_t timeToSleepMillisec = 1000 * (frameDurationSec);
+
+		boost::this_thread::sleep(boost::posix_time::millisec(timeToSleepMillisec));
 	}
 
 	return true;

+ 3 - 6
client/Client.cpp

@@ -15,9 +15,10 @@
 #include "CPlayerInterface.h"
 #include "CServerHandler.h"
 #include "ClientNetPackVisitors.h"
-#include "adventureMap/CAdventureMapInterface.h"
+#include "adventureMap/AdventureMapInterface.h"
 #include "battle/BattleInterface.h"
 #include "gui/CGuiHandler.h"
+#include "gui/WindowHandler.h"
 #include "mapView/mapHandler.h"
 
 #include "../CCallback.h"
@@ -766,12 +767,8 @@ void CClient::removeGUI()
 {
 	// CClient::endGame
 	GH.curInt = nullptr;
-	if(GH.topInt())
-		GH.topInt()->deactivate();
+	GH.windows().clear();
 	adventureInt.reset();
-	GH.listInt.clear();
-	GH.objsToBlit.clear();
-	GH.statusbar.reset();
 	logGlobal->info("Removed GUI.");
 
 	LOCPLINT = nullptr;

+ 5 - 49
client/ClientCommandManager.cpp

@@ -16,6 +16,7 @@
 #include "PlayerLocalState.h"
 #include "CServerHandler.h"
 #include "gui/CGuiHandler.h"
+#include "gui/WindowHandler.h"
 #include "../lib/NetPacks.h"
 #include "ClientNetPackVisitors.h"
 #include "../lib/CConfigHandler.h"
@@ -100,7 +101,7 @@ void ClientCommandManager::handleGoSoloCommand()
 				CSH->client->installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), elem.first);
 			}
 		}
-		GH.totalRedraw();
+		GH.windows().totalRedraw();
 		giveTurn(color);
 	}
 	session["aiSolo"].Bool() = !session["aiSolo"].Bool();
@@ -141,7 +142,7 @@ void ClientCommandManager::handleControlaiCommand(std::istringstream& singleWord
 		CSH->client->installNewPlayerInterface(std::make_shared<CPlayerInterface>(elem.first), elem.first);
 	}
 
-	GH.totalRedraw();
+	GH.windows().totalRedraw();
 	if(color != PlayerColor::NEUTRAL)
 		giveTurn(color);
 }
@@ -170,7 +171,7 @@ void ClientCommandManager::handleSetBattleAICommand(std::istringstream& singleWo
 
 void ClientCommandManager::handleRedrawCommand()
 {
-	GH.totalRedraw();
+	GH.windows().totalRedraw();
 }
 
 void ClientCommandManager::handleScreenCommand()
@@ -193,18 +194,6 @@ void ClientCommandManager::handleNotDialogCommand()
 	LOCPLINT->showingDialog->setn(false);
 }
 
-void ClientCommandManager::handleGuiCommand()
-{
-	for(const auto & child : GH.listInt)
-	{
-		const auto childPtr = child.get();
-		if(const CIntObject * obj = dynamic_cast<const CIntObject*>(childPtr))
-			printInfoAboutInterfaceObject(obj, 0);
-		else
-			printCommandMessage(std::string(typeid(childPtr).name()) + "\n");
-	}
-}
-
 void ClientCommandManager::handleConvertTextCommand()
 {
 	logGlobal->info("Searching for available maps");
@@ -387,7 +376,7 @@ void ClientCommandManager::handleBonusesCommand(std::istringstream & singleWordB
 		return ss.str();
 	};
 		printCommandMessage("Bonuses of " + LOCPLINT->localState->getCurrentArmy()->getObjectName() + "\n");
-		printCommandMessage(format(LOCPLINT->localState->getCurrentArmy()->getBonusList()) + "\n");
+		printCommandMessage(format(*LOCPLINT->localState->getCurrentArmy()->getAllBonuses(Selector::all, Selector::all)) + "\n");
 
 	printCommandMessage("\nInherited bonuses:\n");
 	TCNodes parents;
@@ -492,36 +481,6 @@ void ClientCommandManager::printCommandMessage(const std::string &commandMessage
 	}
 }
 
-void ClientCommandManager::printInfoAboutInterfaceObject(const CIntObject *obj, int level)
-{
-	std::stringstream sbuffer;
-	sbuffer << std::string(level, '\t');
-
-	sbuffer << typeid(*obj).name() << " *** ";
-	if (obj->active)
-	{
-#define PRINT(check, text) if (obj->active & CIntObject::check) sbuffer << text
-		PRINT(LCLICK, 'L');
-		PRINT(RCLICK, 'R');
-		PRINT(HOVER, 'H');
-		PRINT(MOVE, 'M');
-		PRINT(KEYBOARD, 'K');
-		PRINT(TIME, 'T');
-		PRINT(GENERAL, 'A');
-		PRINT(WHEEL, 'W');
-		PRINT(DOUBLECLICK, 'D');
-#undef  PRINT
-	}
-	else
-		sbuffer << "inactive";
-	sbuffer << " at " << obj->pos.x <<"x"<< obj->pos.y;
-	sbuffer << " (" << obj->pos.w <<"x"<< obj->pos.h << ")";
-	printCommandMessage(sbuffer.str(), ELogLevel::INFO);
-
-	for(const CIntObject *child : obj->children)
-		printInfoAboutInterfaceObject(child, level+1);
-}
-
 void ClientCommandManager::giveTurn(const PlayerColor &colorIdentifier)
 {
 	YourTurn yt;
@@ -574,9 +533,6 @@ void ClientCommandManager::processCommand(const std::string & message, bool call
 	else if(commandName == "not dialog")
 		handleNotDialogCommand();
 
-	else if(commandName == "gui")
-		handleGuiCommand();
-
 	else if(message=="convert txt")
 		handleConvertTextCommand();
 

+ 0 - 4
client/ClientCommandManager.h

@@ -51,9 +51,6 @@ class ClientCommandManager //take mantis #2292 issue about account if thinking a
 	// Set the state indicating if dialog box is active to "no"
 	void handleNotDialogCommand();
 
-	// Displays tree view of currently present VCMI common GUI elements
-	void handleGuiCommand();
-
 	// Dumps all game text, maps text and campaign maps text into Client log between BEGIN TEXT EXPORT and END TEXT EXPORT
 	void handleConvertTextCommand();
 
@@ -92,7 +89,6 @@ class ClientCommandManager //take mantis #2292 issue about account if thinking a
 
 	// Prints in Chat the given message
 	void printCommandMessage(const std::string &commandMessage, ELogLevel::ELogLevel messageType = ELogLevel::NOT_SET);
-	void printInfoAboutInterfaceObject(const CIntObject *obj, int level);
 	void giveTurn(const PlayerColor &color);
 
 public:

+ 0 - 9
client/DPIaware.manifest

@@ -1,9 +0,0 @@
-<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
-<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
-  <asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
-    <asmv3:windowsSettings
-         xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
-      <dpiAware>true</dpiAware>
-    </asmv3:windowsSettings>
-  </asmv3:application>
-</assembly> 

+ 2 - 3
client/NetPacksClient.cpp

@@ -329,13 +329,12 @@ void ApplyClientNetPackVisitor::visitGiveBonus(GiveBonus & pack)
 	case GiveBonus::ETarget::HERO:
 		{
 			const CGHeroInstance *h = gs.getHero(ObjectInstanceID(pack.id));
-			callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, *h->getBonusList().back(), true);
+			callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, true);
 		}
 		break;
 	case GiveBonus::ETarget::PLAYER:
 		{
-			const PlayerState *p = gs.getPlayerState(PlayerColor(pack.id));
-			callInterfaceIfPresent(cl, PlayerColor(pack.id), &IGameEventsReceiver::playerBonusChanged, *p->getBonusList().back(), true);
+			callInterfaceIfPresent(cl, PlayerColor(pack.id), &IGameEventsReceiver::playerBonusChanged, pack.bonus, true);
 		}
 		break;
 	}

+ 6 - 5
client/NetPacksLobbyClient.cpp

@@ -21,6 +21,7 @@
 #include "CServerHandler.h"
 #include "CGameInfo.h"
 #include "gui/CGuiHandler.h"
+#include "gui/WindowHandler.h"
 #include "widgets/Buttons.h"
 #include "widgets/TextControls.h"
 
@@ -38,7 +39,7 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientConnected(LobbyClientCon
 	{
 		handler.c->connectionID = pack.clientId;
 		if(!settings["session"]["headless"].Bool())
-			GH.pushIntT<CLobbyScreen>(static_cast<ESelectionScreen>(handler.screenType));
+			GH.windows().createAndPushWindow<CLobbyScreen>(static_cast<ESelectionScreen>(handler.screenType));
 		handler.state = EClientState::LOBBY;
 	}
 }
@@ -56,8 +57,8 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientDisconnected(LobbyClient
 
 void ApplyOnLobbyScreenNetPackVisitor::visitLobbyClientDisconnected(LobbyClientDisconnected & pack)
 {
-	if(GH.listInt.size())
-		GH.popInts(1);
+	if(GH.windows().count() > 0)
+		GH.windows().popWindows(1);
 }
 
 void ApplyOnLobbyScreenNetPackVisitor::visitLobbyChatMessage(LobbyChatMessage & pack)
@@ -128,7 +129,7 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack
 	if(pack.clientId != -1 && pack.clientId != handler.c->connectionID)
 		return;
 	
-	GH.pushIntT<CLoadingScreen>(std::bind(&CServerHandler::startGameplay, &handler, pack.initializedGameState));
+	GH.windows().createAndPushWindow<CLoadingScreen>(std::bind(&CServerHandler::startGameplay, &handler, pack.initializedGameState));
 }
 
 void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState & pack)
@@ -145,7 +146,7 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState &
 	if(!lobby->bonusSel && handler.si->campState && handler.state == EClientState::LOBBY_CAMPAIGN)
 	{
 		lobby->bonusSel = std::make_shared<CBonusSelection>();
-		GH.pushInt(lobby->bonusSel);
+		GH.windows().pushWindow(lobby->bonusSel);
 	}
 
 	if(lobby->bonusSel)

+ 1 - 1
client/PlayerLocalState.cpp

@@ -15,7 +15,7 @@
 #include "../lib/mapObjects/CGHeroInstance.h"
 #include "../lib/mapObjects/CGTownInstance.h"
 #include "CPlayerInterface.h"
-#include "adventureMap/CAdventureMapInterface.h"
+#include "adventureMap/AdventureMapInterface.h"
 
 PlayerLocalState::PlayerLocalState(CPlayerInterface & owner)
 	: owner(owner)

+ 827 - 0
client/adventureMap/AdventureMapInterface.cpp

@@ -0,0 +1,827 @@
+/*
+ * AdventureMapInterface.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 "AdventureMapInterface.h"
+
+#include "AdventureOptions.h"
+#include "AdventureState.h"
+#include "CInGameConsole.h"
+#include "CMinimap.h"
+#include "CList.h"
+#include "CInfoBar.h"
+#include "MapAudioPlayer.h"
+#include "AdventureMapWidget.h"
+#include "AdventureMapShortcuts.h"
+
+#include "../mapView/mapHandler.h"
+#include "../mapView/MapView.h"
+#include "../windows/InfoWindows.h"
+#include "../CGameInfo.h"
+#include "../gui/CursorHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
+#include "../gui/WindowHandler.h"
+#include "../CMT.h"
+#include "../PlayerLocalState.h"
+#include "../CPlayerInterface.h"
+
+#include "../../CCallback.h"
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/spells/CSpellHandler.h"
+#include "../../lib/mapObjects/CGHeroInstance.h"
+#include "../../lib/CPathfinder.h"
+#include "../../lib/mapping/CMap.h"
+
+std::shared_ptr<AdventureMapInterface> adventureInt;
+
+AdventureMapInterface::AdventureMapInterface():
+	mapAudio(new MapAudioPlayer()),
+	spellBeingCasted(nullptr),
+	scrollingWasActive(false),
+	scrollingWasBlocked(false)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	pos.x = pos.y = 0;
+	pos.w = GH.screenDimensions().x;
+	pos.h = GH.screenDimensions().y;
+	setMoveEventStrongInterest(true); // handle all mouse move events to prevent dead mouse move space in fullscreen mode
+
+	shortcuts = std::make_shared<AdventureMapShortcuts>(*this);
+
+	widget = std::make_shared<AdventureMapWidget>(shortcuts);
+	shortcuts->setState(EAdventureState::MAKING_TURN);
+	widget->getMapView()->onViewMapActivated();
+
+	addUsedEvents(KEYBOARD | TIME);
+}
+
+void AdventureMapInterface::onMapViewMoved(const Rect & visibleArea, int mapLevel)
+{
+	shortcuts->onMapViewMoved(visibleArea, mapLevel);
+	widget->getMinimap()->onMapViewMoved(visibleArea, mapLevel);
+	widget->onMapViewMoved(visibleArea, mapLevel);
+}
+
+void AdventureMapInterface::onAudioResumed()
+{
+	mapAudio->onAudioResumed();
+}
+
+void AdventureMapInterface::onAudioPaused()
+{
+	mapAudio->onAudioPaused();
+}
+
+void AdventureMapInterface::onHeroMovementStarted(const CGHeroInstance * hero)
+{
+	widget->getInfoBar()->popAll();
+	widget->getInfoBar()->showSelection();
+}
+
+void AdventureMapInterface::onHeroChanged(const CGHeroInstance *h)
+{
+	widget->getHeroList()->update(h);
+
+	if (h && h == LOCPLINT->localState->getCurrentHero() && !widget->getInfoBar()->showingComponents())
+		widget->getInfoBar()->showSelection();
+
+	widget->updateActiveState();
+}
+
+void AdventureMapInterface::onTownChanged(const CGTownInstance * town)
+{
+	widget->getTownList()->update(town);
+
+	if (town && town == LOCPLINT->localState->getCurrentTown() && !widget->getInfoBar()->showingComponents())
+		widget->getInfoBar()->showSelection();
+}
+
+void AdventureMapInterface::showInfoBoxMessage(const std::vector<Component> & components, std::string message, int timer)
+{
+	widget->getInfoBar()->pushComponents(components, message, timer);
+}
+
+void AdventureMapInterface::activate()
+{
+	CIntObject::activate();
+
+	adjustActiveness();
+
+	screenBuf = screen;
+	
+	if(LOCPLINT)
+	{
+		LOCPLINT->cingconsole->activate();
+		LOCPLINT->cingconsole->pos = this->pos;
+	}
+
+	GH.fakeMouseMove(); //to restore the cursor
+}
+
+void AdventureMapInterface::deactivate()
+{
+	CIntObject::deactivate();
+	CCS->curh->set(Cursor::Map::POINTER);
+}
+
+void AdventureMapInterface::showAll(SDL_Surface * to)
+{
+	CIntObject::showAll(to);
+	LOCPLINT->cingconsole->show(to);
+}
+
+void AdventureMapInterface::show(SDL_Surface * to)
+{
+	CIntObject::show(to);
+	LOCPLINT->cingconsole->show(to);
+}
+
+void AdventureMapInterface::tick(uint32_t msPassed)
+{
+	handleMapScrollingUpdate(msPassed);
+
+	// we want animations to be active during enemy turn but map itself to be non-interactive
+	// so call timer update directly on inactive element
+	widget->getMapView()->tick(msPassed);
+}
+
+void AdventureMapInterface::handleMapScrollingUpdate(uint32_t timePassed)
+{
+	/// Width of window border, in pixels, that triggers map scrolling
+	static constexpr uint32_t borderScrollWidth = 15;
+
+	uint32_t scrollSpeedPixels = settings["adventure"]["scrollSpeedPixels"].Float();
+	uint32_t scrollDistance = scrollSpeedPixels * timePassed / 1000;
+
+	Point cursorPosition = GH.getCursorPosition();
+	Point scrollDirection;
+
+	if (cursorPosition.x < borderScrollWidth)
+		scrollDirection.x = -1;
+
+	if (cursorPosition.x > GH.screenDimensions().x - borderScrollWidth)
+		scrollDirection.x = +1;
+
+	if (cursorPosition.y < borderScrollWidth)
+		scrollDirection.y = -1;
+
+	if (cursorPosition.y > GH.screenDimensions().y - borderScrollWidth)
+		scrollDirection.y = +1;
+
+	Point scrollDelta = scrollDirection * scrollDistance;
+
+	bool cursorInScrollArea = scrollDelta != Point(0,0);
+	bool scrollingActive = cursorInScrollArea && isActive() && shortcuts->optionSidePanelActive() && !scrollingWasBlocked;
+	bool scrollingBlocked = GH.isKeyboardCtrlDown();
+
+	if (!scrollingWasActive && scrollingBlocked)
+	{
+		scrollingWasBlocked = true;
+		return;
+	}
+
+	if (!cursorInScrollArea && scrollingWasBlocked)
+	{
+		scrollingWasBlocked = false;
+		return;
+	}
+
+	if (scrollingActive)
+		widget->getMapView()->onMapScrolled(scrollDelta);
+
+	if (!scrollingActive && !scrollingWasActive)
+		return;
+
+	if(scrollDelta.x > 0)
+	{
+		if(scrollDelta.y < 0)
+			CCS->curh->set(Cursor::Map::SCROLL_NORTHEAST);
+		if(scrollDelta.y > 0)
+			CCS->curh->set(Cursor::Map::SCROLL_SOUTHEAST);
+		if(scrollDelta.y == 0)
+			CCS->curh->set(Cursor::Map::SCROLL_EAST);
+	}
+	if(scrollDelta.x < 0)
+	{
+		if(scrollDelta.y < 0)
+			CCS->curh->set(Cursor::Map::SCROLL_NORTHWEST);
+		if(scrollDelta.y > 0)
+			CCS->curh->set(Cursor::Map::SCROLL_SOUTHWEST);
+		if(scrollDelta.y == 0)
+			CCS->curh->set(Cursor::Map::SCROLL_WEST);
+	}
+
+	if (scrollDelta.x == 0)
+	{
+		if(scrollDelta.y < 0)
+			CCS->curh->set(Cursor::Map::SCROLL_NORTH);
+		if(scrollDelta.y > 0)
+			CCS->curh->set(Cursor::Map::SCROLL_SOUTH);
+		if(scrollDelta.y == 0)
+			CCS->curh->set(Cursor::Map::POINTER);
+	}
+
+	scrollingWasActive = scrollingActive;
+}
+
+void AdventureMapInterface::centerOnTile(int3 on)
+{
+	widget->getMapView()->onCenteredTile(on);
+}
+
+void AdventureMapInterface::centerOnObject(const CGObjectInstance * obj)
+{
+	widget->getMapView()->onCenteredObject(obj);
+}
+
+void AdventureMapInterface::keyPressed(EShortcut key)
+{
+	if (key == EShortcut::GLOBAL_CANCEL && spellBeingCasted)
+		hotkeyAbortCastingMode();
+
+	//fake mouse use to trigger onTileHovered()
+	GH.fakeMouseMove();
+}
+
+void AdventureMapInterface::onSelectionChanged(const CArmedInstance *sel)
+{
+	assert(sel);
+
+	widget->getInfoBar()->popAll();
+	mapAudio->onSelectionChanged(sel);
+	bool centerView = !settings["session"]["autoSkip"].Bool();
+
+	if (centerView)
+		centerOnObject(sel);
+
+	if(sel->ID==Obj::TOWN)
+	{
+		auto town = dynamic_cast<const CGTownInstance*>(sel);
+
+		widget->getInfoBar()->showTownSelection(town);
+		widget->getTownList()->select(town);
+		widget->getHeroList()->select(nullptr);
+		onHeroChanged(nullptr);
+	}
+	else //hero selected
+	{
+		auto hero = dynamic_cast<const CGHeroInstance*>(sel);
+
+		widget->getInfoBar()->showHeroSelection(hero);
+		widget->getHeroList()->select(hero);
+		widget->getTownList()->select(nullptr);
+
+		LOCPLINT->localState->verifyPath(hero);
+		onHeroChanged(hero);
+	}
+
+	widget->updateActiveState();
+	widget->getHeroList()->redraw();
+	widget->getTownList()->redraw();
+}
+
+void AdventureMapInterface::onMapTilesChanged(boost::optional<std::unordered_set<int3>> positions)
+{
+	if (positions)
+		widget->getMinimap()->updateTiles(*positions);
+	else
+		widget->getMinimap()->update();
+}
+
+void AdventureMapInterface::onHotseatWaitStarted(PlayerColor playerID)
+{
+	onCurrentPlayerChanged(playerID);
+	setState(EAdventureState::HOTSEAT_WAIT);
+}
+
+void AdventureMapInterface::onEnemyTurnStarted(PlayerColor playerID)
+{
+	if(settings["session"]["spectate"].Bool())
+		return;
+
+	mapAudio->onEnemyTurnStarted();
+	widget->getMinimap()->setAIRadar(true);
+	widget->getInfoBar()->startEnemyTurn(LOCPLINT->cb->getCurrentPlayer());
+	setState(EAdventureState::ENEMY_TURN);
+
+}
+
+void AdventureMapInterface::setState(EAdventureState state)
+{
+	shortcuts->setState(state);
+	adjustActiveness();
+	widget->updateActiveState();
+}
+
+void AdventureMapInterface::adjustActiveness()
+{
+	bool widgetMustBeActive = isActive() && shortcuts->optionSidePanelActive();
+	bool mapViewMustBeActive = isActive() && (shortcuts->optionMapViewActive());
+
+	if (widgetMustBeActive && !widget->isActive())
+		widget->activate();
+
+	if (!widgetMustBeActive && widget->isActive())
+		widget->deactivate();
+
+	if (mapViewMustBeActive && !widget->getMapView()->isActive())
+		widget->getMapView()->activate();
+
+	if (!mapViewMustBeActive && widget->getMapView()->isActive())
+		widget->getMapView()->deactivate();
+}
+
+void AdventureMapInterface::onCurrentPlayerChanged(PlayerColor playerID)
+{
+	LOCPLINT->localState->setSelection(nullptr);
+
+	if (playerID == currentPlayerID)
+		return;
+
+	currentPlayerID = playerID;
+	widget->setPlayer(playerID);
+}
+
+void AdventureMapInterface::onPlayerTurnStarted(PlayerColor playerID)
+{
+	onCurrentPlayerChanged(playerID);
+
+	setState(EAdventureState::MAKING_TURN);
+	if(LOCPLINT->cb->getCurrentPlayer() == LOCPLINT->playerID
+		|| settings["session"]["spectate"].Bool())
+	{
+		widget->getMinimap()->setAIRadar(false);
+		widget->getInfoBar()->showSelection();
+	}
+
+	widget->getHeroList()->update();
+	widget->getTownList()->update();
+
+	const CGHeroInstance * heroToSelect = nullptr;
+
+	// find first non-sleeping hero
+	for (auto hero : LOCPLINT->localState->getWanderingHeroes())
+	{
+		if (!LOCPLINT->localState->isHeroSleeping(hero))
+		{
+			heroToSelect = hero;
+			break;
+		}
+	}
+
+	//select first hero if available.
+	if (heroToSelect != nullptr)
+	{
+		LOCPLINT->localState->setSelection(heroToSelect);
+	}
+	else if (LOCPLINT->localState->getOwnedTowns().size())
+	{
+		LOCPLINT->localState->setSelection(LOCPLINT->localState->getOwnedTown(0));
+	}
+	else
+	{
+		LOCPLINT->localState->setSelection(LOCPLINT->localState->getWanderingHero(0));
+	}
+
+	//show new day animation and sound on infobar
+	widget->getInfoBar()->showDate();
+
+	onHeroChanged(nullptr);
+	showAll(screen);
+	mapAudio->onPlayerTurnStarted();
+
+	if(settings["session"]["autoSkip"].Bool() && !GH.isKeyboardShiftDown())
+	{
+		if(auto iw = GH.windows().topWindow<CInfoWindow>())
+			iw->close();
+
+		hotkeyEndingTurn();
+	}
+}
+
+void AdventureMapInterface::hotkeyEndingTurn()
+{
+	if(settings["session"]["spectate"].Bool())
+		return;
+
+	LOCPLINT->makingTurn = false;
+	LOCPLINT->cb->endTurn();
+	mapAudio->onPlayerTurnEnded();
+}
+
+const CGObjectInstance* AdventureMapInterface::getActiveObject(const int3 &mapPos)
+{
+	std::vector < const CGObjectInstance * > bobjs = LOCPLINT->cb->getBlockingObjs(mapPos);  //blocking objects at tile
+
+	if (bobjs.empty())
+		return nullptr;
+
+	return *boost::range::max_element(bobjs, &CMapHandler::compareObjectBlitOrder);
+}
+
+void AdventureMapInterface::onTileLeftClicked(const int3 &mapPos)
+{
+	if(!shortcuts->optionMapViewActive())
+		return;
+
+	//FIXME: this line breaks H3 behavior for Dimension Door
+	if(!LOCPLINT->cb->isVisible(mapPos))
+		return;
+	if(!LOCPLINT->makingTurn)
+		return;
+
+	const TerrainTile *tile = LOCPLINT->cb->getTile(mapPos);
+
+	const CGObjectInstance *topBlocking = getActiveObject(mapPos);
+
+	int3 selPos = LOCPLINT->localState->getCurrentArmy()->getSightCenter();
+	if(spellBeingCasted)
+	{
+		assert(shortcuts->optionSpellcasting());
+
+		if (!isInScreenRange(selPos, mapPos))
+			return;
+
+		const TerrainTile *heroTile = LOCPLINT->cb->getTile(selPos);
+
+		switch(spellBeingCasted->id)
+		{
+		case SpellID::SCUTTLE_BOAT: //Scuttle Boat
+			if(topBlocking && topBlocking->ID == Obj::BOAT)
+				performSpellcasting(mapPos);
+			break;
+		case SpellID::DIMENSION_DOOR:
+			if(!tile || tile->isClear(heroTile))
+				performSpellcasting(mapPos);
+			break;
+		}
+		return;
+	}
+	//check if we can select this object
+	bool canSelect = topBlocking && topBlocking->ID == Obj::HERO && topBlocking->tempOwner == LOCPLINT->playerID;
+	canSelect |= topBlocking && topBlocking->ID == Obj::TOWN && LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, topBlocking->tempOwner);
+
+	bool isHero = false;
+	if(LOCPLINT->localState->getCurrentArmy()->ID != Obj::HERO) //hero is not selected (presumably town)
+	{
+		if(LOCPLINT->localState->getCurrentArmy() == topBlocking) //selected town clicked
+			LOCPLINT->openTownWindow(static_cast<const CGTownInstance*>(topBlocking));
+		else if(canSelect)
+			LOCPLINT->localState->setSelection(static_cast<const CArmedInstance*>(topBlocking));
+	}
+	else if(const CGHeroInstance * currentHero = LOCPLINT->localState->getCurrentHero()) //hero is selected
+	{
+		isHero = true;
+
+		const CGPathNode *pn = LOCPLINT->cb->getPathsInfo(currentHero)->getPathInfo(mapPos);
+		if(currentHero == topBlocking) //clicked selected hero
+		{
+			LOCPLINT->openHeroWindow(currentHero);
+			return;
+		}
+		else if(canSelect && pn->turns == 255 ) //selectable object at inaccessible tile
+		{
+			LOCPLINT->localState->setSelection(static_cast<const CArmedInstance*>(topBlocking));
+			return;
+		}
+		else //still here? we need to move hero if we clicked end of already selected path or calculate a new path otherwise
+		{
+			if(LOCPLINT->localState->hasPath(currentHero) &&
+			   LOCPLINT->localState->getPath(currentHero).endPos() == mapPos)//we'll be moving
+			{
+				if(!CGI->mh->hasOngoingAnimations())
+					LOCPLINT->moveHero(currentHero, LOCPLINT->localState->getPath(currentHero));
+				return;
+			}
+			else //remove old path and find a new one if we clicked on accessible tile
+			{
+				LOCPLINT->localState->setPath(currentHero, mapPos);
+				onHeroChanged(currentHero);
+			}
+		}
+	} //end of hero is selected "case"
+	else
+	{
+		throw std::runtime_error("Nothing is selected...");
+	}
+
+	const auto shipyard = ourInaccessibleShipyard(topBlocking);
+	if(isHero && shipyard != nullptr)
+	{
+		LOCPLINT->showShipyardDialogOrProblemPopup(shipyard);
+	}
+}
+
+void AdventureMapInterface::onTileHovered(const int3 &mapPos)
+{
+	if(!shortcuts->optionMapViewActive())
+		return;
+
+	//may occur just at the start of game (fake move before full intiialization)
+	if(!LOCPLINT->localState->getCurrentArmy())
+		return;
+
+	if(!LOCPLINT->cb->isVisible(mapPos))
+	{
+		CCS->curh->set(Cursor::Map::POINTER);
+		GH.statusbar()->clear();
+		return;
+	}
+	auto objRelations = PlayerRelations::ALLIES;
+	const CGObjectInstance *objAtTile = getActiveObject(mapPos);
+	if(objAtTile)
+	{
+		objRelations = LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, objAtTile->tempOwner);
+		std::string text = LOCPLINT->localState->getCurrentHero() ? objAtTile->getHoverText(LOCPLINT->localState->getCurrentHero()) : objAtTile->getHoverText(LOCPLINT->playerID);
+		boost::replace_all(text,"\n"," ");
+		GH.statusbar()->write(text);
+	}
+	else
+	{
+		std::string hlp = CGI->mh->getTerrainDescr(mapPos, false);
+		GH.statusbar()->write(hlp);
+	}
+
+	if(spellBeingCasted)
+	{
+		switch(spellBeingCasted->id)
+		{
+		case SpellID::SCUTTLE_BOAT:
+			{
+			int3 hpos = LOCPLINT->localState->getCurrentArmy()->getSightCenter();
+
+			if(objAtTile && objAtTile->ID == Obj::BOAT && isInScreenRange(hpos, mapPos))
+				CCS->curh->set(Cursor::Map::SCUTTLE_BOAT);
+			else
+				CCS->curh->set(Cursor::Map::POINTER);
+			return;
+			}
+		case SpellID::DIMENSION_DOOR:
+			{
+				const TerrainTile * t = LOCPLINT->cb->getTile(mapPos, false);
+				int3 hpos = LOCPLINT->localState->getCurrentArmy()->getSightCenter();
+				if((!t || t->isClear(LOCPLINT->cb->getTile(hpos))) && isInScreenRange(hpos, mapPos))
+					CCS->curh->set(Cursor::Map::TELEPORT);
+				else
+					CCS->curh->set(Cursor::Map::POINTER);
+				return;
+			}
+		}
+	}
+
+	if(LOCPLINT->localState->getCurrentArmy()->ID == Obj::TOWN)
+	{
+		if(objAtTile)
+		{
+			if(objAtTile->ID == Obj::TOWN && objRelations != PlayerRelations::ENEMIES)
+				CCS->curh->set(Cursor::Map::TOWN);
+			else if(objAtTile->ID == Obj::HERO && objRelations == PlayerRelations::SAME_PLAYER)
+				CCS->curh->set(Cursor::Map::HERO);
+			else
+				CCS->curh->set(Cursor::Map::POINTER);
+		}
+		else
+			CCS->curh->set(Cursor::Map::POINTER);
+	}
+	else if(const CGHeroInstance * hero = LOCPLINT->localState->getCurrentHero())
+	{
+		std::array<Cursor::Map, 4> cursorMove      = { Cursor::Map::T1_MOVE,       Cursor::Map::T2_MOVE,       Cursor::Map::T3_MOVE,       Cursor::Map::T4_MOVE,       };
+		std::array<Cursor::Map, 4> cursorAttack    = { Cursor::Map::T1_ATTACK,     Cursor::Map::T2_ATTACK,     Cursor::Map::T3_ATTACK,     Cursor::Map::T4_ATTACK,     };
+		std::array<Cursor::Map, 4> cursorSail      = { Cursor::Map::T1_SAIL,       Cursor::Map::T2_SAIL,       Cursor::Map::T3_SAIL,       Cursor::Map::T4_SAIL,       };
+		std::array<Cursor::Map, 4> cursorDisembark = { Cursor::Map::T1_DISEMBARK,  Cursor::Map::T2_DISEMBARK,  Cursor::Map::T3_DISEMBARK,  Cursor::Map::T4_DISEMBARK,  };
+		std::array<Cursor::Map, 4> cursorExchange  = { Cursor::Map::T1_EXCHANGE,   Cursor::Map::T2_EXCHANGE,   Cursor::Map::T3_EXCHANGE,   Cursor::Map::T4_EXCHANGE,   };
+		std::array<Cursor::Map, 4> cursorVisit     = { Cursor::Map::T1_VISIT,      Cursor::Map::T2_VISIT,      Cursor::Map::T3_VISIT,      Cursor::Map::T4_VISIT,      };
+		std::array<Cursor::Map, 4> cursorSailVisit = { Cursor::Map::T1_SAIL_VISIT, Cursor::Map::T2_SAIL_VISIT, Cursor::Map::T3_SAIL_VISIT, Cursor::Map::T4_SAIL_VISIT, };
+
+		const CGPathNode * pathNode = LOCPLINT->cb->getPathsInfo(hero)->getPathInfo(mapPos);
+		assert(pathNode);
+
+		if((GH.isKeyboardAltDown() || settings["gameTweaks"]["forceMovementInfo"].Bool()) && pathNode->reachable()) //overwrite status bar text with movement info
+		{
+			showMoveDetailsInStatusbar(*hero, *pathNode);
+		}
+
+		int turns = pathNode->turns;
+		vstd::amin(turns, 3);
+		switch(pathNode->action)
+		{
+		case CGPathNode::NORMAL:
+		case CGPathNode::TELEPORT_NORMAL:
+			if(pathNode->layer == EPathfindingLayer::LAND)
+				CCS->curh->set(cursorMove[turns]);
+			else
+				CCS->curh->set(cursorSailVisit[turns]);
+			break;
+
+		case CGPathNode::VISIT:
+		case CGPathNode::BLOCKING_VISIT:
+		case CGPathNode::TELEPORT_BLOCKING_VISIT:
+			if(objAtTile && objAtTile->ID == Obj::HERO)
+			{
+				if(LOCPLINT->localState->getCurrentArmy()  == objAtTile)
+					CCS->curh->set(Cursor::Map::HERO);
+				else
+					CCS->curh->set(cursorExchange[turns]);
+			}
+			else if(pathNode->layer == EPathfindingLayer::LAND)
+				CCS->curh->set(cursorVisit[turns]);
+			else
+				CCS->curh->set(cursorSailVisit[turns]);
+			break;
+
+		case CGPathNode::BATTLE:
+		case CGPathNode::TELEPORT_BATTLE:
+			CCS->curh->set(cursorAttack[turns]);
+			break;
+
+		case CGPathNode::EMBARK:
+			CCS->curh->set(cursorSail[turns]);
+			break;
+
+		case CGPathNode::DISEMBARK:
+			CCS->curh->set(cursorDisembark[turns]);
+			break;
+
+		default:
+			if(objAtTile && objRelations != PlayerRelations::ENEMIES)
+			{
+				if(objAtTile->ID == Obj::TOWN)
+					CCS->curh->set(Cursor::Map::TOWN);
+				else if(objAtTile->ID == Obj::HERO && objRelations == PlayerRelations::SAME_PLAYER)
+					CCS->curh->set(Cursor::Map::HERO);
+				else
+					CCS->curh->set(Cursor::Map::POINTER);
+			}
+			else
+				CCS->curh->set(Cursor::Map::POINTER);
+			break;
+		}
+	}
+
+	if(ourInaccessibleShipyard(objAtTile))
+	{
+		CCS->curh->set(Cursor::Map::T1_SAIL);
+	}
+}
+
+void AdventureMapInterface::showMoveDetailsInStatusbar(const CGHeroInstance & hero, const CGPathNode & pathNode)
+{
+	const int maxMovementPointsAtStartOfLastTurn = pathNode.turns > 0 ? hero.maxMovePoints(pathNode.layer == EPathfindingLayer::LAND) : hero.movement;
+	const int movementPointsLastTurnCost = maxMovementPointsAtStartOfLastTurn - pathNode.moveRemains;
+	const int remainingPointsAfterMove = pathNode.turns == 0 ? pathNode.moveRemains : 0;
+
+	std::string result = VLC->generaltexth->translate("vcmi.adventureMap", pathNode.turns > 0 ? "moveCostDetails" : "moveCostDetailsNoTurns");
+
+	boost::replace_first(result, "%TURNS", std::to_string(pathNode.turns));
+	boost::replace_first(result, "%POINTS", std::to_string(movementPointsLastTurnCost));
+	boost::replace_first(result, "%REMAINING", std::to_string(remainingPointsAfterMove));
+
+	GH.statusbar()->write(result);
+}
+
+void AdventureMapInterface::onTileRightClicked(const int3 &mapPos)
+{
+	if(!shortcuts->optionMapViewActive())
+		return;
+
+	if(spellBeingCasted)
+	{
+		hotkeyAbortCastingMode();
+		return;
+	}
+
+	if(!LOCPLINT->cb->isVisible(mapPos))
+	{
+		CRClickPopup::createAndPush(VLC->generaltexth->allTexts[61]); //Uncharted Territory
+		return;
+	}
+
+	const CGObjectInstance * obj = getActiveObject(mapPos);
+	if(!obj)
+	{
+		// Bare or undiscovered terrain
+		const TerrainTile * tile = LOCPLINT->cb->getTile(mapPos);
+		if(tile)
+		{
+			std::string hlp = CGI->mh->getTerrainDescr(mapPos, true);
+			CRClickPopup::createAndPush(hlp);
+		}
+		return;
+	}
+
+	CRClickPopup::createAndPush(obj, GH.getCursorPosition(), ETextAlignment::CENTER);
+}
+
+void AdventureMapInterface::enterCastingMode(const CSpell * sp)
+{
+	assert(sp->id == SpellID::SCUTTLE_BOAT || sp->id == SpellID::DIMENSION_DOOR);
+	spellBeingCasted = sp;
+	Settings config = settings.write["session"]["showSpellRange"];
+	config->Bool() = true;
+
+	setState(EAdventureState::CASTING_SPELL);
+}
+
+void AdventureMapInterface::exitCastingMode()
+{
+	assert(spellBeingCasted);
+	spellBeingCasted = nullptr;
+	setState(EAdventureState::MAKING_TURN);
+
+	Settings config = settings.write["session"]["showSpellRange"];
+	config->Bool() = false;
+}
+
+void AdventureMapInterface::hotkeyAbortCastingMode()
+{
+	exitCastingMode();
+	LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[731]); //Spell cancelled
+}
+
+void AdventureMapInterface::performSpellcasting(const int3 & dest)
+{
+	SpellID id = spellBeingCasted->id;
+	exitCastingMode();
+	LOCPLINT->cb->castSpell(LOCPLINT->localState->getCurrentHero(), id, dest);
+}
+
+Rect AdventureMapInterface::terrainAreaPixels() const
+{
+	return widget->getMapView()->pos;
+}
+
+const IShipyard * AdventureMapInterface::ourInaccessibleShipyard(const CGObjectInstance *obj) const
+{
+	const IShipyard *ret = IShipyard::castFrom(obj);
+
+	if(!ret ||
+		obj->tempOwner != currentPlayerID ||
+		(CCS->curh->get<Cursor::Map>() != Cursor::Map::T1_SAIL && CCS->curh->get<Cursor::Map>() != Cursor::Map::POINTER))
+		return nullptr;
+
+	return ret;
+}
+
+void AdventureMapInterface::hotkeyExitWorldView()
+{
+	setState(EAdventureState::MAKING_TURN);
+	widget->getMapView()->onViewMapActivated();
+}
+
+void AdventureMapInterface::openWorldView(int tileSize)
+{
+	setState(EAdventureState::WORLD_VIEW);
+	widget->getMapView()->onViewWorldActivated(tileSize);
+}
+
+void AdventureMapInterface::openWorldView()
+{
+	openWorldView(11);
+}
+
+void AdventureMapInterface::openWorldView(const std::vector<ObjectPosInfo>& objectPositions, bool showTerrain)
+{
+	openWorldView(11);
+	widget->getMapView()->onViewSpellActivated(11, objectPositions, showTerrain);
+}
+
+void AdventureMapInterface::hotkeyNextTown()
+{
+	widget->getTownList()->selectNext();
+}
+
+void AdventureMapInterface::hotkeySwitchMapLevel()
+{
+	widget->getMapView()->onMapLevelSwitched();
+}
+
+void AdventureMapInterface::hotkeyZoom(int delta)
+{
+	widget->getMapView()->onMapZoomLevelChanged(delta);
+}
+
+void AdventureMapInterface::onScreenResize()
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	widget.reset();
+	pos.x = pos.y = 0;
+	pos.w = GH.screenDimensions().x;
+	pos.h = GH.screenDimensions().y;
+
+	widget = std::make_shared<AdventureMapWidget>(shortcuts);
+	widget->getMapView()->onViewMapActivated();
+	widget->setPlayer(currentPlayerID);
+	widget->updateActiveState();
+	widget->getMinimap()->update();
+	widget->getInfoBar()->showSelection();
+
+	adjustActiveness();
+}

+ 41 - 86
client/adventureMap/CAdventureMapInterface.h → client/adventureMap/AdventureMapInterface.h

@@ -1,5 +1,5 @@
 /*
- * CAdvMapInt.h, part of VCMI engine
+ * AdventureMapInterface.h, part of VCMI engine
  *
  * Authors: listed in file AUTHORS in main folder
  *
@@ -29,8 +29,8 @@ class CButton;
 class IImage;
 class CAnimImage;
 class CGStatusBar;
-class CAdvMapPanel;
-class CAdvMapWorldViewPanel;
+class AdventureMapWidget;
+class AdventureMapShortcuts;
 class CAnimation;
 class MapView;
 class CResDataBar;
@@ -39,122 +39,77 @@ class CTownList;
 class CInfoBar;
 class CMinimap;
 class MapAudioPlayer;
+enum class EAdventureState;
 
 struct MapDrawingInfo;
 
 /// That's a huge class which handles general adventure map actions and
 /// shows the right menu(questlog, spellbook, end turn,..) from where you
 /// can get to the towns and heroes.
-class CAdventureMapInterface : public CIntObject
+class AdventureMapInterface : public CIntObject
 {
 private:
-	enum class EGameState
-	{
-		NOT_INITIALIZED,
-		HOTSEAT_WAIT,
-		MAKING_TURN,
-		ENEMY_TURN,
-		WORLD_VIEW
-	};
-
-	EGameState state;
-
 	/// currently acting player
 	PlayerColor currentPlayerID;
 
-	/// uses EDirections enum
-	bool scrollingCursorSet;
-
-	const CSpell *spellBeingCasted; //nullptr if none
-
-	std::vector<std::shared_ptr<CAnimImage>> gems;
-
-	std::shared_ptr<IImage> bg;
-	std::shared_ptr<IImage> bgWorldView;
-	std::shared_ptr<CButton> kingOverview;
-	std::shared_ptr<CButton> sleepWake;
-	std::shared_ptr<CButton> underground;
-	std::shared_ptr<CButton> questlog;
-	std::shared_ptr<CButton> moveHero;
-	std::shared_ptr<CButton> spellbook;
-	std::shared_ptr<CButton> advOptions;
-	std::shared_ptr<CButton> sysOptions;
-	std::shared_ptr<CButton> nextHero;
-	std::shared_ptr<CButton> endTurn;
-	std::shared_ptr<CButton> worldViewUnderground;
-
-	std::shared_ptr<MapView> terrain;
-	std::shared_ptr<CMinimap> minimap;
-	std::shared_ptr<CHeroList> heroList;
-	std::shared_ptr<CTownList> townList;
-	std::shared_ptr<CInfoBar> infoBar;
-	std::shared_ptr<CGStatusBar> statusbar;
-	std::shared_ptr<CResDataBar> resdatabar;
-
-	std::shared_ptr<CAdvMapPanel> panelMain; // panel that holds all right-side buttons in normal view
-	std::shared_ptr<CAdvMapWorldViewPanel> panelWorldView; // panel that holds all buttons and other ui in world view
-	std::shared_ptr<CAdvMapPanel> activeMapPanel; // currently active panel (either main or world view, depending on current mode)
-
-	std::shared_ptr<CAnimation> worldViewIcons;// images for world view overlay
+	/// if true, cursor was changed to scrolling and must be reset back once scroll is over
+	bool scrollingWasActive;
+
+	/// if true, then scrolling was blocked via ctrl and should not restart until player move cursor outside scrolling area
+	bool scrollingWasBlocked;
+
+	/// spell for which player is selecting target, or nullptr if none
+	const CSpell *spellBeingCasted;
 
 	std::shared_ptr<MapAudioPlayer> mapAudio;
+	std::shared_ptr<AdventureMapWidget> widget;
+	std::shared_ptr<AdventureMapShortcuts> shortcuts;
 
 private:
-	//functions bound to buttons
-	void fshowOverview();
-	void fworldViewBack();
-	void fworldViewScale1x();
-	void fworldViewScale2x();
-	void fworldViewScale4x();
-	void fswitchLevel();
-	void fshowQuestlog();
-	void fsleepWake();
-	void fmoveHero();
-	void fshowSpellbok();
-	void fadventureOPtions();
-	void fsystemOptions();
-	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
-
-	const IShipyard * ourInaccessibleShipyard(const CGObjectInstance *obj) const; //checks if obj is our ashipyard and cursor is 0,0 -> returns shipyard or nullptr else
-
-	// update locked state of buttons
-	void updateButtons();
-
-	void handleMapScrollingUpdate();
+	void setState(EAdventureState state);
 
-	void showMoveDetailsInStatusbar(const CGHeroInstance & hero, const CGPathNode & pathNode);
+	/// updates active state of game window whenever game state changes
+	void adjustActiveness();
 
-	const CGObjectInstance *getActiveObject(const int3 &tile);
+	/// checks if obj is our ashipyard and cursor is 0,0 -> returns shipyard or nullptr else
+	const IShipyard * ourInaccessibleShipyard(const CGObjectInstance *obj) const;
 
-	std::optional<Point> keyToMoveDirection(EShortcut key);
+	/// check and if necessary reacts on scrolling by moving cursor to screen edge
+	void handleMapScrollingUpdate(uint32_t msPassed);
 
-	void endingTurn();
+	void showMoveDetailsInStatusbar(const CGHeroInstance & hero, const CGPathNode & pathNode);
+
+	const CGObjectInstance *getActiveObject(const int3 &tile);
 
 	/// exits currently opened world view mode and returns to normal map
-	void exitWorldView();
 	void exitCastingMode();
-	void leaveCastingMode(const int3 & castTarget);
-	void abortCastingMode();
+
+	/// casts current spell at specified location
+	void performSpellcasting(const int3 & castTarget);
 
 protected:
-	// CIntObject interface implementation
+	/// CIntObject interface implementation
 
 	void activate() override;
 	void deactivate() override;
 
+	void tick(uint32_t msPassed) override;
 	void show(SDL_Surface * to) override;
 	void showAll(SDL_Surface * to) override;
 
 	void keyPressed(EShortcut key) override;
 
+	void onScreenResize() override;
+
 public:
-	CAdventureMapInterface();
+	AdventureMapInterface();
+
+	void hotkeyAbortCastingMode();
+	void hotkeyExitWorldView();
+	void hotkeyEndingTurn();
+	void hotkeyNextTown();
+	void hotkeySwitchMapLevel();
+	void hotkeyZoom(int delta);
 
 	/// Called by PlayerInterface when specified player is ready to start his turn
 	void onHotseatWaitStarted(PlayerColor playerID);
@@ -225,4 +180,4 @@ public:
 	void openWorldView(const std::vector<ObjectPosInfo>& objectPositions, bool showTerrain);
 };
 
-extern std::shared_ptr<CAdventureMapInterface> adventureInt;
+extern std::shared_ptr<AdventureMapInterface> adventureInt;

+ 455 - 0
client/adventureMap/AdventureMapShortcuts.cpp

@@ -0,0 +1,455 @@
+/*
+ * AdventureMapShortcuts.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 "AdventureMapShortcuts.h"
+
+#include "../CGameInfo.h"
+#include "../CPlayerInterface.h"
+#include "../PlayerLocalState.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
+#include "../gui/WindowHandler.h"
+#include "../lobby/CSavingScreen.h"
+#include "../mapView/mapHandler.h"
+#include "../windows/CKingdomInterface.h"
+#include "../windows/CSpellWindow.h"
+#include "../windows/CTradeWindow.h"
+#include "../windows/settings/SettingsMainWindow.h"
+#include "AdventureMapInterface.h"
+#include "AdventureOptions.h"
+#include "AdventureState.h"
+
+#include "../../CCallback.h"
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/CPathfinder.h"
+#include "../../lib/mapObjects/CGHeroInstance.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
+#include "../../lib/mapping/CMap.h"
+
+AdventureMapShortcuts::AdventureMapShortcuts(AdventureMapInterface & owner)
+	: owner(owner)
+	, state(EAdventureState::NOT_INITIALIZED)
+	, mapLevel(0)
+{}
+
+void AdventureMapShortcuts::setState(EAdventureState newState)
+{
+	state = newState;
+}
+
+void AdventureMapShortcuts::onMapViewMoved(const Rect & visibleArea, int newMapLevel)
+{
+	mapLevel = newMapLevel;
+}
+
+std::vector<AdventureMapShortcutState> AdventureMapShortcuts::getShortcuts()
+{
+	std::vector<AdventureMapShortcutState> result = {
+		{ EShortcut::ADVENTURE_KINGDOM_OVERVIEW, optionInMapView(),      [this]() { this->showOverview(); } },
+		{ EShortcut::ADVENTURE_EXIT_WORLD_VIEW,  optionInWorldView(),    [this]() { this->worldViewBack(); } },
+		{ EShortcut::ADVENTURE_VIEW_WORLD,       optionInMapView(),      [this]() { this->worldViewScale1x(); } },
+		{ EShortcut::ADVENTURE_VIEW_WORLD_X1,    optionInWorldView(),    [this]() { this->worldViewScale1x(); } },
+		{ EShortcut::ADVENTURE_VIEW_WORLD_X2,    optionInWorldView(),    [this]() { this->worldViewScale2x(); } },
+		{ EShortcut::ADVENTURE_VIEW_WORLD_X4,    optionInWorldView(),    [this]() { this->worldViewScale4x(); } },
+		{ EShortcut::ADVENTURE_TOGGLE_MAP_LEVEL, optionCanToggleLevel(), [this]() { this->switchMapLevel(); } },
+		{ EShortcut::ADVENTURE_QUEST_LOG,        optionCanViewQuests(),  [this]() { this->showQuestlog(); } },
+		{ EShortcut::ADVENTURE_TOGGLE_SLEEP,     optionHeroSelected(),   [this]() { this->toggleSleepWake(); } },
+		{ EShortcut::ADVENTURE_SET_HERO_ASLEEP,  optionHeroAwake(),      [this]() { this->setHeroSleeping(); } },
+		{ EShortcut::ADVENTURE_SET_HERO_AWAKE,   optionHeroSleeping(),   [this]() { this->setHeroAwake(); } },
+		{ EShortcut::ADVENTURE_MOVE_HERO,        optionHeroCanMove(),    [this]() { this->moveHeroAlongPath(); } },
+		{ EShortcut::ADVENTURE_CAST_SPELL,       optionHeroSelected(),   [this]() { this->showSpellbook(); } },
+		{ EShortcut::ADVENTURE_GAME_OPTIONS,     optionInMapView(),      [this]() { this->adventureOptions(); } },
+		{ EShortcut::GLOBAL_OPTIONS,             optionInMapView(),      [this]() { this->systemOptions(); } },
+		{ EShortcut::ADVENTURE_NEXT_HERO,        optionHasNextHero(),    [this]() { this->nextHero(); } },
+		{ EShortcut::GAME_END_TURN,              optionInMapView(),      [this]() { this->endTurn(); } },
+		{ EShortcut::ADVENTURE_THIEVES_GUILD,    optionInMapView(),      [this]() { this->showThievesGuild(); } },
+		{ EShortcut::ADVENTURE_VIEW_SCENARIO,    optionInMapView(),      [this]() { this->showScenarioInfo(); } },
+		{ EShortcut::GAME_SAVE_GAME,             optionInMapView(),      [this]() { this->saveGame(); } },
+		{ EShortcut::GAME_LOAD_GAME,             optionInMapView(),      [this]() { this->loadGame(); } },
+		{ EShortcut::ADVENTURE_DIG_GRAIL,        optionHeroSelected(),   [this]() { this->digGrail(); } },
+		{ EShortcut::ADVENTURE_VIEW_PUZZLE,      optionSidePanelActive(),[this]() { this->viewPuzzleMap(); } },
+		{ EShortcut::GAME_RESTART_GAME,          optionInMapView(),      [this]() { this->restartGame(); } },
+		{ EShortcut::ADVENTURE_VISIT_OBJECT,     optionHeroSelected(),   [this]() { this->visitObject(); } },
+		{ EShortcut::ADVENTURE_VIEW_SELECTED,    optionInMapView(),      [this]() { this->openObject(); } },
+		{ EShortcut::GAME_OPEN_MARKETPLACE,      optionInMapView(),      [this]() { this->showMarketplace(); } },
+		{ EShortcut::ADVENTURE_ZOOM_IN,          optionSidePanelActive(),[this]() { this->zoom(+1); } },
+		{ EShortcut::ADVENTURE_ZOOM_OUT,         optionSidePanelActive(),[this]() { this->zoom(-1); } },
+		{ EShortcut::ADVENTURE_ZOOM_RESET,       optionSidePanelActive(),[this]() { this->zoom( 0); } },
+		{ EShortcut::ADVENTURE_NEXT_TOWN,        optionInMapView(),      [this]() { this->nextTown(); } },
+		{ EShortcut::ADVENTURE_NEXT_OBJECT,      optionInMapView(),      [this]() { this->nextObject(); } },
+		{ EShortcut::ADVENTURE_MOVE_HERO_SW,     optionHeroSelected(),   [this]() { this->moveHeroDirectional({-1, +1}); } },
+		{ EShortcut::ADVENTURE_MOVE_HERO_SS,     optionHeroSelected(),   [this]() { this->moveHeroDirectional({ 0, +1}); } },
+		{ EShortcut::ADVENTURE_MOVE_HERO_SE,     optionHeroSelected(),   [this]() { this->moveHeroDirectional({+1, +1}); } },
+		{ EShortcut::ADVENTURE_MOVE_HERO_WW,     optionHeroSelected(),   [this]() { this->moveHeroDirectional({-1,  0}); } },
+		{ EShortcut::ADVENTURE_MOVE_HERO_EE,     optionHeroSelected(),   [this]() { this->moveHeroDirectional({+1,  0}); } },
+		{ EShortcut::ADVENTURE_MOVE_HERO_NW,     optionHeroSelected(),   [this]() { this->moveHeroDirectional({-1, -1}); } },
+		{ EShortcut::ADVENTURE_MOVE_HERO_NN,     optionHeroSelected(),   [this]() { this->moveHeroDirectional({ 0, -1}); } },
+		{ EShortcut::ADVENTURE_MOVE_HERO_NE,     optionHeroSelected(),   [this]() { this->moveHeroDirectional({+1, -1}); } }
+	};
+	return result;
+}
+
+void AdventureMapShortcuts::showOverview()
+{
+	GH.windows().createAndPushWindow<CKingdomInterface>();
+}
+
+void AdventureMapShortcuts::worldViewBack()
+{
+	owner.hotkeyExitWorldView();
+
+	auto hero = LOCPLINT->localState->getCurrentHero();
+	if (hero)
+		owner.centerOnObject(hero);
+}
+
+void AdventureMapShortcuts::worldViewScale1x()
+{
+	// TODO set corresponding scale button to "selected" mode
+	owner.openWorldView(7);
+}
+
+void AdventureMapShortcuts::worldViewScale2x()
+{
+	owner.openWorldView(11);
+}
+
+void AdventureMapShortcuts::worldViewScale4x()
+{
+	owner.openWorldView(16);
+}
+
+void AdventureMapShortcuts::switchMapLevel()
+{
+	int maxLevels = LOCPLINT->cb->getMapSize().z;
+	if (maxLevels < 2)
+		return;
+
+	owner.hotkeySwitchMapLevel();
+}
+
+void AdventureMapShortcuts::showQuestlog()
+{
+	LOCPLINT->showQuestLog();
+}
+
+void AdventureMapShortcuts::toggleSleepWake()
+{
+	if (!optionHeroSelected())
+		return;
+
+	if (optionHeroAwake())
+		setHeroSleeping();
+	else
+		setHeroAwake();
+}
+
+void AdventureMapShortcuts::setHeroSleeping()
+{
+	const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero();
+	if (h)
+	{
+		LOCPLINT->localState->setHeroAsleep(h);
+		owner.onHeroChanged(h);
+		nextHero();
+	}
+}
+
+void AdventureMapShortcuts::setHeroAwake()
+{
+	const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero();
+	if (h)
+	{
+		LOCPLINT->localState->setHeroAwaken(h);
+		owner.onHeroChanged(h);
+	}
+}
+
+void AdventureMapShortcuts::moveHeroAlongPath()
+{
+	const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero();
+	if (!h || !LOCPLINT->localState->hasPath(h))
+		return;
+
+	LOCPLINT->moveHero(h, LOCPLINT->localState->getPath(h));
+}
+
+void AdventureMapShortcuts::showSpellbook()
+{
+	if (!LOCPLINT->localState->getCurrentHero())
+		return;
+
+	owner.centerOnObject(LOCPLINT->localState->getCurrentHero());
+
+	GH.windows().createAndPushWindow<CSpellWindow>(LOCPLINT->localState->getCurrentHero(), LOCPLINT, false);
+}
+
+void AdventureMapShortcuts::adventureOptions()
+{
+	GH.windows().createAndPushWindow<AdventureOptions>();
+}
+
+void AdventureMapShortcuts::systemOptions()
+{
+	GH.windows().createAndPushWindow<SettingsMainWindow>();
+}
+
+void AdventureMapShortcuts::nextHero()
+{
+	const auto * currHero = LOCPLINT->localState->getCurrentHero();
+	const auto * nextHero = LOCPLINT->localState->getNextWanderingHero(currHero);
+
+	if (nextHero)
+	{
+		LOCPLINT->localState->setSelection(nextHero);
+		owner.centerOnObject(nextHero);
+	}
+}
+
+void AdventureMapShortcuts::endTurn()
+{
+	if(!LOCPLINT->makingTurn)
+		return;
+
+	if(settings["adventure"]["heroReminder"].Bool())
+	{
+		for(auto hero : LOCPLINT->localState->getWanderingHeroes())
+		{
+			if(!LOCPLINT->localState->isHeroSleeping(hero) && hero->movement > 0)
+			{
+				// Only show hero reminder if conditions met:
+				// - There still movement points
+				// - Hero don't have a path or there not points for first step on path
+				LOCPLINT->localState->verifyPath(hero);
+
+				if(!LOCPLINT->localState->hasPath(hero))
+				{
+					LOCPLINT->showYesNoDialog( CGI->generaltexth->allTexts[55], [this](){ owner.hotkeyEndingTurn(); }, nullptr);
+					return;
+				}
+
+				auto path = LOCPLINT->localState->getPath(hero);
+				if (path.nodes.size() < 2 || path.nodes[path.nodes.size() - 2].turns)
+				{
+					LOCPLINT->showYesNoDialog( CGI->generaltexth->allTexts[55], [this](){ owner.hotkeyEndingTurn(); }, nullptr);
+					return;
+				}
+			}
+		}
+	}
+	owner.hotkeyEndingTurn();
+}
+
+void AdventureMapShortcuts::showThievesGuild()
+{
+	//find first town with tavern
+	auto itr = range::find_if(LOCPLINT->localState->getOwnedTowns(), [](const CGTownInstance * town)
+	{
+		return town->hasBuilt(BuildingID::TAVERN);
+	});
+
+	if(itr != LOCPLINT->localState->getOwnedTowns().end())
+		LOCPLINT->showThievesGuildWindow(*itr);
+	else
+		LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithTavern"));
+}
+
+void AdventureMapShortcuts::showScenarioInfo()
+{
+	AdventureOptions::showScenarioInfo();
+}
+
+void AdventureMapShortcuts::saveGame()
+{
+	GH.windows().createAndPushWindow<CSavingScreen>();
+}
+
+void AdventureMapShortcuts::loadGame()
+{
+	LOCPLINT->proposeLoadingGame();
+}
+
+void AdventureMapShortcuts::digGrail()
+{
+	const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero();
+
+	if(h && LOCPLINT->makingTurn)
+		LOCPLINT->tryDiggging(h);
+}
+
+void AdventureMapShortcuts::viewPuzzleMap()
+{
+	LOCPLINT->showPuzzleMap();
+}
+
+void AdventureMapShortcuts::restartGame()
+{
+	LOCPLINT->showYesNoDialog(CGI->generaltexth->translate("vcmi.adventureMap.confirmRestartGame"),
+		[](){ GH.pushUserEvent(EUserEvent::RESTART_GAME); }, nullptr);
+}
+
+void AdventureMapShortcuts::visitObject()
+{
+	const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero();
+
+	if(h)
+		LOCPLINT->cb->moveHero(h,h->pos);
+}
+
+void AdventureMapShortcuts::openObject()
+{
+	const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero();
+	const CGTownInstance *t = LOCPLINT->localState->getCurrentTown();
+	if(h)
+		LOCPLINT->openHeroWindow(h);
+
+	if(t)
+		LOCPLINT->openTownWindow(t);
+}
+
+void AdventureMapShortcuts::showMarketplace()
+{
+	//check if we have any marketplace
+	const CGTownInstance *townWithMarket = nullptr;
+	for(const CGTownInstance *t : LOCPLINT->cb->getTownsInfo())
+	{
+		if(t->hasBuilt(BuildingID::MARKETPLACE))
+		{
+			townWithMarket = t;
+			break;
+		}
+	}
+
+	if(townWithMarket) //if any town has marketplace, open window
+		GH.windows().createAndPushWindow<CMarketplaceWindow>(townWithMarket);
+	else //if not - complain
+		LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket"));
+}
+
+void AdventureMapShortcuts::nextTown()
+{
+	owner.hotkeyNextTown();
+}
+
+void AdventureMapShortcuts::zoom( int distance)
+{
+	owner.hotkeyZoom(distance);
+}
+
+void AdventureMapShortcuts::nextObject()
+{
+	const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero();
+	const CGTownInstance *t = LOCPLINT->localState->getCurrentTown();
+	if(h)
+		nextHero();
+
+	if(t)
+		nextTown();
+}
+
+void AdventureMapShortcuts::moveHeroDirectional(const Point & direction)
+{
+	const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero(); //selected hero
+
+	if(!h)
+		return;
+
+	if (CGI->mh->hasOngoingAnimations())
+		return;
+
+	int3 dst = h->visitablePos() + int3(direction.x, direction.y, 0);
+
+	if (!CGI->mh->isInMap((dst)))
+		return;
+
+	if ( !LOCPLINT->localState->setPath(h, dst))
+		return;
+
+	const CGPath & path = LOCPLINT->localState->getPath(h);
+
+	if (path.nodes.size() > 2)
+		owner.onHeroChanged(h);
+	else
+		if(path.nodes[0].turns == 0)
+			LOCPLINT->moveHero(h, path);
+}
+
+bool AdventureMapShortcuts::optionCanViewQuests()
+{
+	return optionInMapView() && CGI->mh->getMap()->quests.empty();
+}
+
+bool AdventureMapShortcuts::optionCanToggleLevel()
+{
+	return optionInMapView() && LOCPLINT->cb->getMapSize().z > 0;
+}
+
+bool AdventureMapShortcuts::optionMapLevelSurface()
+{
+	return mapLevel == 0;
+}
+
+bool AdventureMapShortcuts::optionHeroSleeping()
+{
+	const CGHeroInstance *hero = LOCPLINT->localState->getCurrentHero();
+	return optionInMapView() && hero && LOCPLINT->localState->isHeroSleeping(hero);
+}
+
+bool AdventureMapShortcuts::optionHeroAwake()
+{
+	const CGHeroInstance *hero = LOCPLINT->localState->getCurrentHero();
+	return optionInMapView() && hero && !LOCPLINT->localState->isHeroSleeping(hero);
+}
+
+bool AdventureMapShortcuts::optionHeroSelected()
+{
+	return optionInMapView() && LOCPLINT->localState->getCurrentHero() != nullptr;
+}
+
+bool AdventureMapShortcuts::optionHeroCanMove()
+{
+	const auto * hero = LOCPLINT->localState->getCurrentHero();
+	return optionInMapView() && hero && hero->movement != 0 && LOCPLINT->localState->hasPath(hero);
+}
+
+bool AdventureMapShortcuts::optionHasNextHero()
+{
+	const auto * hero = LOCPLINT->localState->getCurrentHero();
+	const auto * nextSuitableHero = LOCPLINT->localState->getNextWanderingHero(hero);
+
+	return optionInMapView() && nextSuitableHero != nullptr;
+}
+
+bool AdventureMapShortcuts::optionSpellcasting()
+{
+	return state == EAdventureState::CASTING_SPELL;
+}
+
+bool AdventureMapShortcuts::optionInMapView()
+{
+	return state == EAdventureState::MAKING_TURN;
+}
+
+bool AdventureMapShortcuts::optionInWorldView()
+{
+	return state == EAdventureState::WORLD_VIEW;
+}
+
+bool AdventureMapShortcuts::optionSidePanelActive()
+{
+	return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW;
+}
+
+bool AdventureMapShortcuts::optionMapViewActive()
+{
+	return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::CASTING_SPELL;
+}

+ 88 - 0
client/adventureMap/AdventureMapShortcuts.h

@@ -0,0 +1,88 @@
+/*
+ * AdventureMapShortcuts.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
+
+VCMI_LIB_NAMESPACE_BEGIN
+class Point;
+class Rect;
+VCMI_LIB_NAMESPACE_END
+
+enum class EShortcut;
+class AdventureMapInterface;
+enum class EAdventureState;
+
+struct AdventureMapShortcutState
+{
+	EShortcut shortcut;
+	bool isEnabled;
+	std::function<void()> callback;
+};
+
+/// Class that contains list of functions for shortcuts available from adventure map
+class AdventureMapShortcuts
+{
+	AdventureMapInterface & owner;
+	EAdventureState state;
+	int mapLevel;
+
+	void showOverview();
+	void worldViewBack();
+	void worldViewScale1x();
+	void worldViewScale2x();
+	void worldViewScale4x();
+	void switchMapLevel();
+	void showQuestlog();
+	void toggleSleepWake();
+	void setHeroSleeping();
+	void setHeroAwake();
+	void moveHeroAlongPath();
+	void showSpellbook();
+	void adventureOptions();
+	void systemOptions();
+	void nextHero();
+	void endTurn();
+	void showThievesGuild();
+	void showScenarioInfo();
+	void saveGame();
+	void loadGame();
+	void digGrail();
+	void viewPuzzleMap();
+	void restartGame();
+	void visitObject();
+	void openObject();
+	void showMarketplace();
+	void nextTown();
+	void nextObject();
+	void zoom( int distance);
+	void moveHeroDirectional(const Point & direction);
+
+public:
+	explicit AdventureMapShortcuts(AdventureMapInterface & owner);
+
+	std::vector<AdventureMapShortcutState> getShortcuts();
+
+	bool optionCanViewQuests();
+	bool optionCanToggleLevel();
+	bool optionMapLevelSurface();
+	bool optionHeroSleeping();
+	bool optionHeroAwake();
+	bool optionHeroSelected();
+	bool optionHeroCanMove();
+	bool optionHasNextHero();
+	bool optionSpellcasting();
+	bool optionInMapView();
+	bool optionInWorldView();
+	bool optionSidePanelActive();
+	bool optionMapViewActive();
+
+	void setState(EAdventureState newState);
+	void onMapViewMoved(const Rect & visibleArea, int mapLevel);
+};

+ 455 - 0
client/adventureMap/AdventureMapWidget.cpp

@@ -0,0 +1,455 @@
+/*
+ * CAdventureMapWidget.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 "AdventureMapWidget.h"
+
+#include "AdventureMapShortcuts.h"
+#include "CInfoBar.h"
+#include "CList.h"
+#include "CMinimap.h"
+#include "CResDataBar.h"
+#include "AdventureState.h"
+
+#include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
+#include "../mapView/MapView.h"
+#include "../render/CAnimation.h"
+#include "../render/IImage.h"
+#include "../widgets/Buttons.h"
+#include "../widgets/Images.h"
+#include "../widgets/TextControls.h"
+
+#include "../CPlayerInterface.h"
+#include "../PlayerLocalState.h"
+
+#include "../../lib/StringConstants.h"
+#include "../../lib/filesystem/ResourceID.h"
+
+AdventureMapWidget::AdventureMapWidget( std::shared_ptr<AdventureMapShortcuts> shortcuts )
+	: shortcuts(shortcuts)
+	, mapLevel(0)
+{
+	pos.x = pos.y = 0;
+	pos.w = GH.screenDimensions().x;
+	pos.h = GH.screenDimensions().y;
+
+	REGISTER_BUILDER("adventureInfobar",         &AdventureMapWidget::buildInfobox         );
+	REGISTER_BUILDER("adventureMapImage",        &AdventureMapWidget::buildMapImage        );
+	REGISTER_BUILDER("adventureMapButton",       &AdventureMapWidget::buildMapButton       );
+	REGISTER_BUILDER("adventureMapContainer",    &AdventureMapWidget::buildMapContainer    );
+	REGISTER_BUILDER("adventureMapGameArea",     &AdventureMapWidget::buildMapGameArea     );
+	REGISTER_BUILDER("adventureMapHeroList",     &AdventureMapWidget::buildMapHeroList     );
+	REGISTER_BUILDER("adventureMapIcon",         &AdventureMapWidget::buildMapIcon         );
+	REGISTER_BUILDER("adventureMapTownList",     &AdventureMapWidget::buildMapTownList     );
+	REGISTER_BUILDER("adventureMinimap",         &AdventureMapWidget::buildMinimap         );
+	REGISTER_BUILDER("adventureResourceDateBar", &AdventureMapWidget::buildResourceDateBar );
+	REGISTER_BUILDER("adventureStatusBar",       &AdventureMapWidget::buildStatusBar       );
+	REGISTER_BUILDER("adventurePlayerTexture",   &AdventureMapWidget::buildTexturePlayerColored);
+
+	for (const auto & entry : shortcuts->getShortcuts())
+		addShortcut(entry.shortcut, entry.callback);
+
+	const JsonNode config(ResourceID("config/widgets/adventureMap.json"));
+
+	for(const auto & entry : config["options"]["imagesPlayerColored"].Vector())
+	{
+		ResourceID resourceName(entry.String(), EResType::IMAGE);
+		playerColorerImages.push_back(resourceName.getName());
+	}
+
+	build(config);
+	addUsedEvents(KEYBOARD);
+}
+
+void AdventureMapWidget::onMapViewMoved(const Rect & visibleArea, int newMapLevel)
+{
+	if(mapLevel == newMapLevel)
+		return;
+
+	mapLevel = newMapLevel;
+	updateActiveState();
+}
+
+Rect AdventureMapWidget::readSourceArea(const JsonNode & source, const JsonNode & sourceCommon)
+{
+	const auto & input = source.isNull() ? sourceCommon : source;
+
+	return readArea(input, Rect(Point(0, 0), Point(800, 600)));
+}
+
+Rect AdventureMapWidget::readTargetArea(const JsonNode & source)
+{
+	if(subwidgetSizes.empty())
+		return readArea(source, pos);
+	return readArea(source, subwidgetSizes.back());
+}
+
+Rect AdventureMapWidget::readArea(const JsonNode & source, const Rect & boundingBox)
+{
+	const auto & object = source.Struct();
+
+	if(object.count("left") + object.count("width") + object.count("right") != 2)
+		logGlobal->error("Invalid area definition in widget! Unable to load width!");
+
+	if(object.count("top") + object.count("height") + object.count("bottom") != 2)
+		logGlobal->error("Invalid area definition in widget! Unable to load height!");
+
+	int left = source["left"].Integer();
+	int width = source["width"].Integer();
+	int right = source["right"].Integer();
+
+	int top = source["top"].Integer();
+	int height = source["height"].Integer();
+	int bottom = source["bottom"].Integer();
+
+	Point topLeft(left, top);
+	Point dimensions(width, height);
+
+	if(source["left"].isNull())
+		topLeft.x = boundingBox.w - right - width;
+
+	if(source["width"].isNull())
+		dimensions.x = boundingBox.w - right - left;
+
+	if(source["top"].isNull())
+		topLeft.y = boundingBox.h - bottom - height;
+
+	if(source["height"].isNull())
+		dimensions.y = boundingBox.h - bottom - top;
+
+	return Rect(topLeft + boundingBox.topLeft(), dimensions);
+}
+
+std::shared_ptr<IImage> AdventureMapWidget::loadImage(const std::string & name)
+{
+	ResourceID resource(name, EResType::IMAGE);
+
+	if(images.count(resource.getName()) == 0)
+		images[resource.getName()] = IImage::createFromFile(resource.getName());
+
+	return images[resource.getName()];
+}
+
+std::shared_ptr<CAnimation> AdventureMapWidget::loadAnimation(const std::string & name)
+{
+	ResourceID resource(name, EResType::ANIMATION);
+
+	if(animations.count(resource.getName()) == 0)
+		animations[resource.getName()] = std::make_shared<CAnimation>(resource.getName());
+
+	return animations[resource.getName()];
+}
+
+std::shared_ptr<CIntObject> AdventureMapWidget::buildInfobox(const JsonNode & input)
+{
+	Rect area = readTargetArea(input["area"]);
+	infoBar = std::make_shared<CInfoBar>(area);
+	return infoBar;
+}
+
+std::shared_ptr<CIntObject> AdventureMapWidget::buildMapImage(const JsonNode & input)
+{
+	Rect targetArea = readTargetArea(input["area"]);
+	Rect sourceArea = readSourceArea(input["sourceArea"], input["area"]);
+	std::string image = input["image"].String();
+
+	return std::make_shared<CFilledTexture>(loadImage(image), targetArea, sourceArea);
+}
+
+std::shared_ptr<CIntObject> AdventureMapWidget::buildMapButton(const JsonNode & input)
+{
+	auto position = readTargetArea(input["area"]);
+	auto image = input["image"].String();
+	auto help = readHintText(input["help"]);
+	bool playerColored = input["playerColored"].Bool();
+
+	auto button = std::make_shared<CButton>(position.topLeft(), image, help, 0, EShortcut::NONE, playerColored);
+
+	loadButtonBorderColor(button, input["borderColor"]);
+	loadButtonHotkey(button, input["hotkey"]);
+
+	return button;
+}
+
+std::shared_ptr<CIntObject> AdventureMapWidget::buildMapContainer(const JsonNode & input)
+{
+	auto position = readTargetArea(input["area"]);
+	std::shared_ptr<CAdventureMapContainerWidget> result;
+
+	if (!input["exists"].isNull())
+	{
+		if (!input["exists"]["heightMin"].isNull() && input["exists"]["heightMin"].Integer() >= pos.h)
+			return nullptr;
+		if (!input["exists"]["heightMax"].isNull() && input["exists"]["heightMax"].Integer() < pos.h)
+			return nullptr;
+		if (!input["exists"]["widthMin"].isNull() && input["exists"]["widthMin"].Integer() >= pos.w)
+			return nullptr;
+		if (!input["exists"]["widthMax"].isNull() && input["exists"]["widthMax"].Integer() < pos.w)
+			return nullptr;
+	}
+
+	if (input["overlay"].Bool())
+		result = std::make_shared<CAdventureMapOverlayWidget>();
+	else
+		result = std::make_shared<CAdventureMapContainerWidget>();
+
+	result->disableCondition = input["hideWhen"].String();
+
+	result->moveBy(position.topLeft());
+	subwidgetSizes.push_back(position);
+	for(const auto & entry : input["items"].Vector())
+	{
+		auto widget = buildWidget(entry);
+
+		addWidget(entry["name"].String(), widget);
+		result->ownedChildren.push_back(widget);
+
+		// FIXME: remove cast and replace it with better check
+		if (std::dynamic_pointer_cast<CLabel>(widget) || std::dynamic_pointer_cast<CLabelGroup>(widget))
+			result->addChild(widget.get(), true);
+		else
+			result->addChild(widget.get(), false);
+	}
+	subwidgetSizes.pop_back();
+
+	return result;
+}
+
+std::shared_ptr<CIntObject> AdventureMapWidget::buildMapGameArea(const JsonNode & input)
+{
+	Rect area = readTargetArea(input["area"]);
+	mapView = std::make_shared<MapView>(area.topLeft(), area.dimensions());
+	return mapView;
+}
+
+std::shared_ptr<CIntObject> AdventureMapWidget::buildMapHeroList(const JsonNode & input)
+{
+	Rect area = readTargetArea(input["area"]);
+	subwidgetSizes.push_back(area);
+
+	Rect item = readTargetArea(input["item"]);
+
+	Point itemOffset(input["itemsOffset"]["x"].Integer(), input["itemsOffset"]["y"].Integer());
+	int itemsCount = input["itemsCount"].Integer();
+
+	auto result = std::make_shared<CHeroList>(itemsCount, area, item.topLeft() - area.topLeft(), itemOffset, LOCPLINT->localState->getWanderingHeroes().size());
+
+
+	if(!input["scrollUp"].isNull())
+		result->setScrollUpButton(std::dynamic_pointer_cast<CButton>(buildMapButton(input["scrollUp"])));
+
+	if(!input["scrollDown"].isNull())
+		result->setScrollDownButton(std::dynamic_pointer_cast<CButton>(buildMapButton(input["scrollDown"])));
+
+	subwidgetSizes.pop_back();
+
+	heroList = result;
+	return result;
+}
+
+std::shared_ptr<CIntObject> AdventureMapWidget::buildMapIcon(const JsonNode & input)
+{
+	Rect area = readTargetArea(input["area"]);
+	size_t index = input["index"].Integer();
+	size_t perPlayer = input["perPlayer"].Integer();
+	std::string image = input["image"].String();
+
+	return std::make_shared<CAdventureMapIcon>(area.topLeft(), loadAnimation(image), index, perPlayer);
+}
+
+std::shared_ptr<CIntObject> AdventureMapWidget::buildMapTownList(const JsonNode & input)
+{
+	Rect area = readTargetArea(input["area"]);
+	subwidgetSizes.push_back(area);
+
+	Rect item = readTargetArea(input["item"]);
+	Point itemOffset(input["itemsOffset"]["x"].Integer(), input["itemsOffset"]["y"].Integer());
+	int itemsCount = input["itemsCount"].Integer();
+
+	auto result = std::make_shared<CTownList>(itemsCount, area, item.topLeft() - area.topLeft(), itemOffset, LOCPLINT->localState->getOwnedTowns().size());
+
+
+	if(!input["scrollUp"].isNull())
+		result->setScrollUpButton(std::dynamic_pointer_cast<CButton>(buildMapButton(input["scrollUp"])));
+
+	if(!input["scrollDown"].isNull())
+		result->setScrollDownButton(std::dynamic_pointer_cast<CButton>(buildMapButton(input["scrollDown"])));
+
+	subwidgetSizes.pop_back();
+
+	townList = result;
+	return result;
+}
+
+std::shared_ptr<CIntObject> AdventureMapWidget::buildMinimap(const JsonNode & input)
+{
+	Rect area = readTargetArea(input["area"]);
+	minimap = std::make_shared<CMinimap>(area);
+	return minimap;
+}
+
+std::shared_ptr<CIntObject> AdventureMapWidget::buildResourceDateBar(const JsonNode & input)
+{
+	Rect area = readTargetArea(input["area"]);
+	std::string image = input["image"].String();
+
+	auto result = std::make_shared<CResDataBar>(image, area.topLeft());
+
+	for(auto i = 0; i < GameConstants::RESOURCE_QUANTITY; i++)
+	{
+		const auto & node = input[GameConstants::RESOURCE_NAMES[i]];
+
+		if(node.isNull())
+			continue;
+
+		result->setResourcePosition(GameResID(i), Point(node["x"].Integer(), node["y"].Integer()));
+	}
+
+	result->setDatePosition(Point(input["date"]["x"].Integer(), input["date"]["y"].Integer()));
+
+	return result;
+}
+
+std::shared_ptr<CIntObject> AdventureMapWidget::buildStatusBar(const JsonNode & input)
+{
+	Rect area = readTargetArea(input["area"]);
+	std::string image = input["image"].String();
+
+	auto background = std::make_shared<CFilledTexture>(image, area);
+
+	return CGStatusBar::create(background);
+}
+
+std::shared_ptr<CIntObject> AdventureMapWidget::buildTexturePlayerColored(const JsonNode & input)
+{
+	logGlobal->debug("Building widget CFilledTexture");
+	auto image = input["image"].String();
+	Rect area = readTargetArea(input["area"]);
+	return std::make_shared<FilledTexturePlayerColored>(image, area);
+}
+
+std::shared_ptr<CHeroList> AdventureMapWidget::getHeroList()
+{
+	return heroList;
+}
+
+std::shared_ptr<CTownList> AdventureMapWidget::getTownList()
+{
+	return townList;
+}
+
+std::shared_ptr<CMinimap> AdventureMapWidget::getMinimap()
+{
+	return minimap;
+}
+
+std::shared_ptr<MapView> AdventureMapWidget::getMapView()
+{
+	return mapView;
+}
+
+std::shared_ptr<CInfoBar> AdventureMapWidget::getInfoBar()
+{
+	return infoBar;
+}
+
+void AdventureMapWidget::setPlayer(const PlayerColor & player)
+{
+	setPlayerChildren(this, player);
+}
+
+void AdventureMapWidget::setPlayerChildren(CIntObject * widget, const PlayerColor & player)
+{
+	for(auto & entry : widget->children)
+	{
+		auto container = dynamic_cast<CAdventureMapContainerWidget *>(entry);
+		auto icon = dynamic_cast<CAdventureMapIcon *>(entry);
+		auto button = dynamic_cast<CButton *>(entry);
+		auto texture = dynamic_cast<FilledTexturePlayerColored *>(entry);
+
+		if(button)
+			button->setPlayerColor(player);
+
+		if(icon)
+			icon->setPlayer(player);
+
+		if(container)
+			setPlayerChildren(container, player);
+
+		if (texture)
+			texture->playerColored(player);
+	}
+
+	for(const auto & entry : playerColorerImages)
+	{
+		if(images.count(entry))
+			images[entry]->playerColored(player);
+	}
+
+	redraw();
+}
+
+CAdventureMapIcon::CAdventureMapIcon(const Point & position, std::shared_ptr<CAnimation> animation, size_t index, size_t iconsPerPlayer)
+	: index(index)
+	, iconsPerPlayer(iconsPerPlayer)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	pos += position;
+	image = std::make_shared<CAnimImage>(animation, index);
+}
+
+void CAdventureMapIcon::setPlayer(const PlayerColor & player)
+{
+	image->setFrame(index + player.getNum() * iconsPerPlayer);
+}
+
+void CAdventureMapOverlayWidget::show(SDL_Surface * to)
+{
+	CIntObject::showAll(to);
+}
+
+void AdventureMapWidget::updateActiveStateChildden(CIntObject * widget)
+{
+	for(auto & entry : widget->children)
+	{
+		auto container = dynamic_cast<CAdventureMapContainerWidget *>(entry);
+
+		if (container)
+		{
+			if (container->disableCondition == "heroAwake")
+				container->setEnabled(!shortcuts->optionHeroSleeping());
+
+			if (container->disableCondition == "heroSleeping")
+				container->setEnabled(shortcuts->optionHeroSleeping());
+
+			if (container->disableCondition == "mapLayerSurface")
+				container->setEnabled(shortcuts->optionMapLevelSurface());
+
+			if (container->disableCondition == "mapLayerUnderground")
+				container->setEnabled(!shortcuts->optionMapLevelSurface());
+
+			if (container->disableCondition == "mapViewMode")
+				container->setEnabled(shortcuts->optionInWorldView());
+
+			if (container->disableCondition == "worldViewMode")
+				container->setEnabled(!shortcuts->optionInWorldView());
+
+			updateActiveStateChildden(container);
+		}
+	}
+}
+
+void AdventureMapWidget::updateActiveState()
+{
+	updateActiveStateChildden(this);
+
+	for (auto entry: shortcuts->getShortcuts())
+		setShortcutBlocked(entry.shortcut, !entry.isEnabled);
+}

+ 109 - 0
client/adventureMap/AdventureMapWidget.h

@@ -0,0 +1,109 @@
+/*
+ * CAdventureMapWidget.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 "../gui/InterfaceObjectConfigurable.h"
+
+class CHeroList;
+class CTownList;
+class CMinimap;
+class MapView;
+class CInfoBar;
+class IImage;
+class AdventureMapShortcuts;
+enum class EAdventureState;
+
+/// Internal class of AdventureMapInterface that contains actual UI elements
+class AdventureMapWidget : public InterfaceObjectConfigurable
+{
+	int mapLevel;
+	/// temporary stack of sizes of currently building widgets
+	std::vector<Rect> subwidgetSizes;
+
+	/// list of images on which player-colored palette will be applied
+	std::vector<std::string> playerColorerImages;
+
+	/// list of named images shared between widgets
+	std::map<std::string, std::shared_ptr<IImage>> images;
+	std::map<std::string, std::shared_ptr<CAnimation>> animations;
+
+	/// Widgets that require access from adventure map
+	std::shared_ptr<CHeroList> heroList;
+	std::shared_ptr<CTownList> townList;
+	std::shared_ptr<CMinimap> minimap;
+	std::shared_ptr<MapView> mapView;
+	std::shared_ptr<CInfoBar> infoBar;
+
+	std::shared_ptr<AdventureMapShortcuts> shortcuts;
+
+	Rect readTargetArea(const JsonNode & source);
+	Rect readSourceArea(const JsonNode & source, const JsonNode & sourceCommon);
+	Rect readArea(const JsonNode & source, const Rect & boundingBox);
+
+	std::shared_ptr<IImage> loadImage(const std::string & name);
+	std::shared_ptr<CAnimation> loadAnimation(const std::string & name);
+
+	std::shared_ptr<CIntObject> buildInfobox(const JsonNode & input);
+	std::shared_ptr<CIntObject> buildMapImage(const JsonNode & input);
+	std::shared_ptr<CIntObject> buildMapButton(const JsonNode & input);
+	std::shared_ptr<CIntObject> buildMapContainer(const JsonNode & input);
+	std::shared_ptr<CIntObject> buildMapGameArea(const JsonNode & input);
+	std::shared_ptr<CIntObject> buildMapHeroList(const JsonNode & input);
+	std::shared_ptr<CIntObject> buildMapIcon(const JsonNode & input);
+	std::shared_ptr<CIntObject> buildMapTownList(const JsonNode & input);
+	std::shared_ptr<CIntObject> buildMinimap(const JsonNode & input);
+	std::shared_ptr<CIntObject> buildResourceDateBar(const JsonNode & input);
+	std::shared_ptr<CIntObject> buildStatusBar(const JsonNode & input);
+	std::shared_ptr<CIntObject> buildTexturePlayerColored(const JsonNode &);
+
+
+	void setPlayerChildren(CIntObject * widget, const PlayerColor & player);
+	void updateActiveStateChildden(CIntObject * widget);
+public:
+	explicit AdventureMapWidget( std::shared_ptr<AdventureMapShortcuts> shortcuts );
+
+	std::shared_ptr<CHeroList> getHeroList();
+	std::shared_ptr<CTownList> getTownList();
+	std::shared_ptr<CMinimap> getMinimap();
+	std::shared_ptr<MapView> getMapView();
+	std::shared_ptr<CInfoBar> getInfoBar();
+
+	void setPlayer(const PlayerColor & player);
+
+	void onMapViewMoved(const Rect & visibleArea, int mapLevel);
+	void updateActiveState();
+};
+
+/// Small helper class that provides ownership for shared_ptr's of child elements
+class CAdventureMapContainerWidget : public CIntObject
+{
+	friend class AdventureMapWidget;
+	std::vector<std::shared_ptr<CIntObject>> ownedChildren;
+	std::string disableCondition;
+};
+
+class CAdventureMapOverlayWidget : public CAdventureMapContainerWidget
+{
+public:
+	void show(SDL_Surface * to) override;
+};
+
+/// Small helper class that provides player-colorable icon using animation file
+class CAdventureMapIcon : public CIntObject
+{
+	std::shared_ptr<CAnimImage> image;
+
+	size_t index;
+	size_t iconsPerPlayer;
+public:
+	CAdventureMapIcon(const Point & position, std::shared_ptr<CAnimation> image, size_t index, size_t iconsPerPlayer);
+
+	void setPlayer(const PlayerColor & player);
+};

+ 8 - 7
client/adventureMap/CAdventureOptions.cpp → client/adventureMap/AdventureOptions.cpp

@@ -9,7 +9,7 @@
  */
 
 #include "StdInc.h"
-#include "CAdventureOptions.h"
+#include "AdventureOptions.h"
 
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
@@ -17,13 +17,14 @@
 #include "../lobby/CCampaignInfoScreen.h"
 #include "../lobby/CScenarioInfoScreen.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/WindowHandler.h"
 #include "../gui/Shortcut.h"
 #include "../widgets/Buttons.h"
 
 #include "../../CCallback.h"
 #include "../../lib/StartInfo.h"
 
-CAdventureOptions::CAdventureOptions()
+AdventureOptions::AdventureOptions()
 	: CWindowObject(PLAYER_COLORED, "ADVOPTS")
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
@@ -31,10 +32,10 @@ CAdventureOptions::CAdventureOptions()
 	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), EShortcut::GLOBAL_RETURN);
+	exit = std::make_shared<CButton>(Point(204, 313), "IOK6432.DEF", CButton::tooltip(), std::bind(&AdventureOptions::close, this), EShortcut::GLOBAL_RETURN);
 
 	scenInfo = std::make_shared<CButton>(Point(24, 198), "ADVINFO.DEF", CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_SCENARIO);
-	scenInfo->addCallback(CAdventureOptions::showScenarioInfo);
+	scenInfo->addCallback(AdventureOptions::showScenarioInfo);
 
 	puzzle = std::make_shared<CButton>(Point(24, 81), "ADVPUZ.DEF", CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_PUZZLE);
 	puzzle->addCallback(std::bind(&CPlayerInterface::showPuzzleMap, LOCPLINT));
@@ -46,15 +47,15 @@ CAdventureOptions::CAdventureOptions()
 		dig->block(true);
 }
 
-void CAdventureOptions::showScenarioInfo()
+void AdventureOptions::showScenarioInfo()
 {
 	if(LOCPLINT->cb->getStartInfo()->campState)
 	{
-		GH.pushIntT<CCampaignInfoScreen>();
+		GH.windows().createAndPushWindow<CCampaignInfoScreen>();
 	}
 	else
 	{
-		GH.pushIntT<CScenarioInfoScreen>();
+		GH.windows().createAndPushWindow<CScenarioInfoScreen>();
 	}
 }
 

+ 2 - 2
client/adventureMap/CAdventureOptions.h → client/adventureMap/AdventureOptions.h

@@ -14,7 +14,7 @@
 class CButton;
 
 /// Adventure options dialog where you can view the world, dig, play the replay of the last turn,...
-class CAdventureOptions : public CWindowObject
+class AdventureOptions : public CWindowObject
 {
 	std::shared_ptr<CButton> exit;
 	std::shared_ptr<CButton> viewWorld;
@@ -24,7 +24,7 @@ class CAdventureOptions : public CWindowObject
 	/*std::shared_ptr<CButton> replay*/
 
 public:
-	CAdventureOptions();
+	AdventureOptions();
 
 	static void showScenarioInfo();
 };

+ 20 - 0
client/adventureMap/AdventureState.h

@@ -0,0 +1,20 @@
+/*
+ * AdventureState.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 EAdventureState
+{
+	NOT_INITIALIZED,
+	HOTSEAT_WAIT,
+	MAKING_TURN,
+	ENEMY_TURN,
+	CASTING_SPELL,
+	WORLD_VIEW
+};

+ 0 - 94
client/adventureMap/CAdvMapPanel.cpp

@@ -1,94 +0,0 @@
-/*
- * CAdvMapPanel.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 "CAdvMapPanel.h"
-
-#include "../widgets/Buttons.h"
-#include "../widgets/Images.h"
-#include "../render/CAnimation.h"
-#include "../render/IImage.h"
-#include "../gui/CGuiHandler.h"
-
-CAdvMapPanel::CAdvMapPanel(std::shared_ptr<IImage> bg, Point position)
-	: CIntObject()
-	, background(bg)
-{
-	defActions = 255;
-	recActions = 255;
-	pos.x += position.x;
-	pos.y += position.y;
-	if (bg)
-	{
-		pos.w = bg->width();
-		pos.h = bg->height();
-	}
-}
-
-void CAdvMapPanel::addChildColorableButton(std::shared_ptr<CButton> button)
-{
-	colorableButtons.push_back(button);
-	addChildToPanel(button, ACTIVATE | DEACTIVATE);
-}
-
-void CAdvMapPanel::setPlayerColor(const PlayerColor & clr)
-{
-	for(auto & button : colorableButtons)
-	{
-		button->setPlayerColor(clr);
-	}
-}
-
-void CAdvMapPanel::showAll(SDL_Surface * to)
-{
-	if(background)
-		background->draw(to, pos.x, pos.y);
-
-	CIntObject::showAll(to);
-}
-
-void CAdvMapPanel::addChildToPanel(std::shared_ptr<CIntObject> obj, ui8 actions)
-{
-	otherObjects.push_back(obj);
-	obj->recActions |= actions | SHOWALL;
-	obj->recActions &= ~DISPOSE;
-	addChild(obj.get(), false);
-}
-
-CAdvMapWorldViewPanel::CAdvMapWorldViewPanel(std::shared_ptr<CAnimation> _icons, std::shared_ptr<IImage> bg, Point position, int spaceBottom, const PlayerColor &color)
-	: CAdvMapPanel(bg, position), icons(_icons)
-{
-	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
-	int fillerHeight = bg ? spaceBottom - pos.y - pos.h : 0;
-
-	if(fillerHeight > 0)
-	{
-		backgroundFiller = std::make_shared<CFilledTexture>("DIBOXBCK", Rect(0, pos.h, pos.w, fillerHeight));
-	}
-}
-
-void CAdvMapWorldViewPanel::recolorIcons(const PlayerColor & color, int indexOffset)
-{
-	assert(iconsData.size() == currentIcons.size());
-
-	for(size_t idx = 0; idx < iconsData.size(); idx++)
-	{
-		const auto & data = iconsData.at(idx);
-		currentIcons[idx]->setFrame(data.first + indexOffset);
-	}
-}
-
-void CAdvMapWorldViewPanel::addChildIcon(std::pair<int, Point> data, int indexOffset)
-{
-	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE);
-	iconsData.push_back(data);
-	currentIcons.push_back(std::make_shared<CAnimImage>(icons, data.first + indexOffset, 0, data.second.x, data.second.y));
-}
-

+ 0 - 60
client/adventureMap/CAdvMapPanel.h

@@ -1,60 +0,0 @@
-/*
- * CAdvMapPanel.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 "../gui/CIntObject.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-class PlayerColor;
-VCMI_LIB_NAMESPACE_END
-
-class CAnimation;
-class CAnimImage;
-class CFilledTexture;
-class CButton;
-class IImage;
-
-/// simple panel that contains other displayable elements; used to separate groups of controls
-class CAdvMapPanel : public CIntObject
-{
-	std::vector<std::shared_ptr<CButton>> colorableButtons;
-	std::vector<std::shared_ptr<CIntObject>> otherObjects;
-	/// the surface passed to this obj will be freed in dtor
-	std::shared_ptr<IImage> background;
-public:
-	CAdvMapPanel(std::shared_ptr<IImage> bg, Point position);
-
-	void addChildToPanel(std::shared_ptr<CIntObject> obj, ui8 actions = 0);
-	void addChildColorableButton(std::shared_ptr<CButton> button);
-	/// recolors all buttons to given player color
-	void setPlayerColor(const PlayerColor & clr);
-
-	void showAll(SDL_Surface * to) override;
-};
-
-/// specialized version of CAdvMapPanel that handles recolorable def-based pictures for world view info panel
-class CAdvMapWorldViewPanel : public CAdvMapPanel
-{
-	/// data that allows reconstruction of panel info icons
-	std::vector<std::pair<int, Point>> iconsData;
-	/// ptrs to child-pictures constructed from iconsData
-	std::vector<std::shared_ptr<CAnimImage>> currentIcons;
-	/// surface drawn below world view panel on higher resolutions (won't be needed when world view panel is configured for extraResolutions mod)
-	std::shared_ptr<CFilledTexture> backgroundFiller;
-	std::shared_ptr<CAnimation> icons;
-public:
-	CAdvMapWorldViewPanel(std::shared_ptr<CAnimation> _icons, std::shared_ptr<IImage> bg, Point position, int spaceBottom, const PlayerColor &color);
-
-	void addChildIcon(std::pair<int, Point> data, int indexOffset);
-
-	/// recreates all pictures from given def to recolor them according to current player color
-	void recolorIcons(const PlayerColor & color, int indexOffset);
-};
-

+ 0 - 1346
client/adventureMap/CAdventureMapInterface.cpp

@@ -1,1346 +0,0 @@
-/*
- * CAdvMapInt.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 "CAdventureMapInterface.h"
-
-#include "CAdvMapPanel.h"
-#include "CAdventureOptions.h"
-#include "CInGameConsole.h"
-#include "CMinimap.h"
-#include "CResDataBar.h"
-#include "CList.h"
-#include "CInfoBar.h"
-#include "MapAudioPlayer.h"
-
-#include "../mapView/mapHandler.h"
-#include "../mapView/MapView.h"
-#include "../windows/CKingdomInterface.h"
-#include "../windows/CSpellWindow.h"
-#include "../windows/CTradeWindow.h"
-#include "../windows/GUIClasses.h"
-#include "../windows/InfoWindows.h"
-#include "../CGameInfo.h"
-#include "../CPlayerInterface.h"
-#include "../lobby/CSavingScreen.h"
-#include "../render/CAnimation.h"
-#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"
-#include "../CMT.h"
-#include "../PlayerLocalState.h"
-
-#include "../../CCallback.h"
-#include "../../lib/CConfigHandler.h"
-#include "../../lib/CGeneralTextHandler.h"
-#include "../../lib/spells/CSpellHandler.h"
-#include "../../lib/mapObjects/CGHeroInstance.h"
-#include "../../lib/mapObjects/CGTownInstance.h"
-#include "../../lib/CPathfinder.h"
-#include "../../lib/mapping/CMap.h"
-
-#define ADVOPT (conf.go()->ac)
-
-std::shared_ptr<CAdventureMapInterface> adventureInt;
-
-CAdventureMapInterface::CAdventureMapInterface():
-	minimap(new CMinimap(Rect(ADVOPT.minimapX, ADVOPT.minimapY, ADVOPT.minimapW, ADVOPT.minimapH))),
-	statusbar(CGStatusBar::create(ADVOPT.statusbarX,ADVOPT.statusbarY,ADVOPT.statusbarG)),
-	heroList(new CHeroList(ADVOPT.hlistSize, Point(ADVOPT.hlistX, ADVOPT.hlistY), ADVOPT.hlistAU, ADVOPT.hlistAD)),
-	townList(new CTownList(ADVOPT.tlistSize, Point(ADVOPT.tlistX, ADVOPT.tlistY), ADVOPT.tlistAU, ADVOPT.tlistAD)),
-	infoBar(new CInfoBar(Point(ADVOPT.infoboxX, ADVOPT.infoboxY))),
-	resdatabar(new CResDataBar),
-	mapAudio(new MapAudioPlayer()),
-	terrain(new MapView(Point(ADVOPT.advmapX, ADVOPT.advmapY), Point(ADVOPT.advmapW, ADVOPT.advmapH))),
-	state(EGameState::NOT_INITIALIZED),
-	spellBeingCasted(nullptr),
-	activeMapPanel(nullptr),
-	scrollingCursorSet(false)
-{
-	pos.x = pos.y = 0;
-	pos.w = GH.screenDimensions().x;
-	pos.h = GH.screenDimensions().y;
-	strongInterest = true; // handle all mouse move events to prevent dead mouse move space in fullscreen mode
-
-	bg = IImage::createFromFile(ADVOPT.mainGraphic);
-	if(!ADVOPT.worldViewGraphic.empty())
-	{
-		bgWorldView = IImage::createFromFile(ADVOPT.worldViewGraphic);
-	}
-	else
-	{
-		bgWorldView = nullptr;
-		logGlobal->warn("ADVOPT.worldViewGraphic is empty => bitmap not loaded");
-	}
-	if (!bgWorldView)
-	{
-		logGlobal->warn("bgWorldView not defined in resolution config; fallback to VWorld.bmp");
-		bgWorldView = IImage::createFromFile("VWorld.bmp");
-	}
-
-	worldViewIcons = std::make_shared<CAnimation>("VwSymbol");//todo: customize with ADVOPT
-	worldViewIcons->preload();
-
-	for(int g = 0; g < ADVOPT.gemG.size(); ++g)
-	{
-		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, 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)
-			button->addImage(image);
-		return button;
-	};
-
-	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;
-
-	panelMain = std::make_shared<CAdvMapPanel>(nullptr, Point(0, 0));
-	// TODO correct drawing position
-	panelWorldView = std::make_shared<CAdvMapWorldViewPanel>(worldViewIcons, bgWorldView, Point(heroList->pos.x - 2, 195), panelSpaceBottom, LOCPLINT->playerID);
-
-	panelMain->addChildColorableButton(kingOverview);
-	panelMain->addChildColorableButton(underground);
-	panelMain->addChildColorableButton(questlog);
-	panelMain->addChildColorableButton(sleepWake);
-	panelMain->addChildColorableButton(moveHero);
-	panelMain->addChildColorableButton(spellbook);
-	panelMain->addChildColorableButton(advOptions);
-	panelMain->addChildColorableButton(sysOptions);
-	panelMain->addChildColorableButton(nextHero);
-	panelMain->addChildColorableButton(endTurn);
-
-
-	// TODO move configs to resolutions.json, similarly to previous buttons
-	config::ButtonInfo worldViewBackConfig = config::ButtonInfo();
-	worldViewBackConfig.defName = "IOK6432.DEF";
-	worldViewBackConfig.x = GH.screenDimensions().x - 73;
-	worldViewBackConfig.y = 343 + 195;
-	worldViewBackConfig.playerColoured = false;
-	panelWorldView->addChildToPanel(
-		makeButton(288, std::bind(&CAdventureMapInterface::fworldViewBack,this), worldViewBackConfig, EShortcut::GLOBAL_CANCEL), ACTIVATE | DEACTIVATE);
-
-	config::ButtonInfo worldViewPuzzleConfig = config::ButtonInfo();
-	worldViewPuzzleConfig.defName = "VWPUZ.DEF";
-	worldViewPuzzleConfig.x = GH.screenDimensions().x - 188;
-	worldViewPuzzleConfig.y = 343 + 195;
-	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), EShortcut::ADVENTURE_VIEW_PUZZLE, worldViewPuzzleConfig.playerColoured), ACTIVATE | DEACTIVATE);
-
-	config::ButtonInfo worldViewScale1xConfig = config::ButtonInfo();
-	worldViewScale1xConfig.defName = "VWMAG1.DEF";
-	worldViewScale1xConfig.x = GH.screenDimensions().x - 191;
-	worldViewScale1xConfig.y = 23 + 195;
-	worldViewScale1xConfig.playerColoured = false;
-	panelWorldView->addChildToPanel( // help text is wrong for this button
-		makeButton(291, std::bind(&CAdventureMapInterface::fworldViewScale1x,this), worldViewScale1xConfig, EShortcut::SELECT_INDEX_1), ACTIVATE | DEACTIVATE);
-
-	config::ButtonInfo worldViewScale2xConfig = config::ButtonInfo();
-	worldViewScale2xConfig.defName = "VWMAG2.DEF";
-	worldViewScale2xConfig.x = GH.screenDimensions().x- 191 + 63;
-	worldViewScale2xConfig.y = 23 + 195;
-	worldViewScale2xConfig.playerColoured = false;
-	panelWorldView->addChildToPanel( // help text is wrong for this button
-		makeButton(291, std::bind(&CAdventureMapInterface::fworldViewScale2x,this), worldViewScale2xConfig, EShortcut::SELECT_INDEX_2), ACTIVATE | DEACTIVATE);
-
-	config::ButtonInfo worldViewScale4xConfig = config::ButtonInfo();
-	worldViewScale4xConfig.defName = "VWMAG4.DEF";
-	worldViewScale4xConfig.x = GH.screenDimensions().x- 191 + 126;
-	worldViewScale4xConfig.y = 23 + 195;
-	worldViewScale4xConfig.playerColoured = false;
-	panelWorldView->addChildToPanel( // help text is wrong for this button
-		makeButton(291, std::bind(&CAdventureMapInterface::fworldViewScale4x,this), worldViewScale4xConfig, EShortcut::SELECT_INDEX_4), ACTIVATE | DEACTIVATE);
-
-	config::ButtonInfo worldViewUndergroundConfig = config::ButtonInfo();
-	worldViewUndergroundConfig.defName = "IAM010.DEF";
-	worldViewUndergroundConfig.additionalDefs.push_back("IAM003.DEF");
-	worldViewUndergroundConfig.x = GH.screenDimensions().x - 115;
-	worldViewUndergroundConfig.y = 343 + 195;
-	worldViewUndergroundConfig.playerColoured = true;
-	worldViewUnderground = makeButton(294, std::bind(&CAdventureMapInterface::fswitchLevel,this), worldViewUndergroundConfig, EShortcut::ADVENTURE_TOGGLE_MAP_LEVEL);
-	panelWorldView->addChildColorableButton(worldViewUnderground);
-
-	onCurrentPlayerChanged(LOCPLINT->playerID);
-
-	int iconColorMultiplier = currentPlayerID.getNum() * 19;
-	int wvLeft = heroList->pos.x - 2; // TODO correct drawing position
-	//int wvTop = 195;
-	for (int i = 0; i < 5; ++i)
-	{
-		panelWorldView->addChildIcon(std::pair<int, Point>(i, Point(5, 58 + i * 20)), iconColorMultiplier);
-		panelWorldView->addChildToPanel(std::make_shared<CLabel>(wvLeft + 45, 263 + i * 20, EFonts::FONT_SMALL, ETextAlignment::TOPLEFT,
-												Colors::WHITE, CGI->generaltexth->allTexts[612 + i]));
-	}
-	for (int i = 0; i < 7; ++i)
-	{
-		panelWorldView->addChildIcon(std::pair<int, Point>(i +  5, Point(5, 182 + i * 20)), iconColorMultiplier);
-		panelWorldView->addChildIcon(std::pair<int, Point>(i + 12, Point(160, 182 + i * 20)), iconColorMultiplier);
-		panelWorldView->addChildToPanel(std::make_shared<CLabel>(wvLeft + 45, 387 + i * 20, EFonts::FONT_SMALL, ETextAlignment::TOPLEFT,
-												Colors::WHITE, CGI->generaltexth->allTexts[619 + i]));
-	}
-	panelWorldView->addChildToPanel(std::make_shared<CLabel>(wvLeft +   5, 367, EFonts::FONT_SMALL, ETextAlignment::TOPLEFT,
-											Colors::WHITE, CGI->generaltexth->allTexts[617]));
-	panelWorldView->addChildToPanel(std::make_shared<CLabel>(wvLeft + 45, 367, EFonts::FONT_SMALL, ETextAlignment::TOPLEFT,
-											Colors::WHITE, CGI->generaltexth->allTexts[618]));
-
-	activeMapPanel = panelMain;
-
-	exitWorldView();
-
-	underground->block(!CGI->mh->getMap()->twoLevel);
-	questlog->block(!CGI->mh->getMap()->quests.size());
-	worldViewUnderground->block(!CGI->mh->getMap()->twoLevel);
-}
-
-void CAdventureMapInterface::fshowOverview()
-{
-	GH.pushIntT<CKingdomInterface>();
-}
-
-void CAdventureMapInterface::fworldViewBack()
-{
-	exitWorldView();
-
-	auto hero = LOCPLINT->localState->getCurrentHero();
-	if (hero)
-		centerOnObject(hero);
-}
-
-void CAdventureMapInterface::fworldViewScale1x()
-{
-	// TODO set corresponding scale button to "selected" mode
-	openWorldView(7);
-}
-
-void CAdventureMapInterface::fworldViewScale2x()
-{
-	openWorldView(11);
-}
-
-void CAdventureMapInterface::fworldViewScale4x()
-{
-	openWorldView(16);
-}
-
-void CAdventureMapInterface::fswitchLevel()
-{
-	// with support for future multi-level maps :)
-	int maxLevels = CGI->mh->getMap()->levels();
-	if (maxLevels < 2)
-		return;
-
-	terrain->onMapLevelSwitched();
-}
-
-void CAdventureMapInterface::onMapViewMoved(const Rect & visibleArea, int mapLevel)
-{
-	underground->setIndex(mapLevel, true);
-	underground->redraw();
-
-	worldViewUnderground->setIndex(mapLevel, true);
-	worldViewUnderground->redraw();
-
-	minimap->onMapViewMoved(visibleArea, mapLevel);
-}
-
-void CAdventureMapInterface::onAudioResumed()
-{
-	mapAudio->onAudioResumed();
-}
-
-void CAdventureMapInterface::onAudioPaused()
-{
-	mapAudio->onAudioPaused();
-}
-
-void CAdventureMapInterface::fshowQuestlog()
-{
-	LOCPLINT->showQuestLog();
-}
-
-void CAdventureMapInterface::fsleepWake()
-{
-	const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero();
-	if (!h)
-		return;
-	bool newSleep = !LOCPLINT->localState->isHeroSleeping(h);
-
-	if (newSleep)
-		LOCPLINT->localState->setHeroAsleep(h);
-	else
-		LOCPLINT->localState->setHeroAwaken(h);
-
-	onHeroChanged(h);
-
-	if (newSleep)
-		fnextHero();
-
-	// redraw to update the image of sleep/wake button
-	panelMain->redraw();
-}
-
-void CAdventureMapInterface::fmoveHero()
-{
-	const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero();
-	if (!h || !LOCPLINT->localState->hasPath(h) || CGI->mh->hasOngoingAnimations())
-		return;
-
-	LOCPLINT->moveHero(h, LOCPLINT->localState->getPath(h));
-}
-
-void CAdventureMapInterface::fshowSpellbok()
-{
-	if (!LOCPLINT->localState->getCurrentHero()) //checking necessary values
-		return;
-
-	centerOnObject(LOCPLINT->localState->getCurrentHero());
-
-	GH.pushIntT<CSpellWindow>(LOCPLINT->localState->getCurrentHero(), LOCPLINT, false);
-}
-
-void CAdventureMapInterface::fadventureOPtions()
-{
-	GH.pushIntT<CAdventureOptions>();
-}
-
-void CAdventureMapInterface::fsystemOptions()
-{
-	GH.pushIntT<SettingsMainWindow>();
-}
-
-void CAdventureMapInterface::fnextHero()
-{
-	const auto * currHero = LOCPLINT->localState->getCurrentHero();
-	const auto * nextHero = LOCPLINT->localState->getNextWanderingHero(currHero);
-
-	if (nextHero)
-	{
-		LOCPLINT->localState->setSelection(nextHero);
-		centerOnObject(nextHero);
-	}
-}
-
-void CAdventureMapInterface::fendTurn()
-{
-	if(!LOCPLINT->makingTurn)
-		return;
-
-	if(settings["adventure"]["heroReminder"].Bool())
-	{
-		for(auto hero : LOCPLINT->localState->getWanderingHeroes())
-		{
-			if(!LOCPLINT->localState->isHeroSleeping(hero) && hero->movement > 0)
-			{
-				// Only show hero reminder if conditions met:
-				// - There still movement points
-				// - Hero don't have a path or there not points for first step on path
-				LOCPLINT->localState->verifyPath(hero);
-
-				if(!LOCPLINT->localState->hasPath(hero))
-				{
-					LOCPLINT->showYesNoDialog( CGI->generaltexth->allTexts[55], std::bind(&CAdventureMapInterface::endingTurn, this), nullptr );
-					return;
-				}
-
-				auto path = LOCPLINT->localState->getPath(hero);
-				if (path.nodes.size() < 2 || path.nodes[path.nodes.size() - 2].turns)
-				{
-					LOCPLINT->showYesNoDialog( CGI->generaltexth->allTexts[55], std::bind(&CAdventureMapInterface::endingTurn, this), nullptr );
-					return;
-				}
-			}
-		}
-	}
-	endingTurn();
-}
-
-void CAdventureMapInterface::updateButtons()
-{
-	const auto * hero = LOCPLINT->localState->getCurrentHero();
-
-	sleepWake->block(!hero);
-	spellbook->block(!hero);
-	moveHero->block(!hero || !LOCPLINT->localState->hasPath(hero) || hero->movement == 0);
-
-	const auto * nextSuitableHero = LOCPLINT->localState->getNextWanderingHero(hero);
-	nextHero->block(nextSuitableHero == nullptr);
-
-	if(hero)
-	{
-		bool state = LOCPLINT->localState->isHeroSleeping(hero);
-		sleepWake->setIndex(state ? 1 : 0, true);
-		sleepWake->redraw();
-	}
-}
-
-void CAdventureMapInterface::onHeroMovementStarted(const CGHeroInstance * hero)
-{
-	infoBar->popAll();
-	infoBar->showSelection();
-}
-
-void CAdventureMapInterface::onHeroChanged(const CGHeroInstance *h)
-{
-	heroList->update(h);
-
-	if (h && h == LOCPLINT->localState->getCurrentHero() && !infoBar->showingComponents())
-		infoBar->showSelection();
-
-	updateButtons();
-}
-
-void CAdventureMapInterface::onTownChanged(const CGTownInstance * town)
-{
-	townList->update(town);
-
-	if (town && town == LOCPLINT->localState->getCurrentTown() && !infoBar->showingComponents())
-		infoBar->showSelection();
-}
-
-void CAdventureMapInterface::showInfoBoxMessage(const std::vector<Component> & components, std::string message, int timer)
-{
-	infoBar->pushComponents(components, message, timer);
-}
-
-void CAdventureMapInterface::activate()
-{
-	CIntObject::activate();
-	if (!(active & KEYBOARD))
-		CIntObject::activate(KEYBOARD);
-
-	screenBuf = screen;
-	GH.statusbar = statusbar;
-	
-	if(LOCPLINT)
-	{
-		LOCPLINT->cingconsole->activate();
-		LOCPLINT->cingconsole->pos = this->pos;
-	}
-
-	if(state != EGameState::ENEMY_TURN && state != EGameState::HOTSEAT_WAIT)
-	{
-		assert(state == EGameState::MAKING_TURN);
-
-		activeMapPanel->activate();
-		if (state == EGameState::MAKING_TURN)
-		{
-			heroList->activate();
-			townList->activate();
-			infoBar->activate();
-		}
-		minimap->activate();
-		terrain->activate();
-		statusbar->activate();
-
-		GH.fakeMouseMove(); //to restore the cursor
-	}
-}
-
-void CAdventureMapInterface::deactivate()
-{
-	CIntObject::deactivate();
-
-	if(state != EGameState::ENEMY_TURN && state != EGameState::HOTSEAT_WAIT)
-	{
-		assert(state == EGameState::MAKING_TURN);
-
-		CCS->curh->set(Cursor::Map::POINTER);
-		activeMapPanel->deactivate();
-		if (state == EGameState::MAKING_TURN)
-		{
-			heroList->deactivate();
-			townList->deactivate();
-			infoBar->deactivate();
-		}
-		minimap->deactivate();
-		terrain->deactivate();
-		statusbar->deactivate();
-	}
-}
-
-void CAdventureMapInterface::showAll(SDL_Surface * to)
-{
-	bg->draw(to, 0, 0);
-
-//	if(state != EGameState::MAKING_TURN)
-//		return;
-
-	heroList->showAll(to);
-	townList->showAll(to);
-	infoBar->showAll(to);
-
-	activeMapPanel->showAll(to);
-
-	minimap->showAll(to);
-	terrain->showAll(to);
-	show(to);
-
-	resdatabar->showAll(to);
-	statusbar->show(to);
-	LOCPLINT->cingconsole->show(to);
-}
-
-void CAdventureMapInterface::show(SDL_Surface * to)
-{
-//	if(state != EGameState::MAKING_TURN)
-//		return;
-
-	handleMapScrollingUpdate();
-
-	for(int i = 0; i < 4; i++)
-	{
-		if(settings["session"]["spectate"].Bool())
-			gems[i]->setFrame(PlayerColor(1).getNum());
-		else
-			gems[i]->setFrame(LOCPLINT->playerID.getNum());
-	}
-
-	minimap->show(to);
-	terrain->show(to);
-
-	for(int i = 0; i < 4; i++)
-		gems[i]->showAll(to);
-
-	LOCPLINT->cingconsole->show(to);
-
-	infoBar->show(to);
-	statusbar->showAll(to);
-}
-
-void CAdventureMapInterface::handleMapScrollingUpdate()
-{
-	/// Width of window border, in pixels, that triggers map scrolling
-	static constexpr uint32_t borderScrollWidth = 15;
-
-	uint32_t timePassed = GH.mainFPSmng->getElapsedMilliseconds();
-	uint32_t scrollSpeedPixels = settings["adventure"]["scrollSpeedPixels"].Float();
-	uint32_t scrollDistance = scrollSpeedPixels * timePassed / 1000;
-
-	bool scrollingActive = !GH.isKeyboardCtrlDown() && isActive() && state == EGameState::MAKING_TURN;
-
-	Point cursorPosition = GH.getCursorPosition();
-	Point scrollDirection;
-
-	if (cursorPosition.x < borderScrollWidth)
-		scrollDirection.x = -1;
-
-	if (cursorPosition.x > GH.screenDimensions().x - borderScrollWidth)
-		scrollDirection.x = +1;
-
-	if (cursorPosition.y < borderScrollWidth)
-		scrollDirection.y = -1;
-
-	if (cursorPosition.y > GH.screenDimensions().y - borderScrollWidth)
-		scrollDirection.y = +1;
-
-	Point scrollDelta = scrollDirection * scrollDistance;
-
-	if (scrollingActive && scrollDelta != Point(0,0))
-		terrain->onMapScrolled(scrollDelta);
-
-	if (scrollDelta == Point(0,0) && !scrollingCursorSet)
-		return;
-
-	if(scrollDelta.x > 0)
-	{
-		if(scrollDelta.y < 0)
-			CCS->curh->set(Cursor::Map::SCROLL_NORTHEAST);
-		if(scrollDelta.y > 0)
-			CCS->curh->set(Cursor::Map::SCROLL_SOUTHEAST);
-		if(scrollDelta.y == 0)
-			CCS->curh->set(Cursor::Map::SCROLL_EAST);
-	}
-	if(scrollDelta.x < 0)
-	{
-		if(scrollDelta.y < 0)
-			CCS->curh->set(Cursor::Map::SCROLL_NORTHWEST);
-		if(scrollDelta.y > 0)
-			CCS->curh->set(Cursor::Map::SCROLL_SOUTHWEST);
-		if(scrollDelta.y == 0)
-			CCS->curh->set(Cursor::Map::SCROLL_WEST);
-	}
-
-	if (scrollDelta.x == 0)
-	{
-		if(scrollDelta.y < 0)
-			CCS->curh->set(Cursor::Map::SCROLL_NORTH);
-		if(scrollDelta.y > 0)
-			CCS->curh->set(Cursor::Map::SCROLL_SOUTH);
-		if(scrollDelta.y == 0)
-			CCS->curh->set(Cursor::Map::POINTER);
-	}
-
-	scrollingCursorSet = scrollDelta != Point(0,0);
-}
-
-void CAdventureMapInterface::centerOnTile(int3 on)
-{
-	terrain->onCenteredTile(on);
-}
-
-void CAdventureMapInterface::centerOnObject(const CGObjectInstance * obj)
-{
-	terrain->onCenteredObject(obj);
-}
-
-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 EShortcut::ADVENTURE_THIEVES_GUILD:
-		if(GH.topInt()->type & BLOCK_ADV_HOTKEYS)
-			return;
-
-		{
-			//find first town with tavern
-			auto itr = range::find_if(LOCPLINT->localState->getOwnedTowns(), [](const CGTownInstance * town)
-			{
-				return town->hasBuilt(BuildingID::TAVERN);
-			});
-
-			if(itr != LOCPLINT->localState->getOwnedTowns().end())
-				LOCPLINT->showThievesGuildWindow(*itr);
-			else
-				LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithTavern"));
-		}
-		return;
-	case EShortcut::ADVENTURE_VIEW_SCENARIO:
-		if(isActive())
-			CAdventureOptions::showScenarioInfo();
-		return;
-	case EShortcut::GAME_SAVE_GAME:
-		if(isActive())
-			GH.pushIntT<CSavingScreen>();
-		return;
-	case EShortcut::GAME_LOAD_GAME:
-		if(isActive())
-			LOCPLINT->proposeLoadingGame();
-		return;
-	case EShortcut::ADVENTURE_DIG_GRAIL:
-		{
-			if(h && isActive() && LOCPLINT->makingTurn)
-				LOCPLINT->tryDiggging(h);
-			return;
-		}
-	case EShortcut::ADVENTURE_VIEW_PUZZLE:
-		if(isActive())
-			LOCPLINT->showPuzzleMap();
-		return;
-	case EShortcut::ADVENTURE_VIEW_WORLD:
-		if(isActive())
-			LOCPLINT->viewWorldMap();
-		return;
-	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 EShortcut::ADVENTURE_VISIT_OBJECT: //space - try to revisit current object with selected hero
-		{
-			if(!isActive())
-				return;
-			if(h)
-			{
-				LOCPLINT->cb->moveHero(h,h->pos);
-			}
-		}
-		return;
-	case EShortcut::ADVENTURE_VIEW_SELECTED:
-		{
-			if(!isActive() || !LOCPLINT->localState->getCurrentArmy())
-				return;
-			if(h)
-				LOCPLINT->openHeroWindow(h);
-			else if(t)
-				LOCPLINT->openTownWindow(t);
-			return;
-		}
-	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)
-				return;
-
-			abortCastingMode();
-			return;
-		}
-	case EShortcut::GAME_OPEN_MARKETPLACE:
-		{
-			//act on key down if marketplace windows is not already opened
-			if(GH.topInt()->type & BLOCK_ADV_HOTKEYS)
-				return;
-
-			if(GH.isKeyboardCtrlDown()) //CTRL + T => open marketplace
-			{
-				//check if we have any marketplace
-				const CGTownInstance *townWithMarket = nullptr;
-				for(const CGTownInstance *t : LOCPLINT->cb->getTownsInfo())
-				{
-					if(t->hasBuilt(BuildingID::MARKETPLACE))
-					{
-						townWithMarket = t;
-						break;
-					}
-				}
-
-				if(townWithMarket) //if any town has marketplace, open window
-					GH.pushIntT<CMarketplaceWindow>(townWithMarket);
-				else //if not - complain
-					LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket"));
-			}
-	case EShortcut::ADVENTURE_NEXT_TOWN:
-			if(isActive() && !GH.isKeyboardCtrlDown()) //no ctrl, advmapint is on the top => switch to town
-			{
-				townList->selectNext();
-			}
-			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});
-	}
-}
-
-void CAdventureMapInterface::hotkeyMoveHeroDirectional(Point direction)
-{
-	const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero(); //selected hero
-
-	if(!h || !isActive())
-		return;
-
-	if (CGI->mh->hasOngoingAnimations())
-		return;
-
-	if(direction == Point(0,0))
-	{
-		centerOnObject(h);
-		return;
-	}
-
-	int3 dst = h->visitablePos() + int3(direction.x, direction.y, 0);
-
-	if (!CGI->mh->isInMap((dst)))
-		return;
-
-	if ( !LOCPLINT->localState->setPath(h, dst))
-		return;
-
-	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)
-{
-	assert(sel);
-
-	infoBar->popAll();
-	mapAudio->onSelectionChanged(sel);
-	bool centerView = !settings["session"]["autoSkip"].Bool();
-
-	if (centerView)
-		centerOnObject(sel);
-
-	if(sel->ID==Obj::TOWN)
-	{
-		auto town = dynamic_cast<const CGTownInstance*>(sel);
-
-		infoBar->showTownSelection(town);
-		townList->select(town);
-		heroList->select(nullptr);
-		onHeroChanged(nullptr);
-	}
-	else //hero selected
-	{
-		auto hero = dynamic_cast<const CGHeroInstance*>(sel);
-
-		infoBar->showHeroSelection(hero);
-		heroList->select(hero);
-		townList->select(nullptr);
-
-		LOCPLINT->localState->verifyPath(hero);
-		onHeroChanged(hero);
-	}
-	updateButtons();
-	townList->redraw();
-	heroList->redraw();
-}
-
-bool CAdventureMapInterface::isActive()
-{
-	return active & ~CIntObject::KEYBOARD;
-}
-
-void CAdventureMapInterface::onMapTilesChanged(boost::optional<std::unordered_set<int3>> positions)
-{
-	if (positions)
-		minimap->updateTiles(*positions);
-	else
-		minimap->update();
-}
-
-void CAdventureMapInterface::onHotseatWaitStarted(PlayerColor playerID)
-{
-	onCurrentPlayerChanged(playerID);
-	state = EGameState::HOTSEAT_WAIT;
-}
-
-void CAdventureMapInterface::onEnemyTurnStarted(PlayerColor playerID)
-{
-	if(settings["session"]["spectate"].Bool())
-		return;
-
-	adjustActiveness(true);
-	mapAudio->onEnemyTurnStarted();
-	minimap->setAIRadar(true);
-	infoBar->startEnemyTurn(LOCPLINT->cb->getCurrentPlayer());
-	minimap->showAll(screen);//force refresh on inactive object
-	infoBar->showAll(screen);//force refresh on inactive object
-}
-
-void CAdventureMapInterface::adjustActiveness(bool aiTurnStart)
-{
-	bool wasActive = isActive();
-
-	if(wasActive)
-		deactivate();
-
-	if (aiTurnStart)
-		state = EGameState::ENEMY_TURN;
-	else
-		state = EGameState::MAKING_TURN;
-
-	if(wasActive)
-		activate();
-}
-
-void CAdventureMapInterface::onCurrentPlayerChanged(PlayerColor playerID)
-{
-	LOCPLINT->localState->setSelection(nullptr);
-
-	if (playerID == currentPlayerID)
-		return;
-
-	currentPlayerID = playerID;
-	bg->playerColored(currentPlayerID);
-
-	panelMain->setPlayerColor(currentPlayerID);
-	panelWorldView->setPlayerColor(currentPlayerID);
-	panelWorldView->recolorIcons(currentPlayerID, currentPlayerID.getNum() * 19);
-	resdatabar->colorize(currentPlayerID);
-}
-
-void CAdventureMapInterface::onPlayerTurnStarted(PlayerColor playerID)
-{
-	onCurrentPlayerChanged(playerID);
-
-	state = EGameState::MAKING_TURN;
-	if(LOCPLINT->cb->getCurrentPlayer() == LOCPLINT->playerID
-		|| settings["session"]["spectate"].Bool())
-	{
-		adjustActiveness(false);
-		minimap->setAIRadar(false);
-		infoBar->showSelection();
-	}
-
-	heroList->update();
-	townList->update();
-
-	const CGHeroInstance * heroToSelect = nullptr;
-
-	// find first non-sleeping hero
-	for (auto hero : LOCPLINT->localState->getWanderingHeroes())
-	{
-		if (!LOCPLINT->localState->isHeroSleeping(hero))
-		{
-			heroToSelect = hero;
-			break;
-		}
-	}
-
-	//select first hero if available.
-	if (heroToSelect != nullptr)
-	{
-		LOCPLINT->localState->setSelection(heroToSelect);
-	}
-	else if (LOCPLINT->localState->getOwnedTowns().size())
-	{
-		LOCPLINT->localState->setSelection(LOCPLINT->localState->getOwnedTown(0));
-	}
-	else
-	{
-		LOCPLINT->localState->setSelection(LOCPLINT->localState->getWanderingHero(0));
-	}
-
-	//show new day animation and sound on infobar
-	infoBar->showDate();
-
-	onHeroChanged(nullptr);
-	showAll(screen);
-	mapAudio->onPlayerTurnStarted();
-
-	if(settings["session"]["autoSkip"].Bool() && !GH.isKeyboardShiftDown())
-	{
-		if(CInfoWindow *iw = dynamic_cast<CInfoWindow *>(GH.topInt().get()))
-			iw->close();
-
-		endingTurn();
-	}
-}
-
-void CAdventureMapInterface::endingTurn()
-{
-	if(settings["session"]["spectate"].Bool())
-		return;
-
-	LOCPLINT->makingTurn = false;
-	LOCPLINT->cb->endTurn();
-	mapAudio->onPlayerTurnEnded();
-}
-
-const CGObjectInstance* CAdventureMapInterface::getActiveObject(const int3 &mapPos)
-{
-	std::vector < const CGObjectInstance * > bobjs = LOCPLINT->cb->getBlockingObjs(mapPos);  //blocking objects at tile
-
-	if (bobjs.empty())
-		return nullptr;
-
-	return *boost::range::max_element(bobjs, &CMapHandler::compareObjectBlitOrder);
-/*
-	if (bobjs.back()->ID == Obj::HERO)
-		return bobjs.back();
-	else
-		return bobjs.front();*/
-}
-
-void CAdventureMapInterface::onTileLeftClicked(const int3 &mapPos)
-{
-	if(state != EGameState::MAKING_TURN)
-		return;
-
-	//FIXME: this line breaks H3 behavior for Dimension Door
-	if(!LOCPLINT->cb->isVisible(mapPos))
-		return;
-	if(!LOCPLINT->makingTurn)
-		return;
-
-	const TerrainTile *tile = LOCPLINT->cb->getTile(mapPos);
-
-	const CGObjectInstance *topBlocking = getActiveObject(mapPos);
-
-	int3 selPos = LOCPLINT->localState->getCurrentArmy()->getSightCenter();
-	if(spellBeingCasted)
-	{
-		if (!isInScreenRange(selPos, mapPos))
-			return;
-
-		const TerrainTile *heroTile = LOCPLINT->cb->getTile(selPos);
-
-		switch(spellBeingCasted->id)
-		{
-		case SpellID::SCUTTLE_BOAT: //Scuttle Boat
-			if(topBlocking && topBlocking->ID == Obj::BOAT)
-				leaveCastingMode(mapPos);
-			break;
-		case SpellID::DIMENSION_DOOR:
-			if(!tile || tile->isClear(heroTile))
-				leaveCastingMode(mapPos);
-			break;
-		}
-		return;
-	}
-	//check if we can select this object
-	bool canSelect = topBlocking && topBlocking->ID == Obj::HERO && topBlocking->tempOwner == LOCPLINT->playerID;
-	canSelect |= topBlocking && topBlocking->ID == Obj::TOWN && LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, topBlocking->tempOwner);
-
-	bool isHero = false;
-	if(LOCPLINT->localState->getCurrentArmy()->ID != Obj::HERO) //hero is not selected (presumably town)
-	{
-		if(LOCPLINT->localState->getCurrentArmy() == topBlocking) //selected town clicked
-			LOCPLINT->openTownWindow(static_cast<const CGTownInstance*>(topBlocking));
-		else if(canSelect)
-			LOCPLINT->localState->setSelection(static_cast<const CArmedInstance*>(topBlocking));
-	}
-	else if(const CGHeroInstance * currentHero = LOCPLINT->localState->getCurrentHero()) //hero is selected
-	{
-		isHero = true;
-
-		const CGPathNode *pn = LOCPLINT->cb->getPathsInfo(currentHero)->getPathInfo(mapPos);
-		if(currentHero == topBlocking) //clicked selected hero
-		{
-			LOCPLINT->openHeroWindow(currentHero);
-			return;
-		}
-		else if(canSelect && pn->turns == 255 ) //selectable object at inaccessible tile
-		{
-			LOCPLINT->localState->setSelection(static_cast<const CArmedInstance*>(topBlocking));
-			return;
-		}
-		else //still here? we need to move hero if we clicked end of already selected path or calculate a new path otherwise
-		{
-			if(LOCPLINT->localState->hasPath(currentHero) &&
-			   LOCPLINT->localState->getPath(currentHero).endPos() == mapPos)//we'll be moving
-			{
-				if(!CGI->mh->hasOngoingAnimations())
-					LOCPLINT->moveHero(currentHero, LOCPLINT->localState->getPath(currentHero));
-				return;
-			}
-			else //remove old path and find a new one if we clicked on accessible tile
-			{
-				LOCPLINT->localState->setPath(currentHero, mapPos);
-				onHeroChanged(currentHero);
-			}
-		}
-	} //end of hero is selected "case"
-	else
-	{
-		throw std::runtime_error("Nothing is selected...");
-	}
-
-	const auto shipyard = ourInaccessibleShipyard(topBlocking);
-	if(isHero && shipyard != nullptr)
-	{
-		LOCPLINT->showShipyardDialogOrProblemPopup(shipyard);
-	}
-}
-
-void CAdventureMapInterface::onTileHovered(const int3 &mapPos)
-{
-	if(state != EGameState::MAKING_TURN)
-		return;
-
-	//may occur just at the start of game (fake move before full intiialization)
-	if(!LOCPLINT->localState->getCurrentArmy())
-		return;
-
-	if(!LOCPLINT->cb->isVisible(mapPos))
-	{
-		CCS->curh->set(Cursor::Map::POINTER);
-		statusbar->clear();
-		return;
-	}
-	auto objRelations = PlayerRelations::ALLIES;
-	const CGObjectInstance *objAtTile = getActiveObject(mapPos);
-	if(objAtTile)
-	{
-		objRelations = LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, objAtTile->tempOwner);
-		std::string text = LOCPLINT->localState->getCurrentHero() ? objAtTile->getHoverText(LOCPLINT->localState->getCurrentHero()) : objAtTile->getHoverText(LOCPLINT->playerID);
-		boost::replace_all(text,"\n"," ");
-		statusbar->write(text);
-	}
-	else
-	{
-		std::string hlp = CGI->mh->getTerrainDescr(mapPos, false);
-		statusbar->write(hlp);
-	}
-
-	if(spellBeingCasted)
-	{
-		switch(spellBeingCasted->id)
-		{
-		case SpellID::SCUTTLE_BOAT:
-			{
-			int3 hpos = LOCPLINT->localState->getCurrentArmy()->getSightCenter();
-
-			if(objAtTile && objAtTile->ID == Obj::BOAT && isInScreenRange(hpos, mapPos))
-				CCS->curh->set(Cursor::Map::SCUTTLE_BOAT);
-			else
-				CCS->curh->set(Cursor::Map::POINTER);
-			return;
-			}
-		case SpellID::DIMENSION_DOOR:
-			{
-				const TerrainTile * t = LOCPLINT->cb->getTile(mapPos, false);
-				int3 hpos = LOCPLINT->localState->getCurrentArmy()->getSightCenter();
-				if((!t || t->isClear(LOCPLINT->cb->getTile(hpos))) && isInScreenRange(hpos, mapPos))
-					CCS->curh->set(Cursor::Map::TELEPORT);
-				else
-					CCS->curh->set(Cursor::Map::POINTER);
-				return;
-			}
-		}
-	}
-
-	if(LOCPLINT->localState->getCurrentArmy()->ID == Obj::TOWN)
-	{
-		if(objAtTile)
-		{
-			if(objAtTile->ID == Obj::TOWN && objRelations != PlayerRelations::ENEMIES)
-				CCS->curh->set(Cursor::Map::TOWN);
-			else if(objAtTile->ID == Obj::HERO && objRelations == PlayerRelations::SAME_PLAYER)
-				CCS->curh->set(Cursor::Map::HERO);
-			else
-				CCS->curh->set(Cursor::Map::POINTER);
-		}
-		else
-			CCS->curh->set(Cursor::Map::POINTER);
-	}
-	else if(const CGHeroInstance * hero = LOCPLINT->localState->getCurrentHero())
-	{
-		std::array<Cursor::Map, 4> cursorMove      = { Cursor::Map::T1_MOVE,       Cursor::Map::T2_MOVE,       Cursor::Map::T3_MOVE,       Cursor::Map::T4_MOVE,       };
-		std::array<Cursor::Map, 4> cursorAttack    = { Cursor::Map::T1_ATTACK,     Cursor::Map::T2_ATTACK,     Cursor::Map::T3_ATTACK,     Cursor::Map::T4_ATTACK,     };
-		std::array<Cursor::Map, 4> cursorSail      = { Cursor::Map::T1_SAIL,       Cursor::Map::T2_SAIL,       Cursor::Map::T3_SAIL,       Cursor::Map::T4_SAIL,       };
-		std::array<Cursor::Map, 4> cursorDisembark = { Cursor::Map::T1_DISEMBARK,  Cursor::Map::T2_DISEMBARK,  Cursor::Map::T3_DISEMBARK,  Cursor::Map::T4_DISEMBARK,  };
-		std::array<Cursor::Map, 4> cursorExchange  = { Cursor::Map::T1_EXCHANGE,   Cursor::Map::T2_EXCHANGE,   Cursor::Map::T3_EXCHANGE,   Cursor::Map::T4_EXCHANGE,   };
-		std::array<Cursor::Map, 4> cursorVisit     = { Cursor::Map::T1_VISIT,      Cursor::Map::T2_VISIT,      Cursor::Map::T3_VISIT,      Cursor::Map::T4_VISIT,      };
-		std::array<Cursor::Map, 4> cursorSailVisit = { Cursor::Map::T1_SAIL_VISIT, Cursor::Map::T2_SAIL_VISIT, Cursor::Map::T3_SAIL_VISIT, Cursor::Map::T4_SAIL_VISIT, };
-
-		const CGPathNode * pathNode = LOCPLINT->cb->getPathsInfo(hero)->getPathInfo(mapPos);
-		assert(pathNode);
-
-		if((GH.isKeyboardAltDown() || settings["gameTweaks"]["forceMovementInfo"].Bool()) && pathNode->reachable()) //overwrite status bar text with movement info
-		{
-			showMoveDetailsInStatusbar(*hero, *pathNode);
-		}
-
-		int turns = pathNode->turns;
-		vstd::amin(turns, 3);
-		switch(pathNode->action)
-		{
-		case CGPathNode::NORMAL:
-		case CGPathNode::TELEPORT_NORMAL:
-			if(pathNode->layer == EPathfindingLayer::LAND)
-				CCS->curh->set(cursorMove[turns]);
-			else
-				CCS->curh->set(cursorSailVisit[turns]);
-			break;
-
-		case CGPathNode::VISIT:
-		case CGPathNode::BLOCKING_VISIT:
-		case CGPathNode::TELEPORT_BLOCKING_VISIT:
-			if(objAtTile && objAtTile->ID == Obj::HERO)
-			{
-				if(LOCPLINT->localState->getCurrentArmy()  == objAtTile)
-					CCS->curh->set(Cursor::Map::HERO);
-				else
-					CCS->curh->set(cursorExchange[turns]);
-			}
-			else if(pathNode->layer == EPathfindingLayer::LAND)
-				CCS->curh->set(cursorVisit[turns]);
-			else
-				CCS->curh->set(cursorSailVisit[turns]);
-			break;
-
-		case CGPathNode::BATTLE:
-		case CGPathNode::TELEPORT_BATTLE:
-			CCS->curh->set(cursorAttack[turns]);
-			break;
-
-		case CGPathNode::EMBARK:
-			CCS->curh->set(cursorSail[turns]);
-			break;
-
-		case CGPathNode::DISEMBARK:
-			CCS->curh->set(cursorDisembark[turns]);
-			break;
-
-		default:
-			if(objAtTile && objRelations != PlayerRelations::ENEMIES)
-			{
-				if(objAtTile->ID == Obj::TOWN)
-					CCS->curh->set(Cursor::Map::TOWN);
-				else if(objAtTile->ID == Obj::HERO && objRelations == PlayerRelations::SAME_PLAYER)
-					CCS->curh->set(Cursor::Map::HERO);
-				else
-					CCS->curh->set(Cursor::Map::POINTER);
-			}
-			else
-				CCS->curh->set(Cursor::Map::POINTER);
-			break;
-		}
-	}
-
-	if(ourInaccessibleShipyard(objAtTile))
-	{
-		CCS->curh->set(Cursor::Map::T1_SAIL);
-	}
-}
-
-void CAdventureMapInterface::showMoveDetailsInStatusbar(const CGHeroInstance & hero, const CGPathNode & pathNode)
-{
-	const int maxMovementPointsAtStartOfLastTurn = pathNode.turns > 0 ? hero.maxMovePoints(pathNode.layer == EPathfindingLayer::LAND) : hero.movement;
-	const int movementPointsLastTurnCost = maxMovementPointsAtStartOfLastTurn - pathNode.moveRemains;
-	const int remainingPointsAfterMove = pathNode.turns == 0 ? pathNode.moveRemains : 0;
-
-	std::string result = VLC->generaltexth->translate("vcmi.adventureMap", pathNode.turns > 0 ? "moveCostDetails" : "moveCostDetailsNoTurns");
-
-	boost::replace_first(result, "%TURNS", std::to_string(pathNode.turns));
-	boost::replace_first(result, "%POINTS", std::to_string(movementPointsLastTurnCost));
-	boost::replace_first(result, "%REMAINING", std::to_string(remainingPointsAfterMove));
-
-	statusbar->write(result);
-}
-
-void CAdventureMapInterface::onTileRightClicked(const int3 &mapPos)
-{
-	if(state != EGameState::MAKING_TURN)
-		return;
-
-	if(spellBeingCasted)
-	{
-		abortCastingMode();
-		return;
-	}
-
-	if(!LOCPLINT->cb->isVisible(mapPos))
-	{
-		CRClickPopup::createAndPush(VLC->generaltexth->allTexts[61]); //Uncharted Territory
-		return;
-	}
-
-	const CGObjectInstance * obj = getActiveObject(mapPos);
-	if(!obj)
-	{
-		// Bare or undiscovered terrain
-		const TerrainTile * tile = LOCPLINT->cb->getTile(mapPos);
-		if(tile)
-		{
-			std::string hlp = CGI->mh->getTerrainDescr(mapPos, true);
-			CRClickPopup::createAndPush(hlp);
-		}
-		return;
-	}
-
-	CRClickPopup::createAndPush(obj, GH.getCursorPosition(), ETextAlignment::CENTER);
-}
-
-void CAdventureMapInterface::enterCastingMode(const CSpell * sp)
-{
-	assert(sp->id == SpellID::SCUTTLE_BOAT || sp->id == SpellID::DIMENSION_DOOR);
-	spellBeingCasted = sp;
-	Settings config = settings.write["session"]["showSpellRange"];
-	config->Bool() = true;
-
-	deactivate();
-	terrain->activate();
-	GH.fakeMouseMove();
-}
-
-void CAdventureMapInterface::exitCastingMode()
-{
-	assert(spellBeingCasted);
-	spellBeingCasted = nullptr;
-	terrain->deactivate();
-	activate();
-
-	Settings config = settings.write["session"]["showSpellRange"];
-	config->Bool() = false;
-}
-
-void CAdventureMapInterface::abortCastingMode()
-{
-	exitCastingMode();
-	LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[731]); //Spell cancelled
-}
-
-void CAdventureMapInterface::leaveCastingMode(const int3 & dest)
-{
-	SpellID id = spellBeingCasted->id;
-	exitCastingMode();
-	LOCPLINT->cb->castSpell(LOCPLINT->localState->getCurrentHero(), id, dest);
-}
-
-Rect CAdventureMapInterface::terrainAreaPixels() const
-{
-	return terrain->pos;
-}
-
-const IShipyard * CAdventureMapInterface::ourInaccessibleShipyard(const CGObjectInstance *obj) const
-{
-	const IShipyard *ret = IShipyard::castFrom(obj);
-
-	if(!ret ||
-		obj->tempOwner != currentPlayerID ||
-		(CCS->curh->get<Cursor::Map>() != Cursor::Map::T1_SAIL && CCS->curh->get<Cursor::Map>() != Cursor::Map::POINTER))
-		return nullptr;
-
-	return ret;
-}
-
-void CAdventureMapInterface::exitWorldView()
-{
-	state = EGameState::MAKING_TURN;
-
-	panelMain->activate();
-	panelWorldView->deactivate();
-	activeMapPanel = panelMain;
-
-	townList->activate();
-	heroList->activate();
-	infoBar->activate();
-
-	redraw();
-	terrain->onViewMapActivated();
-}
-
-void CAdventureMapInterface::openWorldView(int tileSize)
-{
-	state = EGameState::WORLD_VIEW;
-	panelMain->deactivate();
-	panelWorldView->activate();
-
-	activeMapPanel = panelWorldView;
-
-	townList->deactivate();
-	heroList->deactivate();
-	infoBar->showSelection(); // to prevent new day animation interfering world view mode
-	infoBar->deactivate();
-
-	redraw();
-	terrain->onViewWorldActivated(tileSize);
-}
-
-void CAdventureMapInterface::openWorldView()
-{
-	openWorldView(11);
-}
-
-void CAdventureMapInterface::openWorldView(const std::vector<ObjectPosInfo>& objectPositions, bool showTerrain)
-{
-	openWorldView(11);
-	terrain->onViewSpellActivated(11, objectPositions, showTerrain);
-}

+ 19 - 24
client/adventureMap/CInGameConsole.cpp

@@ -17,8 +17,11 @@
 #include "../PlayerLocalState.h"
 #include "../ClientCommandManager.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/WindowHandler.h"
 #include "../gui/Shortcut.h"
 #include "../render/Colors.h"
+#include "../adventureMap/AdventureMapInterface.h"
+#include "../windows/CMessage.h"
 
 #include "../../CCallback.h"
 #include "../../lib/CConfigHandler.h"
@@ -50,7 +53,7 @@ void CInGameConsole::show(SDL_Surface * to)
 		Point leftBottomCorner(0, pos.h);
 		Point textPosition(leftBottomCorner.x + 50, leftBottomCorner.y - texts.size() * 20 - 80 + number * 20);
 
-		graphics->fonts[FONT_MEDIUM]->renderTextLeft(to, text.text, Colors::GREEN, textPosition );
+		graphics->fonts[FONT_MEDIUM]->renderTextLeft(to, text.text, Colors::GREEN, pos.topLeft() + textPosition );
 
 		number++;
 	}
@@ -75,7 +78,7 @@ void CInGameConsole::tick(uint32_t msPassed)
 	}
 
 	if(sizeBefore != texts.size())
-		GH.totalRedraw(); // FIXME: ingame console has no parent widget set
+		GH.windows().totalRedraw(); // FIXME: ingame console has no parent widget set
 }
 
 void CInGameConsole::print(const std::string & txt)
@@ -83,30 +86,23 @@ void CInGameConsole::print(const std::string & txt)
 	// boost::unique_lock scope
 	{
 		boost::unique_lock<boost::mutex> lock(texts_mx);
-		int lineLen = conf.go()->ac.outputLineLength;
 
-		if(txt.size() < lineLen)
-		{
-			texts.push_back({txt, 0});
-		}
-		else
-		{
-			assert(lineLen);
-			for(int g = 0; g < txt.size() / lineLen + 1; ++g)
-			{
-				std::string part = txt.substr(g * lineLen, lineLen);
-				if(part.empty())
-					break;
+		// Maximum width for a text line is limited by:
+		// 1) width of adventure map terrain area, for when in-game console is on top of advmap
+		// 2) width of castle/battle window (fixed to 800) when this window is open
+		// 3) arbitrary selected left and right margins
+		int maxWidth = std::min( 800, adventureInt->terrainAreaPixels().w) - 100;
 
-				texts.push_back({part, 0});
-			}
-		}
+		auto splitText = CMessage::breakText(txt, maxWidth, FONT_MEDIUM);
+
+		for (auto const & entry : splitText)
+			texts.push_back({entry, 0});
 
 		while(texts.size() > maxDisplayedTexts)
 			texts.erase(texts.begin());
 	}
 
-	GH.totalRedraw(); // FIXME: ingame console has no parent widget set
+	GH.windows().totalRedraw(); // FIXME: ingame console has no parent widget set
 }
 
 void CInGameConsole::keyPressed (EShortcut key)
@@ -215,22 +211,21 @@ void CInGameConsole::textEdited(const std::string & inputtedText)
 
 void CInGameConsole::startEnteringText()
 {
-	if (!active)
+	if (!isActive())
 		return;
 
 	if (captureAllKeys)
 		return;
 
-	assert(GH.statusbar);
 	assert(currentStatusBar.expired());//effectively, nullptr check
 
-	currentStatusBar = GH.statusbar;
+	currentStatusBar = GH.statusbar();
 
 	captureAllKeys = true;
 	enteredText = "_";
 
-	GH.statusbar->setEnteringMode(true);
-	GH.statusbar->setEnteredText(enteredText);
+	GH.statusbar()->setEnteringMode(true);
+	GH.statusbar()->setEnteredText(enteredText);
 }
 
 void CInGameConsole::endEnteringText(bool processEnteredText)

+ 6 - 6
client/adventureMap/CInfoBar.cpp

@@ -11,7 +11,7 @@
 #include "StdInc.h"
 #include "CInfoBar.h"
 
-#include "CAdventureMapInterface.h"
+#include "AdventureMapInterface.h"
 
 #include "../widgets/CComponent.h"
 #include "../widgets/Images.h"
@@ -24,6 +24,7 @@
 #include "../CPlayerInterface.h"
 #include "../PlayerLocalState.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/WindowHandler.h"
 
 #include "../../CCallback.h"
 #include "../../lib/CGeneralTextHandler.h"
@@ -261,7 +262,7 @@ void CInfoBar::tick(uint32_t msPassed)
 	{
 		timerCounter = 0;
 		removeUsedEvents(TIME);
-		if(GH.topInt() == adventureInt)
+		if(GH.windows().isTopWindow(adventureInt))
 			popComponents(true);
 	}
 	else
@@ -292,9 +293,9 @@ void CInfoBar::clickRight(tribool down, bool previousState)
 void CInfoBar::hover(bool on)
 {
 	if(on)
-		GH.statusbar->write(CGI->generaltexth->zelp[292].first);
+		GH.statusbar()->write(CGI->generaltexth->zelp[292].first);
 	else
-		GH.statusbar->clear();
+		GH.statusbar()->clear();
 }
 
 CInfoBar::CInfoBar(const Rect & position)
@@ -315,8 +316,7 @@ CInfoBar::CInfoBar(const Point & position): CInfoBar(Rect(position.x, position.y
 
 void CInfoBar::setTimer(uint32_t msToTrigger)
 {
-	if (!(active & TIME))
-		addUsedEvents(TIME);
+	addUsedEvents(TIME);
 	timerCounter = msToTrigger;
 }
 

+ 47 - 20
client/adventureMap/CList.cpp

@@ -11,15 +11,17 @@
 #include "StdInc.h"
 #include "CList.h"
 
-#include "CAdventureMapInterface.h"
+#include "AdventureMapInterface.h"
 
 #include "../widgets/Images.h"
 #include "../widgets/Buttons.h"
+#include "../widgets/ObjectLists.h"
 #include "../windows/InfoWindows.h"
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
 #include "../PlayerLocalState.h"
 #include "../gui/CGuiHandler.h"
+#include "../renderSDL/SDL_Extensions.h"
 
 #include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/CHeroHandler.h"
@@ -66,9 +68,9 @@ void CList::CListItem::clickLeft(tribool down, bool previousState)
 void CList::CListItem::hover(bool on)
 {
 	if (on)
-		GH.statusbar->write(getHoverText());
+		GH.statusbar()->write(getHoverText());
 	else
-		GH.statusbar->clear();
+		GH.statusbar()->clear();
 }
 
 void CList::CListItem::onSelect(bool on)
@@ -81,24 +83,44 @@ void CList::CListItem::onSelect(bool on)
 	redraw();
 }
 
-CList::CList(int Size, Point position, std::string btnUp, std::string btnDown, size_t listAmount, int helpUp, int helpDown, CListBox::CreateFunc create)
-	: CIntObject(0, position),
+CList::CList(int Size, Rect widgetDimensions)
+	: CIntObject(0, widgetDimensions.topLeft()),
 	size(Size),
 	selected(nullptr)
+{
+	pos.w = widgetDimensions.w;
+	pos.h = widgetDimensions.h;
+}
+
+void CList::showAll(SDL_Surface * to)
+{
+	CSDL_Ext::fillRect(to, pos, Colors::BLACK);
+	CIntObject::showAll(to);
+}
+
+void CList::createList(Point firstItemPosition, Point itemPositionDelta, size_t listAmount)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
-	scrollUp = std::make_shared<CButton>(Point(0, 0), btnUp, CGI->generaltexth->zelp[helpUp]);
-	scrollDown = std::make_shared<CButton>(Point(0, scrollUp->pos.h + 32*(int)size), btnDown, CGI->generaltexth->zelp[helpDown]);
+	listBox = std::make_shared<CListBox>(std::bind(&CList::createItem, this, _1), firstItemPosition, itemPositionDelta, size, listAmount);
+}
 
-	listBox = std::make_shared<CListBox>(create, Point(1,scrollUp->pos.h), Point(0, 32), size, listAmount);
+void CList::setScrollUpButton(std::shared_ptr<CButton> button)
+{
+	addChild(button.get());
 
-	//assign callback only after list was created
+	scrollUp = button;
 	scrollUp->addCallback(std::bind(&CListBox::moveToPrev, listBox));
-	scrollDown->addCallback(std::bind(&CListBox::moveToNext, listBox));
-
 	scrollUp->addCallback(std::bind(&CList::update, this));
-	scrollDown->addCallback(std::bind(&CList::update, this));
+	update();
+}
 
+void CList::setScrollDownButton(std::shared_ptr<CButton> button)
+{
+	addChild(button.get());
+
+	scrollDown = button;
+	scrollDown->addCallback(std::bind(&CList::update, this));
+	scrollDown->addCallback(std::bind(&CListBox::moveToNext, listBox));
 	update();
 }
 
@@ -107,8 +129,11 @@ void CList::update()
 	bool onTop = listBox->getPos() == 0;
 	bool onBottom = listBox->getPos() + size >= listBox->size();
 
-	scrollUp->block(onTop);
-	scrollDown->block(onBottom);
+	if (scrollUp)
+		scrollUp->block(onTop);
+
+	if (scrollDown)
+		scrollDown->block(onBottom);
 }
 
 void CList::select(std::shared_ptr<CListItem> which)
@@ -223,16 +248,17 @@ std::string CHeroList::CHeroItem::getHoverText()
 	return boost::str(boost::format(CGI->generaltexth->allTexts[15]) % hero->getNameTranslated() % hero->type->heroClass->getNameTranslated());
 }
 
-std::shared_ptr<CIntObject> CHeroList::createHeroItem(size_t index)
+std::shared_ptr<CIntObject> CHeroList::createItem(size_t index)
 {
 	if (LOCPLINT->localState->getWanderingHeroes().size() > index)
 		return std::make_shared<CHeroItem>(this, LOCPLINT->localState->getWanderingHero(index));
 	return std::make_shared<CEmptyHeroItem>();
 }
 
-CHeroList::CHeroList(int size, Point position, std::string btnUp, std::string btnDown):
-	CList(size, position, btnUp, btnDown, LOCPLINT->localState->getWanderingHeroes().size(), 303, 304, std::bind(&CHeroList::createHeroItem, this, _1))
+CHeroList::CHeroList(int visibleItemsCount, Rect widgetPosition, Point firstItemOffset, Point itemOffsetDelta, size_t initialItemsCount)
+	: CList(visibleItemsCount, widgetPosition)
 {
+	createList(firstItemOffset, itemOffsetDelta, initialItemsCount);
 }
 
 void CHeroList::select(const CGHeroInstance * hero)
@@ -261,7 +287,7 @@ void CHeroList::update(const CGHeroInstance * hero)
 	CList::update();
 }
 
-std::shared_ptr<CIntObject> CTownList::createTownItem(size_t index)
+std::shared_ptr<CIntObject> CTownList::createItem(size_t index)
 {
 	if (LOCPLINT->localState->getOwnedTowns().size() > index)
 		return std::make_shared<CTownItem>(this, LOCPLINT->localState->getOwnedTown(index));
@@ -312,9 +338,10 @@ std::string CTownList::CTownItem::getHoverText()
 	return town->getObjectName();
 }
 
-CTownList::CTownList(int size, Point position, std::string btnUp, std::string btnDown):
-	CList(size, position, btnUp, btnDown, LOCPLINT->localState->getOwnedTowns().size(),  306, 307, std::bind(&CTownList::createTownItem, this, _1))
+CTownList::CTownList(int visibleItemsCount, Rect widgetPosition, Point firstItemOffset, Point itemOffsetDelta, size_t initialItemsCount)
+	: CList(visibleItemsCount, widgetPosition)
 {
+	createList(firstItemOffset, itemOffsetDelta, initialItemsCount);
 }
 
 void CTownList::select(const CGTownInstance * town)

+ 23 - 31
client/adventureMap/CList.h

@@ -10,8 +10,6 @@
 #pragma once
 
 #include "../gui/CIntObject.h"
-
-#include "../widgets/ObjectLists.h"
 #include "../../lib/FunctionList.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -21,7 +19,9 @@ class CGTownInstance;
 
 VCMI_LIB_NAMESPACE_END
 
+class CListBox;
 class CButton;
+class CAnimImage;
 
 /// Base UI Element for hero\town lists
 class CList : public CIntObject
@@ -53,23 +53,9 @@ protected:
 		virtual std::string getHoverText()=0;
 	};
 
-	std::shared_ptr<CListBox> listBox;
+private:
 	const size_t size;
 
-	/**
-	 * @brief CList - protected constructor
-	 * @param size - maximal amount of visible at once items
-	 * @param position - cordinates
-	 * @param btnUp - path to image to use as top button
-	 * @param btnDown - path to image to use as bottom button
-	 * @param listAmount - amount of items in the list
-	 * @param helpUp - index in zelp.txt for button help tooltip
-	 * @param helpDown - index in zelp.txt for button help tooltip
-	 * @param create - function for creating items in listbox
-	 * @param destroy - function for deleting items in listbox
-	 */
-	CList(int size, Point position, std::string btnUp, std::string btnDown, size_t listAmount, int helpUp, int helpDown, CListBox::CreateFunc create);
-
 	//for selection\deselection
 	std::shared_ptr<CListItem> selected;
 	void select(std::shared_ptr<CListItem> which);
@@ -78,8 +64,14 @@ protected:
 	std::shared_ptr<CButton> scrollUp;
 	std::shared_ptr<CButton> scrollDown;
 
-	/// should be called when list is invalidated
-	void update();
+protected:
+	std::shared_ptr<CListBox> listBox;
+
+	CList(int size, Rect widgetDimensions);
+
+	void createList(Point firstItemPosition, Point itemPositionDelta, size_t listAmount);
+
+	virtual std::shared_ptr<CIntObject> createItem(size_t index) = 0;
 
 public:
 	/// functions that will be called when selection changes
@@ -88,10 +80,18 @@ public:
 	/// return index of currently selected element
 	int getSelectedIndex();
 
+	void setScrollUpButton(std::shared_ptr<CButton> button);
+	void setScrollDownButton(std::shared_ptr<CButton> button);
+
+	/// should be called when list is invalidated
+	void update();
+
 	/// set of methods to switch selection
 	void selectIndex(int which);
 	void selectNext();
 	void selectPrev();
+
+	void showAll(SDL_Surface * to) override;
 };
 
 /// List of heroes which is shown at the right of the adventure map screen
@@ -125,13 +125,9 @@ class CHeroList	: public CList
 		std::string getHoverText() override;
 	};
 
-	std::shared_ptr<CIntObject> createHeroItem(size_t index);
+	std::shared_ptr<CIntObject> createItem(size_t index);
 public:
-	/**
-	 * @brief CHeroList
-	 * @param size, position, btnUp, btnDown @see CList::CList
-	 */
-	CHeroList(int size, Point position, std::string btnUp, std::string btnDown);
+	CHeroList(int visibleItemsCount, Rect widgetPosition, Point firstItemOffset, Point itemOffsetDelta, size_t initialItemsCount);
 
 	/// Select specific hero and scroll if needed
 	void select(const CGHeroInstance * hero = nullptr);
@@ -159,13 +155,9 @@ class CTownList	: public CList
 		std::string getHoverText() override;
 	};
 
-	std::shared_ptr<CIntObject> createTownItem(size_t index);
+	std::shared_ptr<CIntObject> createItem(size_t index) override;
 public:
-	/**
-	 * @brief CTownList
-	 * @param size, position, btnUp, btnDown @see CList::CList
-	 */
-	CTownList(int size, Point position, std::string btnUp, std::string btnDown);
+	CTownList(int visibleItemsCount, Rect widgetPosition, Point firstItemOffset, Point itemOffsetDelta, size_t initialItemsCount);
 
 	/// Select specific town and scroll if needed
 	void select(const CGTownInstance * town = nullptr);

+ 8 - 6
client/adventureMap/CMinimap.cpp

@@ -11,12 +11,14 @@
 #include "StdInc.h"
 #include "CMinimap.h"
 
-#include "CAdventureMapInterface.h"
+#include "AdventureMapInterface.h"
 
 #include "../widgets/Images.h"
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/MouseButton.h"
+#include "../gui/WindowHandler.h"
 #include "../render/Colors.h"
 #include "../renderSDL/SDL_Extensions.h"
 #include "../render/Canvas.h"
@@ -130,8 +132,8 @@ void CMinimap::moveAdvMapSelection()
 	int3 newLocation = pixelToTile(GH.getCursorPosition() - pos.topLeft());
 	adventureInt->centerOnTile(newLocation);
 
-	if (!(adventureInt->active & GENERAL))
-		GH.totalRedraw(); //redraw this as well as inactive adventure map
+	if (!(adventureInt->isActive()))
+		GH.windows().totalRedraw(); //redraw this as well as inactive adventure map
 	else
 		redraw();//redraw only this
 }
@@ -151,14 +153,14 @@ void CMinimap::clickRight(tribool down, bool previousState)
 void CMinimap::hover(bool on)
 {
 	if(on)
-		GH.statusbar->write(CGI->generaltexth->zelp[291].first);
+		GH.statusbar()->write(CGI->generaltexth->zelp[291].first);
 	else
-		GH.statusbar->clear();
+		GH.statusbar()->clear();
 }
 
 void CMinimap::mouseMoved(const Point & cursorPosition)
 {
-	if(mouseState(MouseButton::LEFT))
+	if(isMouseButtonPressed(MouseButton::LEFT))
 		moveAdvMapSelection();
 }
 

+ 25 - 33
client/adventureMap/CResDataBar.cpp

@@ -19,50 +19,40 @@
 #include "../../CCallback.h"
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/ResourceSet.h"
 
-#define ADVOPT (conf.go()->ac)
-
-CResDataBar::CResDataBar(const std::string & defname, int x, int y, int offx, int offy, int resdist, int datedist)
+CResDataBar::CResDataBar(const std::string & imageName, const Point & position)
 {
-	pos.x += x;
-	pos.y += y;
+	pos.x += position.x;
+	pos.y += position.y;
+
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
-	background = std::make_shared<CPicture>(defname, 0, 0);
+	background = std::make_shared<CPicture>(imageName, 0, 0);
 	background->colorize(LOCPLINT->playerID);
 
 	pos.w = background->pos.w;
 	pos.h = background->pos.h;
 
-	txtpos.resize(8);
-	for (int i = 0; i < 8 ; i++)
-	{
-		txtpos[i].first = pos.x + offx + resdist*i;
-		txtpos[i].second = pos.y + offy;
-	}
-	txtpos[7].first = txtpos[6].first + datedist;
 	addUsedEvents(RCLICK);
 }
 
-CResDataBar::CResDataBar()
+CResDataBar::CResDataBar(const std::string & defname, int x, int y, int offx, int offy, int resdist, int datedist):
+	CResDataBar(defname, Point(x,y))
 {
-	pos.x += ADVOPT.resdatabarX;
-	pos.y += ADVOPT.resdatabarY;
+	for (int i = 0; i < 7 ; i++)
+		resourcePositions[GameResID(i)] = Point( offx + resdist*i, offy );
 
-	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
-	background = std::make_shared<CPicture>(ADVOPT.resdatabarG, 0, 0);
-	background->colorize(LOCPLINT->playerID);
-
-	pos.w = background->pos.w;
-	pos.h = background->pos.h;
+	datePosition = resourcePositions[EGameResID::GOLD] + Point(datedist, 0);
+}
 
-	txtpos.resize(8);
-	for (int i = 0; i < 8 ; i++)
-	{
-		txtpos[i].first = pos.x + ADVOPT.resOffsetX + ADVOPT.resDist*i;
-		txtpos[i].second = pos.y + ADVOPT.resOffsetY;
-	}
-	txtpos[7].first = txtpos[6].first + ADVOPT.resDateDist;
+void CResDataBar::setDatePosition(const Point & position)
+{
+	datePosition = position;
+}
 
+void CResDataBar::setResourcePosition(const GameResID & resource, const Point & position)
+{
+	resourcePositions[resource] = position;
 }
 
 std::string CResDataBar::buildDateString()
@@ -80,13 +70,15 @@ 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 (GameResID i=EGameResID::WOOD; i <= GameResID(EGameResID::GOLD); ++i)
+	for (auto & entry : resourcePositions)
 	{
-		std::string text = std::to_string(LOCPLINT->cb->getResourceAmount(i));
+		std::string text = std::to_string(LOCPLINT->cb->getResourceAmount(entry.first));
 
-		graphics->fonts[FONT_SMALL]->renderTextLeft(to, text, Colors::WHITE, Point(txtpos[i].first, txtpos[i].second));
+		graphics->fonts[FONT_SMALL]->renderTextLeft(to, text, Colors::WHITE, pos.topLeft() + entry.second);
 	}
-	graphics->fonts[FONT_SMALL]->renderTextLeft(to, buildDateString(), Colors::WHITE, Point(txtpos[7].first, txtpos[7].second));
+
+	if (datePosition)
+		graphics->fonts[FONT_SMALL]->renderTextLeft(to, buildDateString(), Colors::WHITE, pos.topLeft() + *datePosition);
 }
 
 void CResDataBar::showAll(SDL_Surface * to)

+ 15 - 3
client/adventureMap/CResDataBar.h

@@ -11,6 +11,11 @@
 
 #include "../gui/CIntObject.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+enum class EGameResID : int8_t;
+using GameResID = Identifier<EGameResID>;
+VCMI_LIB_NAMESPACE_END
+
 /// Resources bar which shows information about how many gold, crystals,... you have
 /// Current date is displayed too
 class CResDataBar : public CIntObject
@@ -19,14 +24,21 @@ class CResDataBar : public CIntObject
 
 	std::shared_ptr<CPicture> background;
 
-	std::vector<std::pair<int,int> > txtpos;
-
+	std::map<GameResID, Point> resourcePositions;
+	std::optional<Point> datePosition;
 
 	void draw(SDL_Surface * to);
 public:
-	CResDataBar();
+
+	/// For dynamically-sized UI windows, e.g. adventure map interface
+	CResDataBar(const std::string & imageName, const Point & position);
+
+	/// For fixed-size UI windows, e.g. CastleInterface
 	CResDataBar(const std::string &defname, int x, int y, int offx, int offy, int resdist, int datedist);
 
+	void setDatePosition(const Point & position);
+	void setResourcePosition(const GameResID & resource, const Point & position);
+
 	void colorize(PlayerColor player);
 	void showAll(SDL_Surface * to) override;
 };

+ 9 - 8
client/battle/BattleActionsController.cpp

@@ -22,6 +22,7 @@
 #include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/CIntObject.h"
+#include "../gui/WindowHandler.h"
 #include "../windows/CCreatureWindow.h"
 
 #include "../../CCallback.h"
@@ -668,7 +669,7 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, B
 
 		case PossiblePlayerBattleAction::CREATURE_INFO:
 		{
-			GH.pushIntT<CStackWindow>(targetStack, false);
+			GH.windows().createAndPushWindow<CStackWindow>(targetStack, false);
 			return;
 		}
 
@@ -772,7 +773,7 @@ void BattleActionsController::onHexHovered(BattleHex hoveredHex)
 	if (owner.openingPlaying())
 	{
 		currentConsoleMsg = VLC->generaltexth->translate("vcmi.battleWindow.pressKeyToSkipIntro");
-		GH.statusbar->write(currentConsoleMsg);
+		GH.statusbar()->write(currentConsoleMsg);
 		return;
 	}
 
@@ -782,7 +783,7 @@ void BattleActionsController::onHexHovered(BattleHex hoveredHex)
 	if (hoveredHex == BattleHex::INVALID)
 	{
 		if (!currentConsoleMsg.empty())
-			GH.statusbar->clearIfMatching(currentConsoleMsg);
+			GH.statusbar()->clearIfMatching(currentConsoleMsg);
 
 		currentConsoleMsg.clear();
 		CCS->curh->set(Cursor::Combat::BLOCKED);
@@ -805,10 +806,10 @@ void BattleActionsController::onHexHovered(BattleHex hoveredHex)
 	}
 
 	if (!currentConsoleMsg.empty())
-		GH.statusbar->clearIfMatching(currentConsoleMsg);
+		GH.statusbar()->clearIfMatching(currentConsoleMsg);
 
 	if (!newConsoleMsg.empty())
-		GH.statusbar->write(newConsoleMsg);
+		GH.statusbar()->write(newConsoleMsg);
 
 	currentConsoleMsg = newConsoleMsg;
 }
@@ -818,7 +819,7 @@ void BattleActionsController::onHoverEnded()
 	CCS->curh->set(Cursor::Combat::POINTER);
 
 	if (!currentConsoleMsg.empty())
-		GH.statusbar->clearIfMatching(currentConsoleMsg);
+		GH.statusbar()->clearIfMatching(currentConsoleMsg);
 
 	currentConsoleMsg.clear();
 }
@@ -849,7 +850,7 @@ void BattleActionsController::onHexLeftClicked(BattleHex clickedHex)
 	{
 		actionRealize(action, clickedHex);
 
-		GH.statusbar->clear();
+		GH.statusbar()->clear();
 	}
 	else
 	{
@@ -973,7 +974,7 @@ void BattleActionsController::onHexRightClicked(BattleHex clickedHex)
 	auto selectedStack = owner.curInt->cb->battleGetStackByPos(clickedHex, true);
 
 	if (selectedStack != nullptr)
-		GH.pushIntT<CStackWindow>(selectedStack, true);
+		GH.windows().createAndPushWindow<CStackWindow>(selectedStack, true);
 
 	if (clickedHex == BattleHex::HERO_ATTACKER && owner.attackingHero)
 		owner.attackingHero->heroRightClicked();

+ 17 - 17
client/battle/BattleAnimationClasses.cpp

@@ -222,7 +222,7 @@ bool DummyAnimation::init()
 	return true;
 }
 
-void DummyAnimation::nextFrame()
+void DummyAnimation::tick(uint32_t msPassed)
 {
 	counter++;
 	if(counter > howMany)
@@ -300,7 +300,7 @@ ECreatureAnimType MeleeAttackAnimation::selectGroup(bool multiAttack)
 	return mutPosToGroup[mutPos];
 }
 
-void MeleeAttackAnimation::nextFrame()
+void MeleeAttackAnimation::tick(uint32_t msPassed)
 {
 	size_t currentFrame = stackAnimation(attackingStack)->getCurrentFrame();
 	size_t totalFrames = stackAnimation(attackingStack)->framesInGroup(getGroup());
@@ -308,7 +308,7 @@ void MeleeAttackAnimation::nextFrame()
 	if ( currentFrame * 2 >= totalFrames )
 		owner.executeAnimationStage(EAnimationEvents::HIT);
 
-	AttackAnimation::nextFrame();
+	AttackAnimation::tick(msPassed);
 }
 
 MeleeAttackAnimation::MeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool multiAttack)
@@ -379,15 +379,15 @@ bool MovementAnimation::init()
 	return true;
 }
 
-void MovementAnimation::nextFrame()
+void MovementAnimation::tick(uint32_t msPassed)
 {
-	progress += float(GH.mainFPSmng->getElapsedMilliseconds()) / 1000 * progressPerSecond;
+	progress += float(msPassed) / 1000 * progressPerSecond;
 
 	//moving instructions
 	myAnim->pos.x = static_cast<Sint16>(begX + distanceX * progress );
 	myAnim->pos.y = static_cast<Sint16>(begY + distanceY * progress );
 
-	BattleAnimation::nextFrame();
+	BattleAnimation::tick(msPassed);
 
 	if(progress >= 1.0)
 	{
@@ -577,9 +577,9 @@ bool ColorTransformAnimation::init()
 	return true;
 }
 
-void ColorTransformAnimation::nextFrame()
+void ColorTransformAnimation::tick(uint32_t msPassed)
 {
-	float elapsed  = GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+	float elapsed  = msPassed / 1000.f;
 	float fullTime = AnimationControls::getFadeInDuration();
 	float delta    = elapsed / fullTime;
 	totalProgress += delta;
@@ -699,7 +699,7 @@ void RangedAttackAnimation::emitProjectile()
 	projectileEmitted = true;
 }
 
-void RangedAttackAnimation::nextFrame()
+void RangedAttackAnimation::tick(uint32_t msPassed)
 {
 	// animation should be paused if there is an active projectile
 	if (projectileEmitted)
@@ -716,7 +716,7 @@ void RangedAttackAnimation::nextFrame()
 	else
 		stackAnimation(attackingStack)->playUntil(static_cast<size_t>(-1));
 
-	AttackAnimation::nextFrame();
+	AttackAnimation::tick(msPassed);
 
 	if (!projectileEmitted)
 	{
@@ -790,9 +790,9 @@ CatapultAnimation::CatapultAnimation(BattleInterface & owner, const CStack * att
 	logAnim->debug("Created shooting anim for %s", stack->getName());
 }
 
-void CatapultAnimation::nextFrame()
+void CatapultAnimation::tick(uint32_t msPassed)
 {
-	ShootingAnimation::nextFrame();
+	ShootingAnimation::tick(msPassed);
 
 	if ( explosionEmitted)
 		return;
@@ -988,9 +988,9 @@ bool EffectAnimation::init()
 	return true;
 }
 
-void EffectAnimation::nextFrame()
+void EffectAnimation::tick(uint32_t msPassed)
 {
-	playEffect();
+	playEffect(msPassed);
 
 	if (effectFinished)
 	{
@@ -1020,7 +1020,7 @@ void EffectAnimation::onEffectFinished()
 	effectFinished = true;
 }
 
-void EffectAnimation::playEffect()
+void EffectAnimation::playEffect(uint32_t msPassed)
 {
 	if ( effectFinished )
 		return;
@@ -1029,7 +1029,7 @@ void EffectAnimation::playEffect()
 	{
 		if(elem.effectID == ID)
 		{
-			elem.currentFrame += AnimationControls::getSpellEffectSpeed() * GH.mainFPSmng->getElapsedMilliseconds() / 1000;
+			elem.currentFrame += AnimationControls::getSpellEffectSpeed() * msPassed / 1000;
 
 			if(elem.currentFrame >= elem.animation->size())
 			{
@@ -1113,7 +1113,7 @@ void HeroCastAnimation::emitAnimationEvent()
 	owner.executeAnimationStage(EAnimationEvents::HIT);
 }
 
-void HeroCastAnimation::nextFrame()
+void HeroCastAnimation::tick(uint32_t msPassed)
 {
 	float frame = hero->getFrame();
 

+ 10 - 10
client/battle/BattleAnimationClasses.h

@@ -48,7 +48,7 @@ public:
 
 	bool isInitialized();
 	bool tryInitialize();
-	virtual void nextFrame() {} //call every new frame
+	virtual void tick(uint32_t msPassed) {} //call every new frame
 	virtual ~BattleAnimation();
 
 	BattleAnimation(BattleInterface & owner);
@@ -120,7 +120,7 @@ class ColorTransformAnimation : public BattleStackAnimation
 	float totalProgress;
 
 	bool init() override;
-	void nextFrame() override;
+	void tick(uint32_t msPassed) override;
 
 public:
 	ColorTransformAnimation(BattleInterface & owner, const CStack * _stack, const std::string & colorFilterName, const CSpell * spell);
@@ -157,7 +157,7 @@ private:
 
 public:
 	bool init() override;
-	void nextFrame() override;
+	void tick(uint32_t msPassed) override;
 
 	MovementAnimation(BattleInterface & owner, const CStack *_stack, std::vector<BattleHex> _destTiles, int _distance);
 	~MovementAnimation();
@@ -220,7 +220,7 @@ class MeleeAttackAnimation : public AttackAnimation
 public:
 	MeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool multiAttack);
 
-	void nextFrame() override;
+	void tick(uint32_t msPassed) override;
 };
 
 
@@ -246,7 +246,7 @@ public:
 	~RangedAttackAnimation();
 
 	bool init() override;
-	void nextFrame() override;
+	void tick(uint32_t msPassed) override;
 };
 
 /// Shooting attack
@@ -275,7 +275,7 @@ public:
 	CatapultAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender, int _catapultDmg = 0);
 
 	void createProjectile(const Point & from, const Point & dest) const override;
-	void nextFrame() override;
+	void tick(uint32_t msPassed) override;
 };
 
 class CastAnimation : public RangedAttackAnimation
@@ -300,7 +300,7 @@ private:
 	int howMany;
 public:
 	bool init() override;
-	void nextFrame() override;
+	void tick(uint32_t msPassed) override;
 
 	DummyAnimation(BattleInterface & owner, int howManyFrames);
 };
@@ -324,7 +324,7 @@ class EffectAnimation : public BattleAnimation
 
 	void onEffectFinished();
 	void clearEffect();
-	void playEffect();
+	void playEffect(uint32_t msPassed);
 
 public:
 	enum EEffectFlags
@@ -349,7 +349,7 @@ public:
 	 ~EffectAnimation();
 
 	bool init() override;
-	void nextFrame() override;
+	void tick(uint32_t msPassed) override;
 };
 
 class HeroCastAnimation : public BattleAnimation
@@ -367,6 +367,6 @@ class HeroCastAnimation : public BattleAnimation
 public:
 	HeroCastAnimation(BattleInterface & owner, std::shared_ptr<BattleHero> hero, BattleHex dest, const CStack * defender, const CSpell * spell);
 
-	void nextFrame() override;
+	void tick(uint32_t msPassed) override;
 	bool init() override;
 };

+ 15 - 6
client/battle/BattleFieldController.cpp

@@ -39,7 +39,7 @@ BattleFieldController::BattleFieldController(BattleInterface & owner):
 	owner(owner)
 {
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
-	strongInterest = true;
+	setMoveEventStrongInterest(true);
 
 	//preparing cells and hexes
 	cellBorder = IImage::createFromFile("CCELLGRD.BMP", EImageBlitMode::COLORKEY);
@@ -68,8 +68,13 @@ BattleFieldController::BattleFieldController(BattleInterface & owner):
 	backgroundWithHexes = std::make_unique<Canvas>(Point(background->width(), background->height()));
 
 	updateAccessibleHexes();
-	addUsedEvents(LCLICK | RCLICK | MOVE);
+	addUsedEvents(LCLICK | RCLICK | MOVE | TIME);
+}
+
+void BattleFieldController::activate()
+{
 	LOCPLINT->cingconsole->pos = this->pos;
+	CIntObject::activate();
 }
 
 void BattleFieldController::createHeroes()
@@ -129,7 +134,7 @@ void BattleFieldController::renderBattlefield(Canvas & canvas)
 
 	renderer.execute(clippedCanvas);
 
-	owner.projectilesController->showProjectiles(clippedCanvas);
+	owner.projectilesController->render(clippedCanvas);
 }
 
 void BattleFieldController::showBackground(Canvas & canvas)
@@ -606,12 +611,16 @@ void BattleFieldController::showAll(SDL_Surface * to)
 	show(to);
 }
 
-void BattleFieldController::show(SDL_Surface * to)
+void BattleFieldController::tick(uint32_t msPassed)
 {
 	updateAccessibleHexes();
-	owner.stacksController->update();
-	owner.obstacleController->update();
+	owner.stacksController->tick(msPassed);
+	owner.obstacleController->tick(msPassed);
+	owner.projectilesController->tick(msPassed);
+}
 
+void BattleFieldController::show(SDL_Surface * to)
+{
 	Canvas canvas(to);
 	CSDL_Ext::CClipRectGuard guard(to, pos);
 

+ 2 - 0
client/battle/BattleFieldController.h

@@ -66,9 +66,11 @@ class BattleFieldController : public CIntObject
 	void mouseMoved(const Point & cursorPosition) override;
 	void clickLeft(tribool down, bool previousState) override;
 	void clickRight(tribool down, bool previousState) override;
+	void activate() override;
 
 	void showAll(SDL_Surface * to) override;
 	void show(SDL_Surface * to) override;
+	void tick(uint32_t msPassed) override;
 public:
 	BattleFieldController(BattleInterface & owner);
 

+ 5 - 4
client/battle/BattleInterface.cpp

@@ -28,8 +28,9 @@
 #include "../CPlayerInterface.h"
 #include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/WindowHandler.h"
 #include "../render/Canvas.h"
-#include "../adventureMap/CAdventureMapInterface.h"
+#include "../adventureMap/AdventureMapInterface.h"
 
 #include "../../CCallback.h"
 #include "../../lib/CStack.h"
@@ -95,7 +96,7 @@ BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet *
 	adventureInt->onAudioPaused();
 	ongoingAnimationsState.set(true);
 
-	GH.pushInt(windowObject);
+	GH.windows().pushWindow(windowObject);
 	windowObject->blockUI(true);
 	windowObject->updateQueue();
 
@@ -167,7 +168,7 @@ BattleInterface::~BattleInterface()
 void BattleInterface::redrawBattlefield()
 {
 	fieldController->redrawBackgroundWithHexes();
-	GH.totalRedraw();
+	GH.windows().totalRedraw();
 }
 
 void BattleInterface::stackReset(const CStack * stack)
@@ -328,7 +329,7 @@ void BattleInterface::battleFinished(const BattleResult& br, QueryID queryID)
 	{
 		curInt->cb->selectionMade(selection, queryID);
 	};
-	GH.pushInt(wnd);
+	GH.windows().pushWindow(wnd);
 	
 	curInt->waitWhileDialog(); // Avoid freeze when AI end turn after battle. Check bug #1897
 	CPlayerInterface::battleInt = nullptr;

+ 28 - 19
client/battle/BattleInterfaceClasses.cpp

@@ -25,6 +25,8 @@
 #include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/Shortcut.h"
+#include "../gui/MouseButton.h"
+#include "../gui/WindowHandler.h"
 #include "../render/Canvas.h"
 #include "../render/IImage.h"
 #include "../widgets/Buttons.h"
@@ -202,6 +204,25 @@ const CGHeroInstance * BattleHero::instance()
 	return hero;
 }
 
+void BattleHero::tick(uint32_t msPassed)
+{
+	size_t groupIndex = static_cast<size_t>(phase);
+
+	float timePassed = msPassed / 1000.f;
+
+	flagCurrentFrame += currentSpeed * timePassed;
+	currentFrame += currentSpeed * timePassed;
+
+	if(flagCurrentFrame >= flagAnimation->size(0))
+		flagCurrentFrame -= flagAnimation->size(0);
+
+	if(currentFrame >= animation->size(groupIndex))
+	{
+		currentFrame -= animation->size(groupIndex);
+		switchToNextPhase();
+	}
+}
+
 void BattleHero::render(Canvas & canvas)
 {
 	size_t groupIndex = static_cast<size_t>(phase);
@@ -219,20 +240,6 @@ void BattleHero::render(Canvas & canvas)
 
 	canvas.draw(flagFrame, flagPosition);
 	canvas.draw(heroFrame, heroPosition);
-
-	float timePassed = float(GH.mainFPSmng->getElapsedMilliseconds()) / 1000.f;
-
-	flagCurrentFrame += currentSpeed * timePassed;
-	currentFrame += currentSpeed * timePassed;
-
-	if(flagCurrentFrame >= flagAnimation->size(0))
-		flagCurrentFrame -= flagAnimation->size(0);
-
-	if(currentFrame >= animation->size(groupIndex))
-	{
-		currentFrame -= animation->size(groupIndex);
-		switchToNextPhase();
-	}
 }
 
 void BattleHero::pause()
@@ -284,7 +291,7 @@ void BattleHero::heroLeftClicked()
 	if(owner.getCurrentPlayerInterface()->cb->battleCanCastSpell(hero, spells::Mode::HERO) == ESpellCastProblem::OK) //check conditions
 	{
 		CCS->curh->set(Cursor::Map::POINTER);
-		GH.pushIntT<CSpellWindow>(hero, owner.getCurrentPlayerInterface());
+		GH.windows().createAndPushWindow<CSpellWindow>(hero, owner.getCurrentPlayerInterface());
 	}
 }
 
@@ -299,7 +306,7 @@ void BattleHero::heroRightClicked()
 	{
 		auto h = defender ? owner.defendingHeroInstance : owner.attackingHeroInstance;
 		targetHero.initFromHero(h, InfoAboutHero::EInfoLevel::INBATTLE);
-		GH.pushIntT<HeroInfoWindow>(targetHero, &windowPosition);
+		GH.windows().createAndPushWindow<HeroInfoWindow>(targetHero, &windowPosition);
 	}
 }
 
@@ -354,6 +361,8 @@ BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * her
 
 	switchToNextPhase();
 	play();
+
+	addUsedEvents(TIME);
 }
 
 HeroInfoWindow::HeroInfoWindow(const InfoAboutHero & hero, Point * position)
@@ -584,8 +593,8 @@ void BattleResultWindow::buttonPressed(int button)
 
 	close();
 
-	if(dynamic_cast<BattleWindow*>(GH.topInt().get()))
-		GH.popInts(1); //pop battle interface if present
+	if(GH.windows().topWindow<BattleWindow>())
+		GH.windows().popWindows(1); //pop battle interface if present
 
 	//Result window and battle interface are gone. We requested all dialogs to be closed before opening the battle,
 	//so we can be sure that there is no dialogs left on GUI stack.
@@ -684,7 +693,7 @@ std::optional<uint32_t> StackQueue::getHoveredUnitIdIfAny() const
 {
 	for(const auto & stackBox : stackBoxes)
 	{
-		if(stackBox->hovered || stackBox->mouseState(MouseButton::RIGHT))
+		if(stackBox->isHovered() || stackBox->isMouseButtonPressed(MouseButton::RIGHT))
 		{
 			return stackBox->getBoundUnitID();
 		}

+ 1 - 0
client/battle/BattleInterfaceClasses.h

@@ -114,6 +114,7 @@ public:
 	void setPhase(EHeroAnimType newPhase); //sets phase of hero animation
 
 	void collectRenderableObjects(BattleRenderer & renderer);
+	void tick(uint32_t msPassed) override;
 
 	float getFrame() const;
 	void onPhaseFinished(const std::function<void()> &);

+ 2 - 2
client/battle/BattleObstacleController.cpp

@@ -159,9 +159,9 @@ void BattleObstacleController::collectRenderableObjects(BattleRenderer & rendere
 	}
 }
 
-void BattleObstacleController::update()
+void BattleObstacleController::tick(uint32_t msPassed)
 {
-	timePassed += GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+	timePassed += msPassed / 1000.f;
 }
 
 std::shared_ptr<IImage> BattleObstacleController::getObstacleImage(const CObstacleInstance & oi)

+ 1 - 1
client/battle/BattleObstacleController.h

@@ -50,7 +50,7 @@ public:
 	BattleObstacleController(BattleInterface & owner);
 
 	/// called every frame
-	void update();
+	void tick(uint32_t msPassed);
 
 	/// call-in from network pack, add newly placed obstacles with any required animations
 	void obstaclePlaced(const std::vector<std::shared_ptr<const CObstacleInstance>> & oi);

+ 28 - 10
client/battle/BattleProjectileController.cpp

@@ -58,15 +58,18 @@ void ProjectileMissile::show(Canvas & canvas)
 
 		canvas.draw(image, pos);
 	}
+}
 
-	float timePassed = GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+void ProjectileMissile::tick(uint32_t msPassed)
+{
+	float timePassed = msPassed / 1000.f;
 	progress += timePassed * speed;
 }
 
-void ProjectileAnimatedMissile::show(Canvas & canvas)
+void ProjectileAnimatedMissile::tick(uint32_t msPassed)
 {
-	ProjectileMissile::show(canvas);
-	frameProgress += AnimationControls::getSpellEffectSpeed() * GH.mainFPSmng->getElapsedMilliseconds() / 1000;
+	ProjectileMissile::tick(msPassed);
+	frameProgress += AnimationControls::getSpellEffectSpeed() * msPassed / 1000;
 	size_t animationSize = animation->size(reverse ? 1 : 0);
 	while (frameProgress > animationSize)
 		frameProgress -= animationSize;
@@ -74,9 +77,15 @@ void ProjectileAnimatedMissile::show(Canvas & canvas)
 	frameNum = std::floor(frameProgress);
 }
 
+void ProjectileCatapult::tick(uint32_t msPassed)
+{
+	frameProgress += AnimationControls::getSpellEffectSpeed() * msPassed / 1000;
+	float timePassed = msPassed / 1000.f;
+	progress += timePassed * speed;
+}
+
 void ProjectileCatapult::show(Canvas & canvas)
 {
-	frameProgress += AnimationControls::getSpellEffectSpeed() * GH.mainFPSmng->getElapsedMilliseconds() / 1000;
 	int frameCounter = std::floor(frameProgress);
 	int frameIndex = (frameCounter + 1) % animation->size(0);
 
@@ -90,9 +99,6 @@ void ProjectileCatapult::show(Canvas & canvas)
 
 		canvas.draw(image, pos);
 	}
-
-	float timePassed = GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
-	progress += timePassed * speed;
 }
 
 void ProjectileRay::show(Canvas & canvas)
@@ -135,8 +141,11 @@ void ProjectileRay::show(Canvas & canvas)
 			canvas.drawLine(Point(x1 + i, y1), Point(x2 + i, y2), ray.start, ray.end);
 		}
 	}
+}
 
-	float timePassed = GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+void ProjectileRay::tick(uint32_t msPassed)
+{
+	float timePassed = msPassed / 1000.f;
 	progress += timePassed * speed;
 }
 
@@ -217,13 +226,22 @@ void BattleProjectileController::emitStackProjectile(const CStack * stack)
 	}
 }
 
-void BattleProjectileController::showProjectiles(Canvas & canvas)
+void BattleProjectileController::render(Canvas & canvas)
 {
 	for ( auto projectile: projectiles)
 	{
 		if ( projectile->playing )
 			projectile->show(canvas);
 	}
+}
+
+void BattleProjectileController::tick(uint32_t msPassed)
+{
+	for ( auto projectile: projectiles)
+	{
+		if ( projectile->playing )
+			projectile->tick(msPassed);
+	}
 
 	vstd::erase_if(projectiles, [&](const std::shared_ptr<ProjectileBase> & projectile){
 		return projectile->progress > 1.0f;

+ 9 - 2
client/battle/BattleProjectileController.h

@@ -28,6 +28,7 @@ struct ProjectileBase
 {
 	virtual ~ProjectileBase() = default;
 	virtual void show(Canvas & canvas) =  0;
+	virtual void tick(uint32_t msPassed) = 0;
 
 	Point from; // initial position on the screen
 	Point dest; // target position on the screen
@@ -42,6 +43,7 @@ struct ProjectileBase
 struct ProjectileMissile : ProjectileBase
 {
 	void show(Canvas & canvas) override;
+	void tick(uint32_t msPassed) override;
 
 	std::shared_ptr<CAnimation> animation;
 	int frameNum;  // frame to display from projectile animation
@@ -51,7 +53,7 @@ struct ProjectileMissile : ProjectileBase
 /// Projectile for spell - render animation moving in straight line from origin to destination
 struct ProjectileAnimatedMissile : ProjectileMissile
 {
-	void show(Canvas & canvas) override;
+	void tick(uint32_t msPassed) override;
 	float frameProgress;
 };
 
@@ -59,6 +61,7 @@ struct ProjectileAnimatedMissile : ProjectileMissile
 struct ProjectileCatapult : ProjectileBase
 {
 	void show(Canvas & canvas) override;
+	void tick(uint32_t msPassed) override;
 
 	std::shared_ptr<CAnimation> animation;
 	float frameProgress;
@@ -68,6 +71,7 @@ struct ProjectileCatapult : ProjectileBase
 struct ProjectileRay : ProjectileBase
 {
 	void show(Canvas & canvas) override;
+	void tick(uint32_t msPassed) override;
 
 	std::vector<CCreature::CreatureAnimation::RayColor> rayConfig;
 };
@@ -102,7 +106,10 @@ public:
 	BattleProjectileController(BattleInterface & owner);
 
 	/// renders all currently active projectiles
-	void showProjectiles(Canvas & canvas);
+	void render(Canvas & canvas);
+
+	/// updates positioning / animations of all projectiles
+	void tick(uint32_t msPassed);
 
 	/// returns true if stack has projectile that is yet to hit target
 	bool hasActiveProjectile(const CStack * stack, bool emittedOnly) const;

+ 15 - 7
client/battle/BattleStacksController.cpp

@@ -335,13 +335,12 @@ void BattleStacksController::showStack(Canvas & canvas, const CStack * stack)
 	}
 
 	stackAnimation[stack->unitId()]->nextFrame(canvas, fullFilter, facingRight(stack)); // do actual blit
-	stackAnimation[stack->unitId()]->incrementFrame(float(GH.mainFPSmng->getElapsedMilliseconds()) / 1000);
 }
 
-void BattleStacksController::update()
+void BattleStacksController::tick(uint32_t msPassed)
 {
 	updateHoveredStacks();
-	updateBattleAnimations();
+	updateBattleAnimations(msPassed);
 }
 
 void BattleStacksController::initializeBattleAnimations()
@@ -352,21 +351,30 @@ void BattleStacksController::initializeBattleAnimations()
 			elem->tryInitialize();
 }
 
-void BattleStacksController::stepFrameBattleAnimations()
+void BattleStacksController::tickFrameBattleAnimations(uint32_t msPassed)
 {
+	for (auto stack : owner.curInt->cb->battleGetAllStacks(true))
+	{
+		if (stackAnimation.find(stack->unitId()) == stackAnimation.end()) //e.g. for summoned but not yet handled stacks
+			continue;
+
+		stackAnimation[stack->unitId()]->incrementFrame(msPassed / 1000.f);
+	}
+
 	// operate on copy - to prevent potential iterator invalidation due to push_back's
 	// FIXME? : remove remaining calls to addNewAnim from BattleAnimation::nextFrame (only Catapult explosion at the time of writing)
+
 	auto copiedVector = currentAnimations;
 	for (auto & elem : copiedVector)
 		if (elem && elem->isInitialized())
-			elem->nextFrame();
+			elem->tick(msPassed);
 }
 
-void BattleStacksController::updateBattleAnimations()
+void BattleStacksController::updateBattleAnimations(uint32_t msPassed)
 {
 	bool hadAnimations = !currentAnimations.empty();
 	initializeBattleAnimations();
-	stepFrameBattleAnimations();
+	tickFrameBattleAnimations(msPassed);
 	vstd::erase(currentAnimations, nullptr);
 
 	if (hadAnimations && currentAnimations.empty())

+ 3 - 3
client/battle/BattleStacksController.h

@@ -91,9 +91,9 @@ class BattleStacksController
 	void removeExpiredColorFilters();
 
 	void initializeBattleAnimations();
-	void stepFrameBattleAnimations();
+	void tickFrameBattleAnimations(uint32_t msPassed);
 
-	void updateBattleAnimations();
+	void updateBattleAnimations(uint32_t msPassed);
 	void updateHoveredStacks();
 
 	std::vector<const CStack *> selectHoveredStacks();
@@ -138,7 +138,7 @@ public:
 	const CStack* getActiveStack() const;
 	const std::vector<uint32_t> getHoveredStacksUnitIds() const;
 
-	void update();
+	void tick(uint32_t msPassed);
 
 	/// returns position of animation needed to place stack in specific hex
 	Point getStackPositionAtHex(BattleHex hexNum, const CStack * creature) const;

+ 15 - 9
client/battle/BattleWindow.cpp

@@ -22,6 +22,7 @@
 #include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/Shortcut.h"
+#include "../gui/WindowHandler.h"
 #include "../windows/CSpellWindow.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/Images.h"
@@ -72,7 +73,6 @@ BattleWindow::BattleWindow(BattleInterface & owner):
 	
 	console = widget<BattleConsole>("console");
 
-	GH.statusbar = console;
 	owner.console = console;
 
 	owner.fieldController.reset( new BattleFieldController(owner));
@@ -153,7 +153,7 @@ void BattleWindow::hideQueue()
 		pos.h -= queue->pos.h;
 		pos = center();
 	}
-	GH.totalRedraw();
+	GH.windows().totalRedraw();
 }
 
 void BattleWindow::showQueue()
@@ -166,7 +166,7 @@ void BattleWindow::showQueue()
 
 	createQueue();
 	updateQueue();
-	GH.totalRedraw();
+	GH.windows().totalRedraw();
 }
 
 void BattleWindow::updateQueue()
@@ -176,17 +176,23 @@ void BattleWindow::updateQueue()
 
 void BattleWindow::activate()
 {
-	GH.statusbar = console;
+	GH.setStatusbar(console);
 	CIntObject::activate();
 	LOCPLINT->cingconsole->activate();
 }
 
 void BattleWindow::deactivate()
 {
+	GH.setStatusbar(nullptr);
 	CIntObject::deactivate();
 	LOCPLINT->cingconsole->deactivate();
 }
 
+bool BattleWindow::captureThisKey(EShortcut key)
+{
+	return owner.openingPlaying();
+}
+
 void BattleWindow::keyPressed(EShortcut key)
 {
 	if (owner.openingPlaying())
@@ -252,7 +258,7 @@ void BattleWindow::bOptionsf()
 
 	CCS->curh->set(Cursor::Map::POINTER);
 
-	GH.pushIntT<SettingsMainWindow>(&owner);
+	GH.windows().createAndPushWindow<SettingsMainWindow>(&owner);
 }
 
 void BattleWindow::bSurrenderf()
@@ -359,7 +365,7 @@ void BattleWindow::showAlternativeActionIcon(PossiblePlayerBattleAction action)
 	}
 		
 	auto anim = std::make_shared<CAnimation>(iconName);
-	w->setImage(anim, false);
+	w->setImage(anim);
 	w->redraw();
 }
 
@@ -420,7 +426,7 @@ void BattleWindow::bSpellf()
 
 	if(spellCastProblem == ESpellCastProblem::OK)
 	{
-		GH.pushIntT<CSpellWindow>(myHero, owner.curInt.get());
+		GH.windows().createAndPushWindow<CSpellWindow>(myHero, owner.curInt.get());
 	}
 	else if (spellCastProblem == ESpellCastProblem::MAGIC_IS_BLOCKED)
 	{
@@ -565,7 +571,7 @@ void BattleWindow::show(SDL_Surface *to)
 
 void BattleWindow::close()
 {
-	if(GH.topInt().get() != this)
+	if(!GH.windows().isTopWindow(this))
 		logGlobal->error("Only top interface must be closed");
-	GH.popInts(1);
+	GH.windows().popWindows(1);
 }

+ 1 - 0
client/battle/BattleWindow.h

@@ -85,6 +85,7 @@ public:
 	void activate() override;
 	void deactivate() override;
 	void keyPressed(EShortcut key) override;
+	bool captureThisKey(EShortcut key) override;
 	void clickRight(tribool down, bool previousState) override;
 	void show(SDL_Surface *to) override;
 	void showAll(SDL_Surface *to) override;

+ 269 - 0
client/eventsSDL/InputHandler.cpp

@@ -0,0 +1,269 @@
+/*
+* InputHandler.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 "InputHandler.h"
+
+#include "NotificationHandler.h"
+#include "InputSourceMouse.h"
+#include "InputSourceKeyboard.h"
+#include "InputSourceTouch.h"
+#include "InputSourceText.h"
+#include "UserEventHandler.h"
+
+#include "../gui/CGuiHandler.h"
+#include "../gui/CursorHandler.h"
+#include "../gui/EventDispatcher.h"
+#include "../gui/MouseButton.h"
+#include "../CMT.h"
+#include "../CPlayerInterface.h"
+#include "../CGameInfo.h"
+
+#include "../../lib/CConfigHandler.h"
+
+#include <SDL_events.h>
+#include <SDL_hints.h>
+
+InputHandler::InputHandler()
+	: mouseHandler(std::make_unique<InputSourceMouse>())
+	, keyboardHandler(std::make_unique<InputSourceKeyboard>())
+	, fingerHandler(std::make_unique<InputSourceTouch>())
+	, textHandler(std::make_unique<InputSourceText>())
+	, userHandler(std::make_unique<UserEventHandler>())
+	, mouseButtonsMask(0)
+	, pointerSpeedMultiplier(settings["general"]["relativePointerSpeedMultiplier"].Float())
+{
+}
+
+InputHandler::~InputHandler() = default;
+
+void InputHandler::handleCurrentEvent(const SDL_Event & current)
+{
+	switch (current.type)
+	{
+		case SDL_KEYDOWN:
+			return keyboardHandler->handleEventKeyDown(current.key);
+		case SDL_KEYUP:
+			return keyboardHandler->handleEventKeyUp(current.key);
+		case SDL_MOUSEMOTION:
+			return mouseHandler->handleEventMouseMotion(current.motion);
+		case SDL_MOUSEBUTTONDOWN:
+			return mouseHandler->handleEventMouseButtonDown(current.button);
+		case SDL_MOUSEWHEEL:
+			return mouseHandler->handleEventMouseWheel(current.wheel);
+		case SDL_TEXTINPUT:
+			return textHandler->handleEventTextInput(current.text);
+		case SDL_TEXTEDITING:
+			return textHandler->handleEventTextEditing(current.edit);
+		case SDL_MOUSEBUTTONUP:
+			return mouseHandler->handleEventMouseButtonUp(current.button);
+		case SDL_FINGERMOTION:
+			return fingerHandler->handleEventFingerMotion(current.tfinger);
+		case SDL_FINGERDOWN:
+			return fingerHandler->handleEventFingerDown(current.tfinger);
+		case SDL_FINGERUP:
+			return fingerHandler->handleEventFingerUp(current.tfinger);
+	}
+}
+
+void InputHandler::processEvents()
+{
+	boost::unique_lock<boost::mutex> lock(eventsMutex);
+	for (auto const & currentEvent : eventsQueue)
+	{
+		if (currentEvent.type == SDL_MOUSEMOTION)
+		{
+			cursorPosition = Point(currentEvent.motion.x, currentEvent.motion.y);
+			mouseButtonsMask = currentEvent.motion.state;
+		}
+		handleCurrentEvent(currentEvent);
+	}
+	eventsQueue.clear();
+}
+
+bool InputHandler::ignoreEventsUntilInput()
+{
+	bool inputFound = false;
+
+	boost::unique_lock<boost::mutex> lock(eventsMutex);
+	for (auto const & event : eventsQueue)
+	{
+		switch(event.type)
+		{
+			case SDL_MOUSEBUTTONDOWN:
+			case SDL_FINGERDOWN:
+			case SDL_KEYDOWN:
+				inputFound = true;
+		}
+	}
+	eventsQueue.clear();
+
+	return inputFound;
+}
+
+void InputHandler::preprocessEvent(const SDL_Event & ev)
+{
+	if((ev.type==SDL_QUIT) ||(ev.type == SDL_KEYDOWN && ev.key.keysym.sym==SDLK_F4 && (ev.key.keysym.mod & KMOD_ALT)))
+	{
+#ifdef VCMI_ANDROID
+		handleQuit(false);
+#else
+		handleQuit();
+#endif
+		return;
+	}
+#ifdef VCMI_ANDROID
+	else if (ev.type == SDL_KEYDOWN && ev.key.keysym.scancode == SDL_SCANCODE_AC_BACK)
+	{
+		handleQuit(true);
+	}
+#endif
+	else if(ev.type == SDL_KEYDOWN && ev.key.keysym.sym==SDLK_F4)
+	{
+		Settings full = settings.write["video"]["fullscreen"];
+		full->Bool() = !full->Bool();
+		return;
+	}
+	else if(ev.type == SDL_USEREVENT)
+	{
+		userHandler->handleUserEvent(ev.user);
+
+		return;
+	}
+	else if(ev.type == SDL_WINDOWEVENT)
+	{
+		switch (ev.window.event) {
+		case SDL_WINDOWEVENT_RESTORED:
+#ifndef VCMI_IOS
+			{
+				boost::unique_lock<boost::recursive_mutex> lock(*CPlayerInterface::pim);
+				GH.onScreenResize();
+			}
+#endif
+			break;
+		}
+		return;
+	}
+	else if(ev.type == SDL_SYSWMEVENT)
+	{
+		if(!settings["session"]["headless"].Bool() && settings["general"]["notifications"].Bool())
+		{
+			NotificationHandler::handleSdlEvent(ev);
+		}
+	}
+
+	//preprocessing
+	if(ev.type == SDL_MOUSEMOTION)
+	{
+		if (CCS && CCS->curh)
+			CCS->curh->cursorMove(ev.motion.x, ev.motion.y);
+	}
+
+	{
+		boost::unique_lock<boost::mutex> lock(eventsMutex);
+
+		if(ev.type == SDL_MOUSEMOTION && !eventsQueue.empty() && eventsQueue.back().type == SDL_MOUSEMOTION)
+		{
+			// In a sequence of mouse motion events, skip all but the last one.
+			// This prevents freezes when every motion event takes longer to handle than interval at which
+			// the events arrive (like dragging on the minimap in world view, with redraw at every event)
+			// so that the events would start piling up faster than they can be processed.
+			eventsQueue.back() = ev;
+			return;
+		}
+		eventsQueue.push_back(ev);
+	}
+}
+
+void InputHandler::fetchEvents()
+{
+	SDL_Event ev;
+
+	while(1 == SDL_PollEvent(&ev))
+	{
+		preprocessEvent(ev);
+	}
+}
+
+bool InputHandler::isKeyboardCtrlDown() const
+{
+#ifdef VCMI_MAC
+	return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LGUI] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RGUI];
+#else
+	return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LCTRL] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RCTRL];
+#endif
+}
+
+bool InputHandler::isKeyboardAltDown() const
+{
+	return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LALT] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RALT];
+}
+
+bool InputHandler::isKeyboardShiftDown() const
+{
+	return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LSHIFT] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RSHIFT];
+}
+
+
+void InputHandler::fakeMoveCursor(float dx, float dy)
+{
+	int x, y, w, h;
+
+	SDL_Event event;
+	SDL_MouseMotionEvent sme = {SDL_MOUSEMOTION, 0, 0, 0, 0, 0, 0, 0, 0};
+
+	sme.state = SDL_GetMouseState(&x, &y);
+	SDL_GetWindowSize(mainWindow, &w, &h);
+
+	sme.x = GH.getCursorPosition().x + (int)(pointerSpeedMultiplier * w * dx);
+	sme.y = GH.getCursorPosition().y + (int)(pointerSpeedMultiplier * h * dy);
+
+	vstd::abetween(sme.x, 0, w);
+	vstd::abetween(sme.y, 0, h);
+
+	event.motion = sme;
+	SDL_PushEvent(&event);
+}
+
+void InputHandler::startTextInput(const Rect & where)
+{
+	textHandler->startTextInput(where);
+}
+
+void InputHandler::stopTextInput()
+{
+	textHandler->stopTextInput();
+}
+
+bool InputHandler::isMouseButtonPressed(MouseButton button) const
+{
+	static_assert(static_cast<uint32_t>(MouseButton::LEFT)   == SDL_BUTTON_LEFT,   "mismatch between VCMI and SDL enum!");
+	static_assert(static_cast<uint32_t>(MouseButton::MIDDLE) == SDL_BUTTON_MIDDLE, "mismatch between VCMI and SDL enum!");
+	static_assert(static_cast<uint32_t>(MouseButton::RIGHT)  == SDL_BUTTON_RIGHT,  "mismatch between VCMI and SDL enum!");
+	static_assert(static_cast<uint32_t>(MouseButton::EXTRA1) == SDL_BUTTON_X1,     "mismatch between VCMI and SDL enum!");
+	static_assert(static_cast<uint32_t>(MouseButton::EXTRA2) == SDL_BUTTON_X2,     "mismatch between VCMI and SDL enum!");
+
+	uint32_t index = static_cast<uint32_t>(button);
+	return mouseButtonsMask & SDL_BUTTON(index);
+}
+
+void InputHandler::pushUserEvent(EUserEvent usercode, void * userdata)
+{
+	SDL_Event event;
+	event.type = SDL_USEREVENT;
+	event.user.code = static_cast<int32_t>(usercode);
+	event.user.data1 = userdata;
+	SDL_PushEvent(&event);
+}
+
+const Point & InputHandler::getCursorPosition() const
+{
+	return cursorPosition;
+}

+ 77 - 0
client/eventsSDL/InputHandler.h

@@ -0,0 +1,77 @@
+/*
+* InputHandler.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 "../lib/Rect.h"
+
+enum class EUserEvent;
+enum class MouseButton;
+union SDL_Event;
+
+class InputSourceMouse;
+class InputSourceKeyboard;
+class InputSourceTouch;
+class InputSourceText;
+class UserEventHandler;
+
+class InputHandler
+{
+	std::vector<SDL_Event> eventsQueue;
+	boost::mutex eventsMutex;
+
+	Point cursorPosition;
+	float pointerSpeedMultiplier;
+	int mouseButtonsMask;
+
+	void preprocessEvent(const SDL_Event & event);
+	void handleCurrentEvent(const SDL_Event & current);
+
+	std::unique_ptr<InputSourceMouse> mouseHandler;
+	std::unique_ptr<InputSourceKeyboard> keyboardHandler;
+	std::unique_ptr<InputSourceTouch> fingerHandler;
+	std::unique_ptr<InputSourceText> textHandler;
+	std::unique_ptr<UserEventHandler> userHandler;
+
+public:
+	InputHandler();
+	~InputHandler();
+
+	/// Fetches events from SDL input system and prepares them for processing
+	void fetchEvents();
+	/// Performs actual processing and dispatching of previously fetched events
+	void processEvents();
+
+	/// drops all incoming events without processing them
+	/// returns true if input event has been found
+	bool ignoreEventsUntilInput();
+
+	void fakeMoveCursor(float dx, float dy);
+
+	/// Initiates text input in selected area, potentially creating IME popup (mobile systems only at the moment)
+	void startTextInput(const Rect & where);
+
+	/// Ends any existing text input state
+	void stopTextInput();
+
+	/// Returns true if selected mouse button is pressed at the moment
+	bool isMouseButtonPressed(MouseButton button) const;
+
+	/// Generates new user event that will be processed on next frame
+	void pushUserEvent(EUserEvent usercode, void * userdata);
+
+	/// Returns current position of cursor, in VCMI logical screen coordinates
+	const Point & getCursorPosition() const;
+
+	/// returns true if chosen keyboard key is currently pressed down
+	bool isKeyboardAltDown() const;
+	bool isKeyboardCtrlDown() const;
+	bool isKeyboardShiftDown() const;
+};

+ 85 - 0
client/eventsSDL/InputSourceKeyboard.cpp

@@ -0,0 +1,85 @@
+/*
+* InputSourceKeyboard.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 "InputSourceKeyboard.h"
+
+#include "../../lib/CConfigHandler.h"
+#include "../CPlayerInterface.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/EventDispatcher.h"
+#include "../gui/ShortcutHandler.h"
+
+#include <SDL_events.h>
+#include <SDL_hints.h>
+
+InputSourceKeyboard::InputSourceKeyboard()
+{
+#ifdef VCMI_MAC
+	// Ctrl+click should be treated as a right click on Mac OS X
+	SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, "1");
+#endif
+}
+
+void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key)
+{
+	if(key.repeat != 0)
+		return; // ignore periodic event resends
+
+	assert(key.state == SDL_PRESSED);
+
+	if(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
+		Settings s = settings.write["session"];
+		switch(key.keysym.sym)
+		{
+			case SDLK_F5:
+				if(settings["session"]["spectate-locked-pim"].Bool())
+					CPlayerInterface::pim->unlock();
+				else
+					CPlayerInterface::pim->lock();
+				s["spectate-locked-pim"].Bool() = !settings["session"]["spectate-locked-pim"].Bool();
+				break;
+
+			case SDLK_F6:
+				s["spectate-ignore-hero"].Bool() = !settings["session"]["spectate-ignore-hero"].Bool();
+				break;
+
+			case SDLK_F7:
+				s["spectate-skip-battle"].Bool() = !settings["session"]["spectate-skip-battle"].Bool();
+				break;
+
+			case SDLK_F8:
+				s["spectate-skip-battle-result"].Bool() = !settings["session"]["spectate-skip-battle-result"].Bool();
+				break;
+
+			default:
+				break;
+		}
+		return;
+	}
+
+	auto shortcutsVector = GH.shortcuts().translateKeycode(key.keysym.sym);
+
+	GH.events().dispatchShortcutPressed(shortcutsVector);
+}
+
+void InputSourceKeyboard::handleEventKeyUp(const SDL_KeyboardEvent & key)
+{
+	if(key.repeat != 0)
+		return; // ignore periodic event resends
+
+	assert(key.state == SDL_RELEASED);
+
+	auto shortcutsVector = GH.shortcuts().translateKeycode(key.keysym.sym);
+
+	GH.events().dispatchShortcutReleased(shortcutsVector);
+}

+ 23 - 0
client/eventsSDL/InputSourceKeyboard.h

@@ -0,0 +1,23 @@
+/*
+* InputSourceKeyboard.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
+
+struct SDL_KeyboardEvent;
+
+/// Class that handles keyboard input from SDL events
+class InputSourceKeyboard
+{
+public:
+	InputSourceKeyboard();
+
+	void handleEventKeyDown(const SDL_KeyboardEvent & current);
+	void handleEventKeyUp(const SDL_KeyboardEvent & current);
+};

+ 72 - 0
client/eventsSDL/InputSourceMouse.cpp

@@ -0,0 +1,72 @@
+/*
+* InputSourceMouse.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 "InputSourceMouse.h"
+
+#include "../../lib/Point.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/EventDispatcher.h"
+#include "../gui/MouseButton.h"
+
+#include <SDL_events.h>
+
+void InputSourceMouse::handleEventMouseMotion(const SDL_MouseMotionEvent & motion)
+{
+	GH.events().dispatchMouseMoved(Point(motion.x, motion.y));
+}
+
+void InputSourceMouse::handleEventMouseButtonDown(const SDL_MouseButtonEvent & button)
+{
+	Point position(button.x, button.y);
+
+	switch(button.button)
+	{
+		case SDL_BUTTON_LEFT:
+			if(button.clicks > 1)
+				GH.events().dispatchMouseDoubleClick(position);
+			else
+				GH.events().dispatchMouseButtonPressed(MouseButton::LEFT, position);
+			break;
+		case SDL_BUTTON_RIGHT:
+			GH.events().dispatchMouseButtonPressed(MouseButton::RIGHT, position);
+			break;
+		case SDL_BUTTON_MIDDLE:
+			GH.events().dispatchMouseButtonPressed(MouseButton::MIDDLE, position);
+			break;
+	}
+}
+
+void InputSourceMouse::handleEventMouseWheel(const SDL_MouseWheelEvent & wheel)
+{
+	// SDL doesn't have the proper values for mouse positions on SDL_MOUSEWHEEL, refetch them
+	int x = 0, y = 0;
+	SDL_GetMouseState(&x, &y);
+
+	GH.events().dispatchMouseScrolled(Point(wheel.x, wheel.y), Point(x, y));
+}
+
+void InputSourceMouse::handleEventMouseButtonUp(const SDL_MouseButtonEvent & button)
+{
+	Point position(button.x, button.y);
+
+	switch(button.button)
+	{
+		case SDL_BUTTON_LEFT:
+			GH.events().dispatchMouseButtonReleased(MouseButton::LEFT, position);
+			break;
+		case SDL_BUTTON_RIGHT:
+			GH.events().dispatchMouseButtonReleased(MouseButton::RIGHT, position);
+			break;
+		case SDL_BUTTON_MIDDLE:
+			GH.events().dispatchMouseButtonReleased(MouseButton::MIDDLE, position);
+			break;
+	}
+}

+ 25 - 0
client/eventsSDL/InputSourceMouse.h

@@ -0,0 +1,25 @@
+/*
+* InputSourceMouse.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
+
+struct SDL_MouseWheelEvent;
+struct SDL_MouseMotionEvent;
+struct SDL_MouseButtonEvent;
+
+/// Class that handles mouse input from SDL events
+class InputSourceMouse
+{
+public:
+	void handleEventMouseMotion(const SDL_MouseMotionEvent & current);
+	void handleEventMouseButtonDown(const SDL_MouseButtonEvent & current);
+	void handleEventMouseWheel(const SDL_MouseWheelEvent & current);
+	void handleEventMouseButtonUp(const SDL_MouseButtonEvent & current);
+};

+ 92 - 0
client/eventsSDL/InputSourceText.cpp

@@ -0,0 +1,92 @@
+/*
+* InputSourceText.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 "InputSourceText.h"
+
+#include "../CMT.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/EventDispatcher.h"
+
+#include "../../lib/Rect.h"
+
+#include <SDL_events.h>
+#include <SDL_render.h>
+
+#ifdef VCMI_APPLE
+#	include <dispatch/dispatch.h>
+#endif
+
+#ifdef VCMI_IOS
+#	include "ios/utils.h"
+#endif
+
+void InputSourceText::handleEventTextInput(const SDL_TextInputEvent & text)
+{
+	GH.events().dispatchTextInput(text.text);
+}
+
+void InputSourceText::handleEventTextEditing(const SDL_TextEditingEvent & text)
+{
+	GH.events().dispatchTextEditing(text.text);
+}
+
+void InputSourceText::startTextInput(const Rect & whereInput)
+{
+#ifdef VCMI_APPLE
+	dispatch_async(dispatch_get_main_queue(), ^{
+#endif
+
+	// TODO ios: looks like SDL bug actually, try fixing there
+	auto renderer = SDL_GetRenderer(mainWindow);
+	float scaleX, scaleY;
+	SDL_Rect viewport;
+	SDL_RenderGetScale(renderer, &scaleX, &scaleY);
+	SDL_RenderGetViewport(renderer, &viewport);
+
+#ifdef VCMI_IOS
+	const auto nativeScale = iOS_utils::screenScale();
+	scaleX /= nativeScale;
+	scaleY /= nativeScale;
+#endif
+
+	SDL_Rect rectInScreenCoordinates;
+	rectInScreenCoordinates.x = (viewport.x + whereInput.x) * scaleX;
+	rectInScreenCoordinates.y = (viewport.y + whereInput.y) * scaleY;
+	rectInScreenCoordinates.w = whereInput.w * scaleX;
+	rectInScreenCoordinates.h = whereInput.h * scaleY;
+
+	SDL_SetTextInputRect(&rectInScreenCoordinates);
+
+	if (SDL_IsTextInputActive() == SDL_FALSE)
+	{
+		SDL_StartTextInput();
+	}
+
+#ifdef VCMI_APPLE
+	});
+#endif
+}
+
+void InputSourceText::stopTextInput()
+{
+#ifdef VCMI_APPLE
+	dispatch_async(dispatch_get_main_queue(), ^{
+#endif
+
+	if (SDL_IsTextInputActive() == SDL_TRUE)
+	{
+		SDL_StopTextInput();
+	}
+
+#ifdef VCMI_APPLE
+	});
+#endif
+}

+ 29 - 0
client/eventsSDL/InputSourceText.h

@@ -0,0 +1,29 @@
+/*
+* InputSourceText.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
+
+VCMI_LIB_NAMESPACE_BEGIN
+class Rect;
+VCMI_LIB_NAMESPACE_END
+
+struct SDL_TextEditingEvent;
+struct SDL_TextInputEvent;
+
+/// Class that handles text input (e.g. IME or direct input from physical keyboard) from SDL events
+class InputSourceText
+{
+public:
+	void handleEventTextInput(const SDL_TextInputEvent & current);
+	void handleEventTextEditing(const SDL_TextEditingEvent & current);
+
+	void startTextInput(const Rect & where);
+	void stopTextInput();
+};

+ 135 - 0
client/eventsSDL/InputSourceTouch.cpp

@@ -0,0 +1,135 @@
+/*
+* InputSourceTouch.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 "InputSourceTouch.h"
+
+#include "InputHandler.h"
+
+#include "../../lib/CConfigHandler.h"
+#include "../CMT.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/EventDispatcher.h"
+#include "../gui/MouseButton.h"
+
+#include <SDL_events.h>
+#include <SDL_render.h>
+#include <SDL_hints.h>
+
+InputSourceTouch::InputSourceTouch()
+	: multifinger(false)
+	, isPointerRelativeMode(settings["general"]["userRelativePointer"].Bool())
+{
+	if(isPointerRelativeMode)
+	{
+		SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0");
+		SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0");
+	}
+}
+
+void InputSourceTouch::handleEventFingerMotion(const SDL_TouchFingerEvent & tfinger)
+{
+	if(isPointerRelativeMode)
+	{
+		GH.input().fakeMoveCursor(tfinger.dx, tfinger.dy);
+	}
+}
+
+void InputSourceTouch::handleEventFingerDown(const SDL_TouchFingerEvent & tfinger)
+{
+	auto fingerCount = SDL_GetNumTouchFingers(tfinger.touchId);
+
+	multifinger = fingerCount > 1;
+
+	if(isPointerRelativeMode)
+	{
+		if(tfinger.x > 0.5)
+		{
+			bool isRightClick = tfinger.y < 0.5;
+
+			fakeMouseButtonEventRelativeMode(true, isRightClick);
+		}
+	}
+#ifndef VCMI_IOS
+	else if(fingerCount == 2)
+	{
+		Point position = convertTouchToMouse(tfinger);
+
+		GH.events().dispatchMouseMoved(position);
+		GH.events().dispatchMouseButtonPressed(MouseButton::RIGHT, position);
+	}
+#endif //VCMI_IOS
+}
+
+void InputSourceTouch::handleEventFingerUp(const SDL_TouchFingerEvent & tfinger)
+{
+#ifndef VCMI_IOS
+	auto fingerCount = SDL_GetNumTouchFingers(tfinger.touchId);
+#endif //VCMI_IOS
+
+	if(isPointerRelativeMode)
+	{
+		if(tfinger.x > 0.5)
+		{
+			bool isRightClick = tfinger.y < 0.5;
+
+			fakeMouseButtonEventRelativeMode(false, isRightClick);
+		}
+	}
+#ifndef VCMI_IOS
+	else if(multifinger)
+	{
+		Point position = convertTouchToMouse(tfinger);
+		GH.events().dispatchMouseMoved(position);
+		GH.events().dispatchMouseButtonReleased(MouseButton::RIGHT, position);
+		multifinger = fingerCount != 0;
+	}
+#endif //VCMI_IOS
+}
+
+Point InputSourceTouch::convertTouchToMouse(const SDL_TouchFingerEvent & tfinger)
+{
+	return Point(tfinger.x * GH.screenDimensions().x, tfinger.y * GH.screenDimensions().y);
+}
+
+void InputSourceTouch::fakeMouseButtonEventRelativeMode(bool down, bool right)
+{
+	SDL_Event event;
+	SDL_MouseButtonEvent sme = {SDL_MOUSEBUTTONDOWN, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+
+	if(!down)
+	{
+		sme.type = SDL_MOUSEBUTTONUP;
+	}
+
+	sme.button = right ? SDL_BUTTON_RIGHT : SDL_BUTTON_LEFT;
+
+	sme.x = GH.getCursorPosition().x;
+	sme.y = GH.getCursorPosition().y;
+
+	float xScale, yScale;
+	int w, h, rLogicalWidth, rLogicalHeight;
+
+	SDL_GetWindowSize(mainWindow, &w, &h);
+	SDL_RenderGetLogicalSize(mainRenderer, &rLogicalWidth, &rLogicalHeight);
+	SDL_RenderGetScale(mainRenderer, &xScale, &yScale);
+
+	SDL_EventState(SDL_MOUSEMOTION, SDL_IGNORE);
+	moveCursorToPosition(Point((int)(sme.x * xScale) + (w - rLogicalWidth * xScale) / 2, (int)(sme.y * yScale + (h - rLogicalHeight * yScale) / 2)));
+	SDL_EventState(SDL_MOUSEMOTION, SDL_ENABLE);
+
+	event.button = sme;
+	SDL_PushEvent(&event);
+}
+
+void InputSourceTouch::moveCursorToPosition(const Point & position)
+{
+	SDL_WarpMouseInWindow(mainWindow, position.x, position.y);
+}

+ 37 - 0
client/eventsSDL/InputSourceTouch.h

@@ -0,0 +1,37 @@
+/*
+* InputSourceTouch.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
+
+VCMI_LIB_NAMESPACE_BEGIN
+class Point;
+VCMI_LIB_NAMESPACE_END
+
+struct SDL_TouchFingerEvent;
+
+/// Class that handles touchscreen input from SDL events
+class InputSourceTouch
+{
+	bool multifinger;
+	bool isPointerRelativeMode;
+
+	/// moves mouse pointer into specified position inside vcmi window
+	void moveCursorToPosition(const Point & position);
+	Point convertTouchToMouse(const SDL_TouchFingerEvent & current);
+
+	void fakeMouseButtonEventRelativeMode(bool down, bool right);
+
+public:
+	InputSourceTouch();
+
+	void handleEventFingerMotion(const SDL_TouchFingerEvent & current);
+	void handleEventFingerDown(const SDL_TouchFingerEvent & current);
+	void handleEventFingerUp(const SDL_TouchFingerEvent & current);
+};

+ 2 - 2
client/gui/NotificationHandler.cpp → client/eventsSDL/NotificationHandler.cpp

@@ -10,11 +10,11 @@
 
 #include "StdInc.h"
 #include "NotificationHandler.h"
-#include <SDL_video.h>
-#include <SDL_events.h>
 
 #if defined(VCMI_WINDOWS)
 #include <SDL_syswm.h>
+#include <SDL_video.h>
+#include <SDL_events.h>
 
 #define WIN32_LEAN_AND_MEAN		// Exclude rarely-used stuff from Windows headers
 // Windows Header Files:

+ 0 - 0
client/gui/NotificationHandler.h → client/eventsSDL/NotificationHandler.h


+ 87 - 0
client/eventsSDL/UserEventHandler.cpp

@@ -0,0 +1,87 @@
+/*
+* EventHandlerSDLUser.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 "UserEventHandler.h"
+
+#include "../CMT.h"
+#include "../CPlayerInterface.h"
+#include "../CServerHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/WindowHandler.h"
+#include "../mainmenu/CMainMenu.h"
+#include "../mainmenu/CPrologEpilogVideo.h"
+
+#include <SDL_events.h>
+
+void UserEventHandler::handleUserEvent(const SDL_UserEvent & user)
+{
+	switch(static_cast<EUserEvent>(user.code))
+	{
+		case EUserEvent::FORCE_QUIT:
+		{
+			handleQuit(false);
+			return;
+		}
+		break;
+		case EUserEvent::RETURN_TO_MAIN_MENU:
+		{
+			CSH->endGameplay();
+			GH.defActionsDef = 63;
+			CMM->menu->switchToTab("main");
+		}
+		break;
+		case EUserEvent::RESTART_GAME:
+		{
+			CSH->sendRestartGame();
+		}
+		break;
+		case EUserEvent::CAMPAIGN_START_SCENARIO:
+		{
+			CSH->campaignServerRestartLock.set(true);
+			CSH->endGameplay();
+			auto ourCampaign = std::shared_ptr<CCampaignState>(reinterpret_cast<CCampaignState *>(user.data1));
+			auto & epilogue = ourCampaign->camp->scenarios[ourCampaign->mapsConquered.back()].epilog;
+			auto finisher = [=]()
+			{
+				if(!ourCampaign->mapsRemaining.empty())
+				{
+					GH.windows().pushWindow(CMM);
+					GH.windows().pushWindow(CMM->menu);
+					CMM->openCampaignLobby(ourCampaign);
+				}
+			};
+			if(epilogue.hasPrologEpilog)
+			{
+				GH.windows().createAndPushWindow<CPrologEpilogVideo>(epilogue, finisher);
+			}
+			else
+			{
+				CSH->campaignServerRestartLock.waitUntil(false);
+				finisher();
+			}
+		}
+		break;
+		case EUserEvent::RETURN_TO_MENU_LOAD:
+			CSH->endGameplay();
+			GH.defActionsDef = 63;
+			CMM->menu->switchToTab("load");
+			break;
+		case EUserEvent::FULLSCREEN_TOGGLED:
+		{
+			boost::unique_lock<boost::recursive_mutex> lock(*CPlayerInterface::pim);
+			GH.onScreenResize();
+			break;
+		}
+		default:
+			logGlobal->error("Unknown user event. Code %d", user.code);
+			break;
+	}
+}

+ 20 - 0
client/eventsSDL/UserEventHandler.h

@@ -0,0 +1,20 @@
+/*
+* EventHandlerSDLUser.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
+
+struct SDL_UserEvent;
+
+/// Class for handling events of type SDL_UserEvent
+class UserEventHandler
+{
+public:
+	void handleUserEvent(const SDL_UserEvent & current);
+};

+ 65 - 656
client/gui/CGuiHandler.cpp

@@ -14,10 +14,15 @@
 #include "CIntObject.h"
 #include "CursorHandler.h"
 #include "ShortcutHandler.h"
+#include "FramerateManager.h"
+#include "WindowHandler.h"
+#include "EventDispatcher.h"
+#include "../eventsSDL/InputHandler.h"
 
 #include "../CGameInfo.h"
 #include "../render/Colors.h"
 #include "../renderSDL/SDL_Extensions.h"
+#include "../renderSDL/ScreenHandler.h"
 #include "../CMT.h"
 #include "../CPlayerInterface.h"
 #include "../battle/BattleInterface.h"
@@ -26,20 +31,8 @@
 #include "../../lib/CConfigHandler.h"
 
 #include <SDL_render.h>
-#include <SDL_timer.h>
-#include <SDL_events.h>
-#include <SDL_keycode.h>
 
-#ifdef VCMI_APPLE
-#include <dispatch/dispatch.h>
-#endif
-
-#ifdef VCMI_IOS
-#include "ios/utils.h"
-#endif
-
-extern std::queue<SDL_Event> SDLEventsQueue;
-extern boost::mutex eventsM;
+CGuiHandler GH;
 
 boost::thread_specific_ptr<bool> inGuiThread;
 
@@ -72,590 +65,44 @@ SSetCaptureState::~SSetCaptureState()
 	GH.defActionsDef = prevActions;
 }
 
-static inline void
-processList(const ui16 mask, const ui16 flag, std::list<CIntObject*> *lst, std::function<void (std::list<CIntObject*> *)> cb)
-{
-	if (mask & flag)
-		cb(lst);
-}
-
-void CGuiHandler::processLists(const ui16 activityFlag, std::function<void (std::list<CIntObject*> *)> cb)
-{
-	processList(CIntObject::LCLICK,activityFlag,&lclickable,cb);
-	processList(CIntObject::RCLICK,activityFlag,&rclickable,cb);
-	processList(CIntObject::MCLICK,activityFlag,&mclickable,cb);
-	processList(CIntObject::HOVER,activityFlag,&hoverable,cb);
-	processList(CIntObject::MOVE,activityFlag,&motioninterested,cb);
-	processList(CIntObject::KEYBOARD,activityFlag,&keyinterested,cb);
-	processList(CIntObject::TIME,activityFlag,&timeinterested,cb);
-	processList(CIntObject::WHEEL,activityFlag,&wheelInterested,cb);
-	processList(CIntObject::DOUBLECLICK,activityFlag,&doubleClickInterested,cb);
-	processList(CIntObject::TEXTINPUT,activityFlag,&textInterested,cb);
-}
-
 void CGuiHandler::init()
 {
+	inputHandlerInstance = std::make_unique<InputHandler>();
+	eventDispatcherInstance = std::make_unique<EventDispatcher>();
+	windowHandlerInstance = std::make_unique<WindowHandler>();
+	screenHandlerInstance = std::make_unique<ScreenHandler>();
 	shortcutsHandlerInstance = std::make_unique<ShortcutHandler>();
-	mainFPSmng = new CFramerateManager();
-	mainFPSmng->init(settings["video"]["targetfps"].Integer());
-	isPointerRelativeMode = settings["general"]["userRelativePointer"].Bool();
-	pointerSpeedMultiplier = settings["general"]["relativePointerSpeedMultiplier"].Float();
-}
-
-void CGuiHandler::handleElementActivate(CIntObject * elem, ui16 activityFlag)
-{
-	processLists(activityFlag,[&](std::list<CIntObject*> * lst){
-		lst->push_front(elem);
-	});
-	elem->active_m |= activityFlag;
-}
-
-void CGuiHandler::handleElementDeActivate(CIntObject * elem, ui16 activityFlag)
-{
-	processLists(activityFlag,[&](std::list<CIntObject*> * lst){
-		auto hlp = std::find(lst->begin(),lst->end(),elem);
-		assert(hlp != lst->end());
-		lst->erase(hlp);
-	});
-	elem->active_m &= ~activityFlag;
-}
-
-void CGuiHandler::popInt(std::shared_ptr<IShowActivatable> top)
-{
-	assert(listInt.front() == top);
-	top->deactivate();
-	disposed.push_back(top);
-	listInt.pop_front();
-	objsToBlit -= top;
-	if(!listInt.empty())
-		listInt.front()->activate();
-	totalRedraw();
-}
-
-void CGuiHandler::pushInt(std::shared_ptr<IShowActivatable> newInt)
-{
-	assert(newInt);
-	assert(!vstd::contains(listInt, newInt)); // do not add same object twice
-
-	//a new interface will be present, we'll need to use buffer surface (unless it's advmapint that will alter screenBuf on activate anyway)
-	screenBuf = screen2;
-
-	if(!listInt.empty())
-		listInt.front()->deactivate();
-	listInt.push_front(newInt);
-	CCS->curh->set(Cursor::Map::POINTER);
-	newInt->activate();
-	objsToBlit.push_back(newInt);
-	totalRedraw();
-}
-
-void CGuiHandler::popInts(int howMany)
-{
-	if(!howMany) return; //senseless but who knows...
-
-	assert(listInt.size() >= howMany);
-	listInt.front()->deactivate();
-	for(int i=0; i < howMany; i++)
-	{
-		objsToBlit -= listInt.front();
-		disposed.push_back(listInt.front());
-		listInt.pop_front();
-	}
-
-	if(!listInt.empty())
-	{
-		listInt.front()->activate();
-		totalRedraw();
-	}
-	fakeMouseMove();
-}
-
-std::shared_ptr<IShowActivatable> CGuiHandler::topInt()
-{
-	if(listInt.empty())
-		return std::shared_ptr<IShowActivatable>();
-	else
-		return listInt.front();
-}
-
-void CGuiHandler::totalRedraw()
-{
-#ifdef VCMI_ANDROID
-	SDL_FillRect(screen2, NULL, SDL_MapRGB(screen2->format, 0, 0, 0));
-#endif
-	for(auto & elem : objsToBlit)
-		elem->showAll(screen2);
-	CSDL_Ext::blitAt(screen2,0,0,screen);
-}
-
-void CGuiHandler::updateTime()
-{
-	int ms = mainFPSmng->getElapsedMilliseconds();
-	std::list<CIntObject*> hlp = timeinterested;
-	for (auto & elem : hlp)
-	{
-		if(!vstd::contains(timeinterested,elem)) continue;
-		(elem)->tick(ms);
-	}
+	framerateManagerInstance = std::make_unique<FramerateManager>(settings["video"]["targetfps"].Integer());
 }
 
 void CGuiHandler::handleEvents()
 {
+	events().dispatchTimer(framerate().getElapsedMilliseconds());
+
 	//player interface may want special event handling
 	if(nullptr != LOCPLINT && LOCPLINT->capturedAllEvents())
 		return;
 
-	boost::unique_lock<boost::mutex> lock(eventsM);
-	while(!SDLEventsQueue.empty())
-	{
-		continueEventHandling = true;
-		SDL_Event currentEvent = SDLEventsQueue.front();
-
-		if (currentEvent.type == SDL_MOUSEMOTION)
-		{
-			cursorPosition = Point(currentEvent.motion.x, currentEvent.motion.y);
-			mouseButtonsMask = currentEvent.motion.state;
-		}
-		SDLEventsQueue.pop();
-
-		// In a sequence of mouse motion events, skip all but the last one.
-		// This prevents freezes when every motion event takes longer to handle than interval at which
-		// the events arrive (like dragging on the minimap in world view, with redraw at every event)
-		// so that the events would start piling up faster than they can be processed.
-		if ((currentEvent.type == SDL_MOUSEMOTION) && !SDLEventsQueue.empty() && (SDLEventsQueue.front().type == SDL_MOUSEMOTION))
-			continue;
-
-		handleCurrentEvent(currentEvent);
-	}
-}
-
-void CGuiHandler::convertTouchToMouse(SDL_Event * current)
-{
-	int rLogicalWidth, rLogicalHeight;
-
-	SDL_RenderGetLogicalSize(mainRenderer, &rLogicalWidth, &rLogicalHeight);
-
-	int adjustedMouseY = (int)(current->tfinger.y * rLogicalHeight);
-	int adjustedMouseX = (int)(current->tfinger.x * rLogicalWidth);
-
-	current->button.x = adjustedMouseX;
-	current->motion.x = adjustedMouseX;
-	current->button.y = adjustedMouseY;
-	current->motion.y = adjustedMouseY;
-}
-
-void CGuiHandler::fakeMoveCursor(float dx, float dy)
-{
-	int x, y, w, h;
-
-	SDL_Event event;
-	SDL_MouseMotionEvent sme = {SDL_MOUSEMOTION, 0, 0, 0, 0, 0, 0, 0, 0};
-
-	sme.state = SDL_GetMouseState(&x, &y);
-	SDL_GetWindowSize(mainWindow, &w, &h);
-
-	sme.x = CCS->curh->position().x + (int)(GH.pointerSpeedMultiplier * w * dx);
-	sme.y = CCS->curh->position().y + (int)(GH.pointerSpeedMultiplier * h * dy);
-
-	vstd::abetween(sme.x, 0, w);
-	vstd::abetween(sme.y, 0, h);
-
-	event.motion = sme;
-	SDL_PushEvent(&event);
+	input().processEvents();
 }
 
 void CGuiHandler::fakeMouseMove()
 {
-	fakeMoveCursor(0, 0);
+	input().fakeMoveCursor(0, 0);
 }
 
 void CGuiHandler::startTextInput(const Rect & whereInput)
 {
-#ifdef VCMI_APPLE
-	dispatch_async(dispatch_get_main_queue(), ^{
-#endif
-
-	// TODO ios: looks like SDL bug actually, try fixing there
-	auto renderer = SDL_GetRenderer(mainWindow);
-	float scaleX, scaleY;
-	SDL_Rect viewport;
-	SDL_RenderGetScale(renderer, &scaleX, &scaleY);
-	SDL_RenderGetViewport(renderer, &viewport);
-
-#ifdef VCMI_IOS
-	const auto nativeScale = iOS_utils::screenScale();
-	scaleX /= nativeScale;
-	scaleY /= nativeScale;
-#endif
-
-	SDL_Rect rectInScreenCoordinates;
-	rectInScreenCoordinates.x = (viewport.x + whereInput.x) * scaleX;
-	rectInScreenCoordinates.y = (viewport.y + whereInput.y) * scaleY;
-	rectInScreenCoordinates.w = whereInput.w * scaleX;
-	rectInScreenCoordinates.h = whereInput.h * scaleY;
-
-	SDL_SetTextInputRect(&rectInScreenCoordinates);
-
-	if (SDL_IsTextInputActive() == SDL_FALSE)
-	{
-		SDL_StartTextInput();
-	}
-
-#ifdef VCMI_APPLE
-	});
-#endif
+	input().startTextInput(whereInput);
 }
 
 void CGuiHandler::stopTextInput()
 {
-#ifdef VCMI_APPLE
-	dispatch_async(dispatch_get_main_queue(), ^{
-#endif
-
-	if (SDL_IsTextInputActive() == SDL_TRUE)
-	{
-		SDL_StopTextInput();
-	}
-
-#ifdef VCMI_APPLE
-	});
-#endif
-}
-
-void CGuiHandler::fakeMouseButtonEventRelativeMode(bool down, bool right)
-{
-	SDL_Event event;
-	SDL_MouseButtonEvent sme = {SDL_MOUSEBUTTONDOWN, 0, 0, 0, 0, 0, 0, 0, 0, 0};
-
-	if(!down)
-	{
-		sme.type = SDL_MOUSEBUTTONUP;
-	}
-
-	sme.button = right ? SDL_BUTTON_RIGHT : SDL_BUTTON_LEFT;
-
-	sme.x = CCS->curh->position().x;
-	sme.y = CCS->curh->position().y;
-
-	float xScale, yScale;
-	int w, h, rLogicalWidth, rLogicalHeight;
-
-	SDL_GetWindowSize(mainWindow, &w, &h);
-	SDL_RenderGetLogicalSize(mainRenderer, &rLogicalWidth, &rLogicalHeight);
-	SDL_RenderGetScale(mainRenderer, &xScale, &yScale);
-
-	SDL_EventState(SDL_MOUSEMOTION, SDL_IGNORE);
-	moveCursorToPosition( Point(
-		(int)(sme.x * xScale) + (w - rLogicalWidth * xScale) / 2,
-		(int)(sme.y * yScale + (h - rLogicalHeight * yScale) / 2)));
-	SDL_EventState(SDL_MOUSEMOTION, SDL_ENABLE);
-
-	event.button = sme;
-	SDL_PushEvent(&event);
-}
-
-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
-			Settings s = settings.write["session"];
-			switch(key.keysym.sym)
-			{
-			case SDLK_F5:
-				if(settings["session"]["spectate-locked-pim"].Bool())
-					LOCPLINT->pim->unlock();
-				else
-					LOCPLINT->pim->lock();
-				s["spectate-locked-pim"].Bool() = !settings["session"]["spectate-locked-pim"].Bool();
-				break;
-
-			case SDLK_F6:
-				s["spectate-ignore-hero"].Bool() = !settings["session"]["spectate-ignore-hero"].Bool();
-				break;
-
-			case SDLK_F7:
-				s["spectate-skip-battle"].Bool() = !settings["session"]["spectate-skip-battle"].Bool();
-				break;
-
-			case SDLK_F8:
-				s["spectate-skip-battle-result"].Bool() = !settings["session"]["spectate-skip-battle-result"].Bool();
-				break;
-
-			case SDLK_F9:
-				//not working yet since CClient::run remain locked after BattleInterface removal
-//				if(LOCPLINT->battleInt)
-//				{
-//					GH.popInts(1);
-//					vstd::clear_pointer(LOCPLINT->battleInt);
-//				}
-				break;
-
-			default:
-				break;
-			}
-			return;
-		}
-
-		auto shortcutsVector = shortcutsHandler().translateKeycode(key.keysym.sym);
-
-		bool keysCaptured = false;
-		for(auto i = keyinterested.begin(); i != keyinterested.end() && continueEventHandling; i++)
-		{
-			for (EShortcut shortcut : shortcutsVector)
-			{
-				if((*i)->captureThisKey(shortcut))
-				{
-					keysCaptured = true;
-					break;
-				}
-			}
-		}
-
-		std::list<CIntObject*> miCopy = keyinterested;
-		for(auto i = miCopy.begin(); i != miCopy.end() && continueEventHandling; i++)
-		{
-			for (EShortcut shortcut : shortcutsVector)
-			{
-				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)
-	{
-		handleMouseMotion(current);
-	}
-	else if(current.type == SDL_MOUSEBUTTONDOWN)
-	{
-		switch(current.button.button)
-		{
-		case SDL_BUTTON_LEFT:
-		{
-			auto doubleClicked = false;
-			if(lastClick == getCursorPosition() && (SDL_GetTicks() - lastClickTime) < 300)
-			{
-				std::list<CIntObject*> hlp = doubleClickInterested;
-				for(auto i = hlp.begin(); i != hlp.end() && continueEventHandling; i++)
-				{
-					if(!vstd::contains(doubleClickInterested, *i)) continue;
-					if((*i)->pos.isInside(current.motion.x, current.motion.y))
-					{
-						(*i)->onDoubleClick();
-						doubleClicked = true;
-					}
-				}
-
-			}
-
-			lastClick = current.motion;
-			lastClickTime = SDL_GetTicks();
-
-			if(!doubleClicked)
-				handleMouseButtonClick(lclickable, MouseButton::LEFT, true);
-			break;
-		}
-		case SDL_BUTTON_RIGHT:
-			handleMouseButtonClick(rclickable, MouseButton::RIGHT, true);
-			break;
-		case SDL_BUTTON_MIDDLE:
-			handleMouseButtonClick(mclickable, MouseButton::MIDDLE, true);
-			break;
-		default:
-			break;
-		}
-	}
-	else if(current.type == SDL_MOUSEWHEEL)
-	{
-		std::list<CIntObject*> hlp = wheelInterested;
-		for(auto i = hlp.begin(); i != hlp.end() && continueEventHandling; i++)
-		{
-			if(!vstd::contains(wheelInterested,*i)) continue;
-			// SDL doesn't have the proper values for mouse positions on SDL_MOUSEWHEEL, refetch them
-			int x = 0, y = 0;
-			SDL_GetMouseState(&x, &y);
-			(*i)->wheelScrolled(current.wheel.y < 0, (*i)->pos.isInside(x, y));
-		}
-	}
-	else if(current.type == SDL_TEXTINPUT)
-	{
-		for(auto it : textInterested)
-		{
-			it->textInputed(current.text.text);
-		}
-	}
-	else if(current.type == SDL_TEXTEDITING)
-	{
-		for(auto it : textInterested)
-		{
-			it->textEdited(current.edit.text);
-		}
-	}
-	else if(current.type == SDL_MOUSEBUTTONUP)
-	{
-		if(!multifinger)
-		{
-			switch(current.button.button)
-			{
-			case SDL_BUTTON_LEFT:
-				handleMouseButtonClick(lclickable, MouseButton::LEFT, false);
-				break;
-			case SDL_BUTTON_RIGHT:
-				handleMouseButtonClick(rclickable, MouseButton::RIGHT, false);
-				break;
-			case SDL_BUTTON_MIDDLE:
-				handleMouseButtonClick(mclickable, MouseButton::MIDDLE, false);
-				break;
-			}
-		}
-	}
-	else if(current.type == SDL_FINGERMOTION)
-	{
-		if(isPointerRelativeMode)
-		{
-			fakeMoveCursor(current.tfinger.dx, current.tfinger.dy);
-		}
-	}
-	else if(current.type == SDL_FINGERDOWN)
-	{
-		auto fingerCount = SDL_GetNumTouchFingers(current.tfinger.touchId);
-
-		multifinger = fingerCount > 1;
-
-		if(isPointerRelativeMode)
-		{
-			if(current.tfinger.x > 0.5)
-			{
-				bool isRightClick = current.tfinger.y < 0.5;
-
-				fakeMouseButtonEventRelativeMode(true, isRightClick);
-			}
-		}
-#ifndef VCMI_IOS
-		else if(fingerCount == 2)
-		{
-			convertTouchToMouse(&current);
-			handleMouseMotion(current);
-			handleMouseButtonClick(rclickable, MouseButton::RIGHT, true);
-		}
-#endif //VCMI_IOS
-	}
-	else if(current.type == SDL_FINGERUP)
-	{
-#ifndef VCMI_IOS
-		auto fingerCount = SDL_GetNumTouchFingers(current.tfinger.touchId);
-#endif //VCMI_IOS
-
-		if(isPointerRelativeMode)
-		{
-			if(current.tfinger.x > 0.5)
-			{
-				bool isRightClick = current.tfinger.y < 0.5;
-
-				fakeMouseButtonEventRelativeMode(false, isRightClick);
-			}
-		}
-#ifndef VCMI_IOS
-		else if(multifinger)
-		{
-			convertTouchToMouse(&current);
-			handleMouseMotion(current);
-			handleMouseButtonClick(rclickable, MouseButton::RIGHT, false);
-			multifinger = fingerCount != 0;
-		}
-#endif //VCMI_IOS
-	}
-} //event end
-
-void CGuiHandler::handleMouseButtonClick(CIntObjectList & interestedObjs, MouseButton btn, bool isPressed)
-{
-	auto hlp = interestedObjs;
-	for(auto i = hlp.begin(); i != hlp.end() && continueEventHandling; i++)
-	{
-		if(!vstd::contains(interestedObjs, *i)) continue;
-
-		auto prev = (*i)->mouseState(btn);
-		if(!isPressed)
-			(*i)->updateMouseState(btn, isPressed);
-		if((*i)->pos.isInside(getCursorPosition()))
-		{
-			if(isPressed)
-				(*i)->updateMouseState(btn, isPressed);
-			(*i)->click(btn, isPressed, prev);
-		}
-		else if(!isPressed)
-			(*i)->click(btn, boost::logic::indeterminate, prev);
-	}
-}
-
-void CGuiHandler::handleMouseMotion(const SDL_Event & current)
-{
-	//sending active, hovered hoverable objects hover() call
-	std::vector<CIntObject*> hlp;
-
-	auto hoverableCopy = hoverable;
-	for(auto & elem : hoverableCopy)
-	{
-		if(elem->pos.isInside(getCursorPosition()))
-		{
-			if (!(elem)->hovered)
-				hlp.push_back((elem));
-		}
-		else if ((elem)->hovered)
-		{
-			(elem)->hover(false);
-			(elem)->hovered = false;
-		}
-	}
-
-	for(auto & elem : hlp)
-	{
-		elem->hover(true);
-		elem->hovered = true;
-	}
-
-	// do not send motion events for events outside our window
-	//if (current.motion.windowID == 0)
-		handleMoveInterested(current.motion);
-}
-
-void CGuiHandler::simpleRedraw()
-{
-	//update only top interface and draw background
-	if(objsToBlit.size() > 1)
-		CSDL_Ext::blitAt(screen2,0,0,screen); //blit background
-	if(!objsToBlit.empty())
-		objsToBlit.back()->show(screen); //blit active interface/window
-}
-
-void CGuiHandler::handleMoveInterested(const SDL_MouseMotionEvent & motion)
-{
-	//sending active, MotionInterested objects mouseMoved() call
-	std::list<CIntObject*> miCopy = motioninterested;
-	for(auto & elem : miCopy)
-	{
-		if(elem->strongInterest || Rect::createAround(elem->pos, 1).isInside( motion.x, motion.y)) //checking bounds including border fixes bug #2476
-		{
-			(elem)->mouseMoved(Point(motion.x, motion.y));
-		}
-	}
+	input().stopTextInput();
 }
 
 void CGuiHandler::renderFrame()
 {
-
 	// Updating GUI requires locking pim mutex (that protects screen and GUI state).
 	// During game:
 	// When ending the game, the pim mutex might be hold by other thread,
@@ -686,71 +133,56 @@ void CGuiHandler::renderFrame()
 
 		SDL_RenderPresent(mainRenderer);
 
-		disposed.clear();
+		windows().onFrameRendered();
 	}
 
-	mainFPSmng->framerateDelay(); // holds a constant FPS
+	framerate().framerateDelay(); // holds a constant FPS
 }
 
-
 CGuiHandler::CGuiHandler()
-	: lastClick(-500, -500)
-	, lastClickTime(0)
-	, defActionsDef(0)
+	: defActionsDef(0)
 	, captureChildren(false)
-	, multifinger(false)
-	, mouseButtonsMask(0)
-	, continueEventHandling(true)
 	, curInt(nullptr)
-	, mainFPSmng(nullptr)
-	, statusbar(nullptr)
+	, fakeStatusBar(std::make_shared<EmptyStatusBar>())
+	, terminate_cond (new CondSh<bool>(false))
 {
-	terminate_cond = new CondSh<bool>(false);
 }
 
 CGuiHandler::~CGuiHandler()
 {
-	delete mainFPSmng;
 	delete terminate_cond;
 }
 
-ShortcutHandler & CGuiHandler::shortcutsHandler()
+ShortcutHandler & CGuiHandler::shortcuts()
 {
+	assert(shortcutsHandlerInstance);
 	return *shortcutsHandlerInstance;
 }
 
-void CGuiHandler::moveCursorToPosition(const Point & position)
+FramerateManager & CGuiHandler::framerate()
 {
-	SDL_WarpMouseInWindow(mainWindow, position.x, position.y);
+	assert(framerateManagerInstance);
+	return *framerateManagerInstance;
 }
 
 bool CGuiHandler::isKeyboardCtrlDown() const
 {
-#ifdef VCMI_MAC
-	return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LGUI] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RGUI];
-#else
-	return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LCTRL] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RCTRL];
-#endif
+	return inputHandlerInstance->isKeyboardCtrlDown();
 }
 
 bool CGuiHandler::isKeyboardAltDown() const
 {
-	return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LALT] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RALT];
+	return inputHandlerInstance->isKeyboardAltDown();
 }
 
 bool CGuiHandler::isKeyboardShiftDown() const
 {
-	return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LSHIFT] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RSHIFT];
-}
-
-void CGuiHandler::breakEventHandling()
-{
-	continueEventHandling = false;
+	return inputHandlerInstance->isKeyboardShiftDown();
 }
 
 const Point & CGuiHandler::getCursorPosition() const
 {
-	return cursorPosition;
+	return inputHandlerInstance->getCursorPosition();
 }
 
 Point CGuiHandler::screenDimensions() const
@@ -758,21 +190,9 @@ Point CGuiHandler::screenDimensions() const
 	return Point(screen->w, screen->h);
 }
 
-bool CGuiHandler::isMouseButtonPressed() const
-{
-	return mouseButtonsMask > 0;
-}
-
 bool CGuiHandler::isMouseButtonPressed(MouseButton button) const
 {
-	static_assert(static_cast<uint32_t>(MouseButton::LEFT)   == SDL_BUTTON_LEFT,   "mismatch between VCMI and SDL enum!");
-	static_assert(static_cast<uint32_t>(MouseButton::MIDDLE) == SDL_BUTTON_MIDDLE, "mismatch between VCMI and SDL enum!");
-	static_assert(static_cast<uint32_t>(MouseButton::RIGHT)  == SDL_BUTTON_RIGHT,  "mismatch between VCMI and SDL enum!");
-	static_assert(static_cast<uint32_t>(MouseButton::EXTRA1) == SDL_BUTTON_X1,     "mismatch between VCMI and SDL enum!");
-	static_assert(static_cast<uint32_t>(MouseButton::EXTRA2) == SDL_BUTTON_X2,     "mismatch between VCMI and SDL enum!");
-
-	uint32_t index = static_cast<uint32_t>(button);
-	return mouseButtonsMask & SDL_BUTTON(index);
+	return inputHandlerInstance->isMouseButtonPressed(button);
 }
 
 void CGuiHandler::drawFPSCounter()
@@ -780,7 +200,7 @@ void CGuiHandler::drawFPSCounter()
 	static SDL_Rect overlay = { 0, 0, 64, 32};
 	uint32_t black = SDL_MapRGB(screen->format, 10, 10, 10);
 	SDL_FillRect(screen, &overlay, black);
-	std::string fps = std::to_string(mainFPSmng->getFramerate());
+	std::string fps = std::to_string(framerate().getFramerate());
 	graphics->fonts[FONT_BIG]->renderTextLeft(screen, fps, Colors::YELLOW, Point(10, 10));
 }
 
@@ -791,63 +211,52 @@ bool CGuiHandler::amIGuiThread()
 
 void CGuiHandler::pushUserEvent(EUserEvent usercode)
 {
-	pushUserEvent(usercode, nullptr);
+	inputHandlerInstance->pushUserEvent(usercode, nullptr);
 }
 
 void CGuiHandler::pushUserEvent(EUserEvent usercode, void * userdata)
 {
-	SDL_Event event;
-	event.type = SDL_USEREVENT;
-	event.user.code = static_cast<int32_t>(usercode);
-	event.user.data1 = userdata;
-	SDL_PushEvent(&event);
+	inputHandlerInstance->pushUserEvent(usercode, userdata);
 }
 
-CFramerateManager::CFramerateManager()
-	: rate(0)
-	, rateticks(0)
-	, fps(0)
-	, accumulatedFrames(0)
-	, accumulatedTime(0)
-	, lastticks(0)
-	, timeElapsed(0)
-{}
-
-void CFramerateManager::init(int newRate)
+IScreenHandler & CGuiHandler::screenHandler()
 {
-	rate = newRate;
-	rateticks = 1000.0 / rate;
-	this->lastticks = SDL_GetTicks();
+	return *screenHandlerInstance;
 }
 
-void CFramerateManager::framerateDelay()
+EventDispatcher & CGuiHandler::events()
 {
-	ui32 currentTicks = SDL_GetTicks();
+	return *eventDispatcherInstance;
+}
 
-	timeElapsed = currentTicks - lastticks;
-	accumulatedFrames++;
+InputHandler & CGuiHandler::input()
+{
+	return *inputHandlerInstance;
+}
 
-	// FPS is higher than it should be, then wait some time
-	if(timeElapsed < rateticks)
-	{
-		int timeToSleep = (uint32_t)ceil(this->rateticks) - timeElapsed;
-		boost::this_thread::sleep(boost::posix_time::milliseconds(timeToSleep));
-	}
+WindowHandler & CGuiHandler::windows()
+{
+	assert(windowHandlerInstance);
+	return *windowHandlerInstance;
+}
 
-	currentTicks = SDL_GetTicks();
-	// recalculate timeElapsed for external calls via getElapsed()
-	// limit it to 100 ms to avoid breaking animation in case of huge lag (e.g. triggered breakpoint)
-	timeElapsed = std::min<ui32>(currentTicks - lastticks, 100);
+std::shared_ptr<IStatusBar> CGuiHandler::statusbar()
+{
+	auto locked = currentStatusBar.lock();
 
-	lastticks = SDL_GetTicks();
+	if (!locked)
+		return fakeStatusBar;
 
-	accumulatedTime += timeElapsed;
+	return locked;
+}
 
-	if(accumulatedFrames >= 100)
-	{
-		//about 2 second should be passed
-		fps = static_cast<int>(ceil(1000.0 / (accumulatedTime / accumulatedFrames)));
-		accumulatedTime = 0;
-		accumulatedFrames = 0;
-	}
+void CGuiHandler::setStatusbar(std::shared_ptr<IStatusBar> newStatusBar)
+{
+	currentStatusBar = newStatusBar;
+}
+
+void CGuiHandler::onScreenResize()
+{
+	screenHandler().onScreenResize();
+	windows().onScreenResize();
 }

+ 40 - 107
client/gui/CGuiHandler.h

@@ -9,118 +9,66 @@
  */
 #pragma once
 
-#include "MouseButton.h"
-#include "../../lib/Point.h"
-
 VCMI_LIB_NAMESPACE_BEGIN
-
 template <typename T> struct CondSh;
+class Point;
 class Rect;
-
 VCMI_LIB_NAMESPACE_END
 
-union SDL_Event;
-struct SDL_MouseMotionEvent;
-
+enum class MouseButton;
 class ShortcutHandler;
-class CFramerateManager;
+class FramerateManager;
 class IStatusBar;
 class CIntObject;
 class IUpdateable;
 class IShowActivatable;
-class IShowable;
+class IScreenHandler;
+class WindowHandler;
+class EventDispatcher;
+class InputHandler;
 
-// TODO: event handling need refactoring
+// TODO: event handling need refactoring. Perhaps convert into delayed function call?
 enum class EUserEvent
 {
-	/*CHANGE_SCREEN_RESOLUTION = 1,*/
-	RETURN_TO_MAIN_MENU = 2,
-	//STOP_CLIENT = 3,
-	RESTART_GAME = 4,
+	RETURN_TO_MAIN_MENU,
+	RESTART_GAME,
 	RETURN_TO_MENU_LOAD,
 	FULLSCREEN_TOGGLED,
 	CAMPAIGN_START_SCENARIO,
-	FORCE_QUIT, //quit client without question
-};
-
-// A fps manager which holds game updates at a constant rate
-class CFramerateManager
-{
-private:
-	double rateticks;
-	ui32 lastticks;
-	ui32 timeElapsed;
-	int rate;
-	int fps; // the actual fps value
-	ui32 accumulatedTime;
-	ui32 accumulatedFrames;
-
-public:
-	CFramerateManager(); // initializes the manager with a given fps rate
-	void init(int newRate); // needs to be called directly before the main game loop to reset the internal timer
-	void framerateDelay(); // needs to be called every game update cycle
-	ui32 getElapsedMilliseconds() const {return this->timeElapsed;}
-	ui32 getFrameNumber() const { return accumulatedFrames; }
-	ui32 getFramerate() const { return fps; };
+	FORCE_QUIT,
 };
 
 // Handles GUI logic and drawing
 class CGuiHandler
 {
-public:
-	CFramerateManager * mainFPSmng; //to keep const framerate
-	std::list<std::shared_ptr<IShowActivatable>> listInt; //list of interfaces - front=foreground; back = background (includes adventure map, window interfaces, all kind of active dialogs, and so on)
-	std::shared_ptr<IStatusBar> statusbar;
-
 private:
-	Point cursorPosition;
-	uint32_t mouseButtonsMask;
+	/// Fake no-op version status bar, for use in windows that have no status bar
+	std::shared_ptr<IStatusBar> fakeStatusBar;
 
-	std::vector<std::shared_ptr<IShowActivatable>> disposed;
+	/// Status bar of current window, if any. Uses weak_ptr to allow potential hanging reference after owned window has been deleted
+	std::weak_ptr<IStatusBar> currentStatusBar;
 
 	std::unique_ptr<ShortcutHandler> shortcutsHandlerInstance;
+	std::unique_ptr<WindowHandler> windowHandlerInstance;
 
-	std::atomic<bool> continueEventHandling;
-	using CIntObjectList = std::list<CIntObject *>;
-
-	//active GUI elements (listening for events
-	CIntObjectList lclickable;
-	CIntObjectList rclickable;
-	CIntObjectList mclickable;
-	CIntObjectList hoverable;
-	CIntObjectList keyinterested;
-	CIntObjectList motioninterested;
-	CIntObjectList timeinterested;
-	CIntObjectList wheelInterested;
-	CIntObjectList doubleClickInterested;
-	CIntObjectList textInterested;
-
-
-	void handleMouseButtonClick(CIntObjectList & interestedObjs, MouseButton btn, bool isPressed);
-	void processLists(const ui16 activityFlag, std::function<void (std::list<CIntObject*> *)> cb);
-	void handleCurrentEvent(SDL_Event &current);
-	void handleMouseMotion(const SDL_Event & current);
-	void handleMoveInterested( const SDL_MouseMotionEvent & motion );
-	void convertTouchToMouse(SDL_Event * current);
-	void fakeMoveCursor(float dx, float dy);
-	void fakeMouseButtonEventRelativeMode(bool down, bool right);
+	std::unique_ptr<IScreenHandler> screenHandlerInstance;
+	std::unique_ptr<FramerateManager> framerateManagerInstance;
+	std::unique_ptr<EventDispatcher> eventDispatcherInstance;
+	std::unique_ptr<InputHandler> inputHandlerInstance;
 
 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();
+	ShortcutHandler & shortcuts();
+	FramerateManager & framerate();
+	EventDispatcher & events();
+	InputHandler & input();
 
+	/// Returns current logical screen dimensions
+	/// May not match size of window if user has UI scaling different from 100%
 	Point screenDimensions() const;
 
-	/// returns true if at least one mouse button is pressed
-	bool isMouseButtonPressed() const;
-
 	/// returns true if specified mouse button is pressed
 	bool isMouseButtonPressed(MouseButton button) const;
 
@@ -132,16 +80,17 @@ public:
 	void startTextInput(const Rect & where);
 	void stopTextInput();
 
-	/// moves mouse pointer into specified position inside vcmi window
-	void moveCursorToPosition(const Point & position);
+	IScreenHandler & screenHandler();
 
-	IUpdateable *curInt;
+	WindowHandler & windows();
+
+	/// Returns currently active status bar. Guaranteed to be non-null
+	std::shared_ptr<IStatusBar> statusbar();
 
-	Point lastClick;
-	unsigned lastClickTime;
-	bool multifinger;
-	bool isPointerRelativeMode;
-	float pointerSpeedMultiplier;
+	/// Set currently active status bar
+	void setStatusbar(std::shared_ptr<IStatusBar>);
+
+	IUpdateable *curInt;
 
 	ui8 defActionsDef; //default auto actions
 	bool captureChildren; //all newly created objects will get their parents from stack and will be added to parents children list
@@ -153,32 +102,16 @@ public:
 	void init();
 	void renderFrame();
 
-	void totalRedraw(); //forces total redraw (using showAll), sets a flag, method gets called at the end of the rendering
-	void simpleRedraw(); //update only top interface and draw background from buffer, sets a flag, method gets called at the end of the rendering
-
-	void pushInt(std::shared_ptr<IShowActivatable> newInt); //deactivate old top interface, activates this one and pushes to the top
-	template <typename T, typename ... Args>
-	void pushIntT(Args && ... args)
-	{
-		auto newInt = std::make_shared<T>(std::forward<Args>(args)...);
-		pushInt(newInt);
-	}
-
-	void popInts(int howMany); //pops one or more interfaces - deactivates top, deletes and removes given number of interfaces, activates new front
-
-	void popInt(std::shared_ptr<IShowActivatable> top); //removes given interface from the top and activates next
-
-	std::shared_ptr<IShowActivatable> topInt(); //returns top interface
+	/// called whenever user selects different resolution, requiring to center/resize all windows
+	void onScreenResize();
 
-	void updateTime(); //handles timeInterested
 	void handleEvents(); //takes events from queue and calls interested objects
 	void fakeMouseMove();
-	void breakEventHandling(); //current event won't be propagated anymore
 	void drawFPSCounter(); // draws the FPS to the upper left corner of the screen
 
-	static bool amIGuiThread();
-	static void pushUserEvent(EUserEvent usercode);
-	static void pushUserEvent(EUserEvent usercode, void * userdata);
+	bool amIGuiThread();
+	void pushUserEvent(EUserEvent usercode);
+	void pushUserEvent(EUserEvent usercode, void * userdata);
 
 	CondSh<bool> * terminate_cond; // confirm termination
 };

+ 54 - 103
client/gui/CIntObject.cpp

@@ -11,25 +11,18 @@
 #include "CIntObject.h"
 
 #include "CGuiHandler.h"
+#include "WindowHandler.h"
 #include "Shortcut.h"
 #include "../renderSDL/SDL_Extensions.h"
 #include "../windows/CMessage.h"
 #include "../CMT.h"
 
-#include <SDL_pixels.h>
-
-IShowActivatable::IShowActivatable()
-{
-	type = 0;
-}
-
 CIntObject::CIntObject(int used_, Point pos_):
 	parent_m(nullptr),
-	active_m(0),
 	parent(parent_m),
-	active(active_m)
+	type(0)
 {
-	hovered = captureAllKeys = strongInterest = false;
+	captureAllKeys = false;
 	used = used_;
 
 	recActions = defActions = GH.defActionsDef;
@@ -45,7 +38,7 @@ CIntObject::CIntObject(int used_, Point pos_):
 
 CIntObject::~CIntObject()
 {
-	if(active_m)
+	if(isActive())
 		deactivate();
 
 	while(!children.empty())
@@ -75,25 +68,16 @@ void CIntObject::showAll(SDL_Surface * to)
 		for(auto & elem : children)
 			if(elem->recActions & SHOWALL)
 				elem->showAll(to);
-
 	}
 }
 
 void CIntObject::activate()
 {
-	if (active_m)
-	{
-		if ((used | GENERAL) == active_m)
-			return;
-		else
-		{
-			logGlobal->warn("Warning: IntObject re-activated with mismatching used and active");
-			deactivate(); //FIXME: better to avoid such possibility at all
-		}
-	}
+	if (isActive())
+		return;
 
-	active_m |= GENERAL;
-	activate(used);
+	activateEvents(used | GENERAL);
+	assert(isActive());
 
 	if(defActions & ACTIVATE)
 		for(auto & elem : children)
@@ -101,20 +85,14 @@ void CIntObject::activate()
 				elem->activate();
 }
 
-void CIntObject::activate(ui16 what)
-{
-	GH.handleElementActivate(this, what);
-}
-
 void CIntObject::deactivate()
 {
-	if (!active_m)
+	if (!isActive())
 		return;
 
-	active_m &= ~ GENERAL;
-	deactivate(active_m);
+	deactivateEvents(ALL);
 
-	assert(!active_m);
+	assert(!isActive());
 
 	if(defActions & DEACTIVATE)
 		for(auto & elem : children)
@@ -122,75 +100,33 @@ void CIntObject::deactivate()
 				elem->deactivate();
 }
 
-void CIntObject::deactivate(ui16 what)
-{
-	GH.handleElementDeActivate(this, what);
-}
-
-void CIntObject::click(MouseButton btn, tribool down, bool previousState)
-{
-	switch(btn)
-	{
-	default:
-	case MouseButton::LEFT:
-		clickLeft(down, previousState);
-		break;
-	case MouseButton::MIDDLE:
-		clickMiddle(down, previousState);
-		break;
-	case MouseButton::RIGHT:
-		clickRight(down, previousState);
-		break;
-	}
-}
-
-void CIntObject::printAtLoc(const std::string & text, int x, int y, EFonts font, SDL_Color kolor, SDL_Surface * dst)
-{
-	graphics->fonts[font]->renderTextLeft(dst, text, kolor, Point(pos.x + x, pos.y + y));
-}
-
-void CIntObject::printAtMiddleLoc(const std::string & text, int x, int y, EFonts font, SDL_Color kolor, SDL_Surface * dst)
-{
-	printAtMiddleLoc(text, Point(x,y), font, kolor, dst);
-}
-
-void CIntObject::printAtMiddleLoc(const std::string & text, const Point &p, EFonts font, SDL_Color kolor, SDL_Surface * dst)
+void CIntObject::printAtMiddleLoc(const std::string & text, const Point &p, EFonts font, const SDL_Color & kolor, SDL_Surface * dst)
 {
 	graphics->fonts[font]->renderTextCenter(dst, text, kolor, pos.topLeft() + p);
 }
 
-void CIntObject::blitAtLoc( SDL_Surface * src, int x, int y, SDL_Surface * dst )
-{
-	CSDL_Ext::blitAt(src, pos.x + x, pos.y + y, dst);
-}
-
-void CIntObject::blitAtLoc(SDL_Surface * src, const Point &p, SDL_Surface * dst)
+void CIntObject::printAtMiddleWBLoc( const std::string & text, const Point &p, EFonts font, int charpr, const SDL_Color & kolor, SDL_Surface * dst)
 {
-	blitAtLoc(src, p.x, p.y, dst);
-}
-
-void CIntObject::printAtMiddleWBLoc( const std::string & text, int x, int y, EFonts font, int charpr, SDL_Color kolor, SDL_Surface * dst)
-{
-	graphics->fonts[font]->renderTextLinesCenter(dst, CMessage::breakText(text, charpr, font), kolor, Point(pos.x + x, pos.y + y));
+	graphics->fonts[font]->renderTextLinesCenter(dst, CMessage::breakText(text, charpr, font), kolor, pos.topLeft() + p);
 }
 
 void CIntObject::addUsedEvents(ui16 newActions)
 {
-	if (active_m)
-		activate(~used & newActions);
+	if (isActive())
+		activateEvents(~used & newActions);
 	used |= newActions;
 }
 
 void CIntObject::removeUsedEvents(ui16 newActions)
 {
-	if (active_m)
-		deactivate(used & newActions);
+	if (isActive())
+		deactivateEvents(used & newActions);
 	used &= ~newActions;
 }
 
 void CIntObject::disable()
 {
-	if(active)
+	if(isActive())
 		deactivate();
 
 	recActions = DISPOSE;
@@ -198,12 +134,23 @@ void CIntObject::disable()
 
 void CIntObject::enable()
 {
-	if(!active_m && (!parent_m || parent_m->active))
+	if(!isActive() && (!parent_m || parent_m->isActive()))
+	{
 		activate();
+		redraw();
+	}
 
 	recActions = 255;
 }
 
+void CIntObject::setEnabled(bool on)
+{
+	if (on)
+		enable();
+	else
+		disable();
+}
+
 void CIntObject::fitToScreen(int borderWidth, bool propagate)
 {
 	Point newPos = pos.topLeft();
@@ -242,11 +189,11 @@ void CIntObject::addChild(CIntObject * child, bool adjustPosition)
 	children.push_back(child);
 	child->parent_m = this;
 	if(adjustPosition)
-		child->pos += pos.topLeft();
+		child->moveBy(pos.topLeft(), adjustPosition);
 
-	if (!active && child->active)
+	if (!isActive() && child->isActive())
 		child->deactivate();
-	if (active && !child->active)
+	if (isActive()&& !child->isActive())
 		child->activate();
 }
 
@@ -271,7 +218,7 @@ void CIntObject::redraw()
 {
 	//currently most of calls come from active objects so this check won't affect them
 	//it should fix glitches when called by inactive elements located below active window
-	if (active)
+	if (isActive())
 	{
 		if (parent_m && (type & REDRAW_PARENT))
 		{
@@ -286,6 +233,16 @@ void CIntObject::redraw()
 	}
 }
 
+bool CIntObject::isInside(const Point & position)
+{
+	return pos.isInside(position);
+}
+
+void CIntObject::onScreenResize()
+{
+	center(pos, true);
+}
+
 const Rect & CIntObject::center( const Rect &r, bool propagate )
 {
 	pos.w = r.w;
@@ -313,6 +270,7 @@ bool CIntObject::captureThisKey(EShortcut key)
 
 CKeyShortcut::CKeyShortcut()
 	: assignedKey(EShortcut::NONE)
+	, shortcutPressed(false)
 {}
 
 CKeyShortcut::CKeyShortcut(EShortcut key)
@@ -322,23 +280,19 @@ CKeyShortcut::CKeyShortcut(EShortcut key)
 
 void CKeyShortcut::keyPressed(EShortcut key)
 {
-	if( assignedKey == key && assignedKey != EShortcut::NONE)
+	if( assignedKey == key && assignedKey != EShortcut::NONE && !shortcutPressed)
 	{
-		bool prev = mouseState(MouseButton::LEFT);
-		updateMouseState(MouseButton::LEFT, true);
-		clickLeft(true, prev);
-
+		shortcutPressed = true;
+		clickLeft(true, false);
 	}
 }
 
 void CKeyShortcut::keyReleased(EShortcut key)
 {
-	if( assignedKey == key && assignedKey != EShortcut::NONE)
+	if( assignedKey == key && assignedKey != EShortcut::NONE && shortcutPressed)
 	{
-		bool prev = mouseState(MouseButton::LEFT);
-		updateMouseState(MouseButton::LEFT, false);
-		clickLeft(false, prev);
-
+		shortcutPressed = false;
+		clickLeft(false, true);
 	}
 }
 
@@ -350,10 +304,7 @@ WindowBase::WindowBase(int used_, Point pos_)
 
 void WindowBase::close()
 {
-	if(GH.topInt().get() != this)
+	if(!GH.windows().isTopWindow(this))
 		logGlobal->error("Only top interface must be closed");
-	GH.popInts(1);
+	GH.windows().popWindows(1);
 }
-
-IStatusBar::~IStatusBar()
-{}

+ 44 - 100
client/gui/CIntObject.h

@@ -9,83 +9,49 @@
  */
 #pragma once
 
-#include "MouseButton.h"
 #include "../render/Graphics.h"
 #include "../../lib/Rect.h"
+#include "EventsReceiver.h"
 
 struct SDL_Surface;
 class CGuiHandler;
 class CPicture;
-enum class EShortcut;
-
-using boost::logic::tribool;
-
-// Defines a activate/deactive method
-class IActivatable
-{
-public:
-	virtual void activate()=0;
-	virtual void deactivate()=0;
-	virtual ~IActivatable(){};
-};
 
 class IUpdateable
 {
 public:
 	virtual void update()=0;
-	virtual ~IUpdateable(){};
+	virtual ~IUpdateable() = default;
 };
 
-// Defines a show method
-class IShowable
+class IShowActivatable
 {
 public:
+	virtual void activate()=0;
+	virtual void deactivate()=0;
+
 	virtual void redraw()=0;
 	virtual void show(SDL_Surface * to) = 0;
-	virtual void showAll(SDL_Surface * to)
-	{
-		show(to);
-	}
-	virtual ~IShowable(){};
-};
+	virtual void showAll(SDL_Surface * to) = 0;
 
-class IShowActivatable : public IShowable, public IActivatable
-{
-public:
-	//redraw parent flag - this int may be semi-transparent and require redraw of parent window
-	enum {BLOCK_ADV_HOTKEYS = 2, REDRAW_PARENT=8};
-	int type; //bin flags using etype
-	IShowActivatable();
-	virtual ~IShowActivatable(){};
+	virtual void onScreenResize() = 0;
+	virtual ~IShowActivatable() = default;
 };
 
 // Base UI element
-class CIntObject : public IShowActivatable //interface object
+class CIntObject : public IShowActivatable, public AEventsReceiver //interface object
 {
-	ui16 used;//change via addUsed() or delUsed
-
-	std::map<MouseButton, bool> currentMouseState;
+	ui16 used;
 
 	//non-const versions of fields to allow changing them in CIntObject
 	CIntObject *parent_m; //parent object
-	ui16 active_m;
-
-protected:
-	//activate or deactivate specific action (LCLICK, RCLICK...)
-	void activate(ui16 what);
-	void deactivate(ui16 what);
 
 public:
-/*
- * Functions and fields that supposed to be private but are not for now.
- * Don't use them unless you really know what they are for
- */
-	std::vector<CIntObject *> children;
-
+	//redraw parent flag - this int may be semi-transparent and require redraw of parent window
+	enum {REDRAW_PARENT=8};
+	int type; //bin flags using etype
 
-/*
- * Public interface
- */
+	std::vector<CIntObject *> children;
 
 	/// read-only parent access. May not be a "clean" solution but allows some compatibility
 	CIntObject * const & parent;
@@ -96,43 +62,14 @@ public:
 	CIntObject(int used=0, Point offset=Point());
 	virtual ~CIntObject();
 
-	void updateMouseState(MouseButton btn, bool state) { currentMouseState[btn] = state; }
-	bool mouseState(MouseButton btn) const { return currentMouseState.count(btn) ? currentMouseState.at(btn) : false; }
-
-	virtual void click(MouseButton btn, tribool down, bool previousState);
-	virtual void clickLeft(tribool down, bool previousState) {}
-	virtual void clickRight(tribool down, bool previousState) {}
-	virtual void clickMiddle(tribool down, bool previousState) {}
-
 	//hover handling
-	/*const*/ bool hovered;  //for determining if object is hovered
-	virtual void hover (bool on){}
+	void hover (bool on) override{}
 
 	//keyboard handling
 	bool captureAllKeys; //if true, only this object should get info about pressed keys
-	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){};
-
-	//mouse movement handling
-	bool strongInterest; //if true - report all mouse movements, if not - only when hovered
-	virtual void mouseMoved (const Point & cursorPosition){}
-
-	//time handling
-	virtual void tick(uint32_t msPassed){}
-
-	//mouse wheel
-	virtual void wheelScrolled(bool down, bool in){}
 
-	//double click
-	virtual void onDoubleClick(){}
+	bool captureThisKey(EShortcut key) override; //allows refining captureAllKeys against specific events (eg. don't capture ENTER)
 
-	// These are the arguments that can be used to determine what kind of input the CIntObject will receive
-	enum {LCLICK=1, RCLICK=2, HOVER=4, MOVE=8, KEYBOARD=16, TIME=32, GENERAL=64, WHEEL=128, DOUBLECLICK=256, TEXTINPUT=512, MCLICK=1024, ALL=0xffff};
-	const ui16 & active;
 	void addUsedEvents(ui16 newActions);
 	void removeUsedEvents(ui16 newActions);
 
@@ -140,8 +77,12 @@ public:
 	ui8 defActions; //which calls will be tried to be redirected to children
 	ui8 recActions; //which calls we allow to receive from parent
 
-	void disable(); //deactivates if needed, blocks all automatic activity, allows only disposal
-	void enable(); //activates if needed, all activity enabled (Warning: may not be symetric with disable if recActions was limited!)
+	/// deactivates if needed, blocks all automatic activity, allows only disposal
+	void disable();
+	/// activates if needed, all activity enabled (Warning: may not be symetric with disable if recActions was limited!)
+	void enable();
+	/// deactivates or activates UI element based on flag
+	void setEnabled(bool on);
 
 	// activate or deactivate object. Inactive object won't receive any input events (keyboard\mouse)
 	// usually used automatically by parent
@@ -155,6 +96,12 @@ public:
 	//request complete redraw of this object
 	void redraw() override;
 
+	/// called only for windows whenever screen size changes
+	/// default behavior is to re-center, can be overriden
+	void onScreenResize() override;
+
+	bool isInside(const Point & position) override;
+
 	const Rect & center(const Rect &r, bool propagate = true); //sets pos so that r will be in the center of screen, assigns sizes of r to pos, returns new position
 	const Rect & center(const Point &p, bool propagate = true);  //moves object so that point p will be in its center
 	const Rect & center(bool propagate = true); //centers when pos.w and pos.h are set, returns new position
@@ -164,30 +111,18 @@ public:
 
 	void addChild(CIntObject *child, bool adjustPosition = false);
 	void removeChild(CIntObject *child, bool adjustPosition = false);
-	//delChild - not needed, use normal "delete child" instead
-	//delChildNull - not needed, use "vstd::clear_pointer(child)" instead
-
-/*
- * Functions that should be used only by specific GUI elements. Don't use them unless you really know why they are here
- */
 
-	//functions for printing text. Use CLabel where possible instead
-	void printAtLoc(const std::string & text, int x, int y, EFonts font, SDL_Color color, SDL_Surface * dst);
-	void printAtMiddleLoc(const std::string & text, int x, int y, EFonts font, SDL_Color color, SDL_Surface * dst);
-	void printAtMiddleLoc(const std::string & text, const Point &p, EFonts font, SDL_Color color, SDL_Surface * dst);
-	void printAtMiddleWBLoc(const std::string & text, int x, int y, EFonts font, int charsPerLine, SDL_Color color, SDL_Surface * dst);
-
-	//image blitting. If possible use CPicture or CAnimImage instead
-	void blitAtLoc(SDL_Surface * src, int x, int y, SDL_Surface * dst);
-	void blitAtLoc(SDL_Surface * src, const Point &p, SDL_Surface * dst);
-
-	friend class CGuiHandler;
+	/// functions for printing text.
+	/// Deprecated. Use CLabel where possible instead
+	void printAtMiddleLoc(const std::string & text, const Point &p, EFonts font, const SDL_Color & color, SDL_Surface * dst);
+	void printAtMiddleWBLoc(const std::string & text, const Point &p, EFonts font, int charsPerLine, const SDL_Color & color, SDL_Surface * dst);
 };
 
 /// Class for binding keys to left mouse button clicks
 /// Classes wanting use it should have it as one of their base classes
 class CKeyShortcut : public virtual CIntObject
 {
+	bool shortcutPressed;
 public:
 	EShortcut assignedKey;
 	CKeyShortcut();
@@ -208,7 +143,7 @@ protected:
 class IStatusBar
 {
 public:
-	virtual ~IStatusBar();
+	virtual ~IStatusBar() = default;
 
 	/// set current text for the status bar
 	virtual void write(const std::string & text) = 0;
@@ -226,3 +161,12 @@ public:
 	virtual void setEnteredText(const std::string & text) = 0;
 
 };
+
+class EmptyStatusBar : public IStatusBar
+{
+	virtual void write(const std::string & text){};
+	virtual void clear(){};
+	virtual void clearIfMatching(const std::string & testedText){};
+	virtual void setEnteringMode(bool on){};
+	virtual void setEnteredText(const std::string & text){};
+};

+ 2 - 1
client/gui/CursorHandler.cpp

@@ -12,6 +12,7 @@
 #include "CursorHandler.h"
 
 #include "CGuiHandler.h"
+#include "FramerateManager.h"
 #include "../renderSDL/CursorSoftware.h"
 #include "../renderSDL/CursorHardware.h"
 #include "../render/CAnimation.h"
@@ -250,7 +251,7 @@ void CursorHandler::updateSpellcastCursor()
 {
 	static const float frameDisplayDuration = 0.1f; // H3 uses 100 ms per frame
 
-	frameTime += GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+	frameTime += GH.framerate().getElapsedMilliseconds() / 1000.f;
 	size_t newFrame = frame;
 
 	while (frameTime >= frameDisplayDuration)

+ 244 - 0
client/gui/EventDispatcher.cpp

@@ -0,0 +1,244 @@
+/*
+ * EventDispatcher.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 "EventDispatcher.h"
+
+#include "EventsReceiver.h"
+#include "FramerateManager.h"
+#include "CGuiHandler.h"
+#include "MouseButton.h"
+
+#include "../../lib/Point.h"
+
+template<typename Functor>
+void EventDispatcher::processLists(ui16 activityFlag, const Functor & cb)
+{
+	auto processList = [&](ui16 mask, EventReceiversList & lst)
+	{
+		if(mask & activityFlag)
+			cb(lst);
+	};
+
+	processList(AEventsReceiver::LCLICK, lclickable);
+	processList(AEventsReceiver::RCLICK, rclickable);
+	processList(AEventsReceiver::MCLICK, mclickable);
+	processList(AEventsReceiver::HOVER, hoverable);
+	processList(AEventsReceiver::MOVE, motioninterested);
+	processList(AEventsReceiver::KEYBOARD, keyinterested);
+	processList(AEventsReceiver::TIME, timeinterested);
+	processList(AEventsReceiver::WHEEL, wheelInterested);
+	processList(AEventsReceiver::DOUBLECLICK, doubleClickInterested);
+	processList(AEventsReceiver::TEXTINPUT, textInterested);
+}
+
+void EventDispatcher::activateElement(AEventsReceiver * elem, ui16 activityFlag)
+{
+	processLists(activityFlag,[&](EventReceiversList & lst){
+		lst.push_front(elem);
+	});
+	elem->activeState |= activityFlag;
+}
+
+void EventDispatcher::deactivateElement(AEventsReceiver * elem, ui16 activityFlag)
+{
+	processLists(activityFlag,[&](EventReceiversList & lst){
+		auto hlp = std::find(lst.begin(),lst.end(),elem);
+		assert(hlp != lst.end());
+		lst.erase(hlp);
+	});
+	elem->activeState &= ~activityFlag;
+}
+
+void EventDispatcher::dispatchTimer(uint32_t msPassed)
+{
+	EventReceiversList hlp = timeinterested;
+	for (auto & elem : hlp)
+	{
+		if(!vstd::contains(timeinterested,elem)) continue;
+		(elem)->tick(msPassed);
+	}
+}
+
+void EventDispatcher::dispatchShortcutPressed(const std::vector<EShortcut> & shortcutsVector)
+{
+	bool keysCaptured = false;
+
+	for(auto & i : keyinterested)
+		for(EShortcut shortcut : shortcutsVector)
+			if(i->captureThisKey(shortcut))
+				keysCaptured = true;
+
+	EventReceiversList miCopy = keyinterested;
+
+	for(auto & i : miCopy)
+	{
+		for(EShortcut shortcut : shortcutsVector)
+			if(vstd::contains(keyinterested, i) && (!keysCaptured || i->captureThisKey(shortcut)))
+			{
+				i->keyPressed(shortcut);
+				if (keysCaptured)
+					return;
+			}
+	}
+}
+
+void EventDispatcher::dispatchShortcutReleased(const std::vector<EShortcut> & shortcutsVector)
+{
+	bool keysCaptured = false;
+
+	for(auto & i : keyinterested)
+		for(EShortcut shortcut : shortcutsVector)
+			if(i->captureThisKey(shortcut))
+				keysCaptured = true;
+
+	EventReceiversList miCopy = keyinterested;
+
+	for(auto & i : miCopy)
+	{
+		for(EShortcut shortcut : shortcutsVector)
+			if(vstd::contains(keyinterested, i) && (!keysCaptured || i->captureThisKey(shortcut)))
+			{
+				i->keyReleased(shortcut);
+				if (keysCaptured)
+					return;
+			}
+	}
+}
+
+EventDispatcher::EventReceiversList & EventDispatcher::getListForMouseButton(MouseButton button)
+{
+	switch (button)
+	{
+		case MouseButton::LEFT:
+			return lclickable;
+		case MouseButton::RIGHT:
+			return rclickable;
+		case MouseButton::MIDDLE:
+			return mclickable;
+	}
+	throw std::runtime_error("Invalid mouse button in getListForMouseButton");
+}
+
+void EventDispatcher::dispatchMouseDoubleClick(const Point & position)
+{
+	bool doubleClicked = false;
+	auto hlp = doubleClickInterested;
+
+	for(auto & i : hlp)
+	{
+		if(!vstd::contains(doubleClickInterested, i))
+			continue;
+
+		if(i->isInside(position))
+		{
+			i->onDoubleClick();
+			doubleClicked = true;
+		}
+	}
+
+	if(!doubleClicked)
+		dispatchMouseButtonPressed(MouseButton::LEFT, position);
+}
+
+void EventDispatcher::dispatchMouseButtonPressed(const MouseButton & button, const Point & position)
+{
+	handleMouseButtonClick(getListForMouseButton(button), button, true);
+}
+
+void EventDispatcher::dispatchMouseButtonReleased(const MouseButton & button, const Point & position)
+{
+	handleMouseButtonClick(getListForMouseButton(button), button, false);
+}
+
+void EventDispatcher::handleMouseButtonClick(EventReceiversList & interestedObjs, MouseButton btn, bool isPressed)
+{
+	auto hlp = interestedObjs;
+	for(auto & i : hlp)
+	{
+		if(!vstd::contains(interestedObjs, i))
+			continue;
+
+		auto prev = i->isMouseButtonPressed(btn);
+		if(!isPressed)
+			i->currentMouseState[btn] = isPressed;
+		if(i->isInside(GH.getCursorPosition()))
+		{
+			if(isPressed)
+				i->currentMouseState[btn] = isPressed;
+			i->click(btn, isPressed, prev);
+		}
+		else if(!isPressed)
+			i->click(btn, boost::logic::indeterminate, prev);
+	}
+}
+
+void EventDispatcher::dispatchMouseScrolled(const Point & distance, const Point & position)
+{
+	EventReceiversList hlp = wheelInterested;
+	for(auto & i : hlp)
+	{
+		if(!vstd::contains(wheelInterested,i))
+			continue;
+		i->wheelScrolled(distance.y < 0, i->isInside(position));
+	}
+}
+
+void EventDispatcher::dispatchTextInput(const std::string & text)
+{
+	for(auto it : textInterested)
+	{
+		it->textInputed(text);
+	}
+}
+
+void EventDispatcher::dispatchTextEditing(const std::string & text)
+{
+	for(auto it : textInterested)
+	{
+		it->textEdited(text);
+	}
+}
+
+void EventDispatcher::dispatchMouseMoved(const Point & position)
+{
+	//sending active, hovered hoverable objects hover() call
+	EventReceiversList hlp;
+
+	auto hoverableCopy = hoverable;
+	for(auto & elem : hoverableCopy)
+	{
+		if(elem->isInside(GH.getCursorPosition()))
+		{
+			if (!(elem)->isHovered())
+				hlp.push_back((elem));
+		}
+		else if ((elem)->isHovered())
+		{
+			(elem)->hover(false);
+			(elem)->hoveredState = false;
+		}
+	}
+
+	for(auto & elem : hlp)
+	{
+		elem->hover(true);
+		elem->hoveredState = true;
+	}
+
+	//sending active, MotionInterested objects mouseMoved() call
+	EventReceiversList miCopy = motioninterested;
+	for(auto & elem : miCopy)
+	{
+		if(elem->strongInterestState || elem->isInside(position)) //checking bounds including border fixes bug #2476
+		{
+			(elem)->mouseMoved(position);
+		}
+	}
+}

+ 68 - 0
client/gui/EventDispatcher.h

@@ -0,0 +1,68 @@
+/*
+ * EventDispatcher.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
+
+VCMI_LIB_NAMESPACE_BEGIN
+class Point;
+VCMI_LIB_NAMESPACE_END
+
+class AEventsReceiver;
+enum class MouseButton;
+enum class EShortcut;
+
+/// Class that receives events from event producers and dispatches it to UI elements that are interested in this event
+class EventDispatcher
+{
+	using EventReceiversList = std::list<AEventsReceiver *>;
+
+	/// list of UI elements that are interested in particular event
+	EventReceiversList lclickable;
+	EventReceiversList rclickable;
+	EventReceiversList mclickable;
+	EventReceiversList hoverable;
+	EventReceiversList keyinterested;
+	EventReceiversList motioninterested;
+	EventReceiversList timeinterested;
+	EventReceiversList wheelInterested;
+	EventReceiversList doubleClickInterested;
+	EventReceiversList textInterested;
+
+	EventReceiversList & getListForMouseButton(MouseButton button);
+
+	void handleMouseButtonClick(EventReceiversList & interestedObjs, MouseButton btn, bool isPressed);
+
+	template<typename Functor>
+	void processLists(ui16 activityFlag, const Functor & cb);
+
+public:
+	/// add specified UI element as interested. Uses unnamed enum from AEventsReceiver for activity flags
+	void activateElement(AEventsReceiver * elem, ui16 activityFlag);
+
+	/// removes specified UI element as interested for specified activities
+	void deactivateElement(AEventsReceiver * elem, ui16 activityFlag);
+
+	/// Regular timer event
+	void dispatchTimer(uint32_t msPassed);
+
+	/// Shortcut events (e.g. keyboard keys)
+	void dispatchShortcutPressed(const std::vector<EShortcut> & shortcuts);
+	void dispatchShortcutReleased(const std::vector<EShortcut> & shortcuts);
+
+	/// Mouse events
+	void dispatchMouseButtonPressed(const MouseButton & button, const Point & position);
+	void dispatchMouseButtonReleased(const MouseButton & button, const Point & position);
+	void dispatchMouseScrolled(const Point & distance, const Point & position);
+	void dispatchMouseDoubleClick(const Point & position);
+	void dispatchMouseMoved(const Point & position);
+
+	/// Text input events
+	void dispatchTextInput(const std::string & text);
+	void dispatchTextEditing(const std::string & text);
+};

+ 74 - 0
client/gui/EventsReceiver.cpp

@@ -0,0 +1,74 @@
+/*
+ * EventsReceiver.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 "EventsReceiver.h"
+
+#include "MouseButton.h"
+#include "CGuiHandler.h"
+#include "EventDispatcher.h"
+
+AEventsReceiver::AEventsReceiver()
+	: activeState(0)
+	, hoveredState(false)
+	, strongInterestState(false)
+{
+}
+
+bool AEventsReceiver::isHovered() const
+{
+	return hoveredState;
+}
+
+bool AEventsReceiver::isActive() const
+{
+	return activeState;
+}
+
+bool AEventsReceiver::isMouseButtonPressed(MouseButton btn) const
+{
+	return currentMouseState.count(btn) ? currentMouseState.at(btn) : false;
+}
+
+void AEventsReceiver::setMoveEventStrongInterest(bool on)
+{
+	strongInterestState = on;
+}
+
+void AEventsReceiver::activateEvents(ui16 what)
+{
+	assert((what & GENERAL) || (activeState & GENERAL));
+
+	activeState |= GENERAL;
+	GH.events().activateElement(this, what);
+}
+
+void AEventsReceiver::deactivateEvents(ui16 what)
+{
+	if (what & GENERAL)
+		activeState &= ~GENERAL;
+	GH.events().deactivateElement(this, what & activeState);
+}
+
+void AEventsReceiver::click(MouseButton btn, tribool down, bool previousState)
+{
+	switch(btn)
+	{
+	default:
+	case MouseButton::LEFT:
+		clickLeft(down, previousState);
+		break;
+	case MouseButton::MIDDLE:
+		clickMiddle(down, previousState);
+		break;
+	case MouseButton::RIGHT:
+		clickRight(down, previousState);
+		break;
+	}
+}

+ 77 - 0
client/gui/EventsReceiver.h

@@ -0,0 +1,77 @@
+/*
+ * EventsReceiver.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
+
+VCMI_LIB_NAMESPACE_BEGIN
+class Point;
+VCMI_LIB_NAMESPACE_END
+
+class EventDispatcher;
+enum class MouseButton;
+enum class EShortcut;
+using boost::logic::tribool;
+
+/// Class that is capable of subscribing and receiving input events
+/// Acts as base class for all UI elements
+class AEventsReceiver
+{
+	friend class EventDispatcher;
+
+	ui16 activeState;
+	bool hoveredState;
+	bool strongInterestState;
+	std::map<MouseButton, bool> currentMouseState;
+
+	void click(MouseButton btn, tribool down, bool previousState);
+protected:
+
+	/// If set, UI element will receive all mouse movement events, even those outside this element
+	void setMoveEventStrongInterest(bool on);
+
+	/// Activates particular events for this UI element. Uses unnamed enum from this class
+	void activateEvents(ui16 what);
+	/// Deactivates particular events for this UI element. Uses unnamed enum from this class
+	void deactivateEvents(ui16 what);
+
+	virtual void clickLeft(tribool down, bool previousState) {}
+	virtual void clickRight(tribool down, bool previousState) {}
+	virtual void clickMiddle(tribool down, bool previousState) {}
+
+	virtual void textInputed(const std::string & enteredText) {}
+	virtual void textEdited(const std::string & enteredText) {}
+
+	virtual void tick(uint32_t msPassed) {}
+	virtual void wheelScrolled(bool down, bool in) {}
+	virtual void mouseMoved(const Point & cursorPosition) {}
+	virtual void hover(bool on) {}
+	virtual void onDoubleClick() {}
+
+	virtual void keyPressed(EShortcut key) {}
+	virtual void keyReleased(EShortcut key) {}
+
+	virtual bool captureThisKey(EShortcut key) = 0;
+	virtual bool isInside(const Point & position) = 0;
+
+public:
+	AEventsReceiver();
+	virtual ~AEventsReceiver() = default;
+
+	/// These are the arguments that can be used to determine what kind of input UI element will receive
+	enum {LCLICK=1, RCLICK=2, HOVER=4, MOVE=8, KEYBOARD=16, TIME=32, GENERAL=64, WHEEL=128, DOUBLECLICK=256, TEXTINPUT=512, MCLICK=1024, ALL=0xffff};
+
+	/// Returns true if element is currently hovered by mouse
+	bool isHovered() const;
+
+	/// Returns true if element is currently active and may receive events
+	bool isActive() const;
+
+	/// Returns true if particular mouse button was pressed when inside this element
+	bool isMouseButtonPressed(MouseButton btn) const;
+};

+ 57 - 0
client/gui/FramerateManager.cpp

@@ -0,0 +1,57 @@
+/*
+ * FramerateManager.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
+ *
+ */
+
+#include "StdInc.h"
+#include "FramerateManager.h"
+
+FramerateManager::FramerateManager(int targetFrameRate)
+	: targetFrameTime(Duration(boost::chrono::seconds(1)) / targetFrameRate)
+	, lastFrameIndex(0)
+	, lastFrameTimes({})
+	, lastTimePoint (Clock::now())
+{
+	boost::range::fill(lastFrameTimes, targetFrameTime);
+}
+
+void FramerateManager::framerateDelay()
+{
+	Duration timeSpentBusy = Clock::now() - lastTimePoint;
+
+	// FPS is higher than it should be, then wait some time
+	if(timeSpentBusy < targetFrameTime)
+		boost::this_thread::sleep_for(targetFrameTime - timeSpentBusy);
+
+	// compute actual timeElapsed taking into account actual sleep interval
+	// limit it to 100 ms to avoid breaking animation in case of huge lag (e.g. triggered breakpoint)
+	TimePoint currentTicks = Clock::now();
+	Duration timeElapsed = currentTicks - lastTimePoint;
+	if(timeElapsed > boost::chrono::milliseconds(100))
+		timeElapsed = boost::chrono::milliseconds(100);
+
+	lastTimePoint = currentTicks;
+	lastFrameIndex = (lastFrameIndex + 1) % lastFrameTimes.size();
+	lastFrameTimes[lastFrameIndex] = timeElapsed;
+}
+
+ui32 FramerateManager::getElapsedMilliseconds() const
+{
+	return lastFrameTimes[lastFrameIndex] / boost::chrono::milliseconds(1);
+}
+
+ui32 FramerateManager::getFramerate() const
+{
+	Duration accumulatedTime = std::accumulate(lastFrameTimes.begin(), lastFrameTimes.end(), Duration());
+
+	auto actualFrameTime = accumulatedTime / lastFrameTimes.size();
+	if(actualFrameTime == actualFrameTime.zero())
+		return 0;
+
+	return std::round(boost::chrono::duration<double>(1) / actualFrameTime);
+};

+ 40 - 0
client/gui/FramerateManager.h

@@ -0,0 +1,40 @@
+/*
+ * FramerateManager.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
+
+/// Framerate manager controls current game frame rate by constantly trying to reach targeted frame rate
+class FramerateManager
+{
+	using Clock = boost::chrono::high_resolution_clock;
+	using TimePoint = Clock::time_point;
+	using Duration = Clock::duration;
+
+	/// cyclic buffer of durations of last frames
+	std::array<Duration, 60> lastFrameTimes;
+
+	Duration targetFrameTime;
+	TimePoint lastTimePoint;
+
+	/// index of last measured frome in lastFrameTimes array
+	ui32 lastFrameIndex;
+
+public:
+	FramerateManager(int targetFramerate);
+
+	/// must be called every frame
+	/// updates framerate calculations and executes sleep to maintain target frame rate
+	void framerateDelay();
+
+	/// returns duration of last frame in seconds
+	ui32 getElapsedMilliseconds() const;
+
+	/// returns current estimation of frame rate
+	ui32 getFramerate() const;
+};

+ 183 - 42
client/gui/InterfaceObjectConfigurable.cpp

@@ -26,6 +26,7 @@
 #include "../windows/InfoWindows.h"
 
 #include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/filesystem/ResourceID.h"
 
 InterfaceObjectConfigurable::InterfaceObjectConfigurable(const JsonNode & config, int used, Point offset):
 	InterfaceObjectConfigurable(used, offset)
@@ -46,6 +47,7 @@ InterfaceObjectConfigurable::InterfaceObjectConfigurable(int used, Point offset)
 	REGISTER_BUILDER("button", &InterfaceObjectConfigurable::buildButton);
 	REGISTER_BUILDER("labelGroup", &InterfaceObjectConfigurable::buildLabelGroup);
 	REGISTER_BUILDER("slider", &InterfaceObjectConfigurable::buildSlider);
+	REGISTER_BUILDER("layout", &InterfaceObjectConfigurable::buildLayout);
 }
 
 void InterfaceObjectConfigurable::registerBuilder(const std::string & type, BuilderFunction f)
@@ -65,32 +67,75 @@ void InterfaceObjectConfigurable::deleteWidget(const std::string & name)
 		widgets.erase(iter);
 }
 
+void InterfaceObjectConfigurable::loadCustomBuilders(const JsonNode & config)
+{
+	for(auto & item : config.Struct())
+	{
+		std::string typeName = item.first;
+		JsonNode baseConfig = item.second;
+
+		auto const & functor = [this, baseConfig](const JsonNode & widgetConfig) -> std::shared_ptr<CIntObject>
+		{
+			JsonNode actualConfig = widgetConfig;
+			JsonUtils::mergeCopy(actualConfig, baseConfig);
+
+			return this->buildWidget(actualConfig);
+		};
+		registerBuilder(typeName, functor);
+	}
+}
+
 void InterfaceObjectConfigurable::build(const JsonNode &config)
 {
 	OBJ_CONSTRUCTION;
+
 	logGlobal->debug("Building configurable interface object");
 	auto * items = &config;
 	
 	if(config.getType() == JsonNode::JsonType::DATA_STRUCT)
 	{
+		if (!config["library"].isNull())
+		{
+			const JsonNode library(ResourceID(config["library"].String()));
+			loadCustomBuilders(library);
+		}
+
+		loadCustomBuilders(config["customTypes"]);
+
 		for(auto & item : config["variables"].Struct())
 		{
 			logGlobal->debug("Read variable named %s", item.first);
 			variables[item.first] = item.second;
 		}
-		
+
 		items = &config["items"];
 	}
 	
-	const std::string unnamedObjectPrefix = "__widget_";
 	for(const auto & item : items->Vector())
-	{
-		std::string name = item["name"].isNull()
-						? unnamedObjectPrefix + std::to_string(unnamedObjectId++)
-						: item["name"].String();
-		logGlobal->debug("Building widget with name %s", name);
-		widgets[name] = buildWidget(item);
-	}
+		addWidget(item["name"].String(), buildWidget(item));
+}
+
+void InterfaceObjectConfigurable::addConditional(const std::string & name, bool active)
+{
+	conditionals[name] = active;
+}
+
+void InterfaceObjectConfigurable::addWidget(const std::string & namePreferred, std::shared_ptr<CIntObject> widget)
+{
+	static const std::string unnamedObjectPrefix = "__widget_";
+
+	std::string nameActual;
+
+	if (widgets.count(namePreferred) == 0)
+		nameActual = namePreferred;
+	else
+		logGlobal->error("Duplicated widget name: '%s'", namePreferred);
+
+	if (nameActual.empty())
+		nameActual = unnamedObjectPrefix + std::to_string(unnamedObjectId++);
+
+	logGlobal->debug("Building widget with name %s", nameActual);
+	widgets[nameActual] = widget;
 }
 
 std::string InterfaceObjectConfigurable::readText(const JsonNode & config) const
@@ -174,6 +219,8 @@ EFonts InterfaceObjectConfigurable::readFont(const JsonNode & config) const
 			return EFonts::FONT_SMALL;
 		if(config.String() == "tiny")
 			return EFonts::FONT_TINY;
+		if(config.String() == "calisto")
+			return EFonts::FONT_CALLI;
 	}
 	logGlobal->debug("Uknown font attribute");
 	return EFonts::FONT_TIMES;
@@ -211,7 +258,7 @@ EShortcut InterfaceObjectConfigurable::readHotkey(const JsonNode & config) const
 		return EShortcut::NONE;
 	}
 
-	EShortcut result = GH.shortcutsHandler().findShortcut(config.String());
+	EShortcut result = GH.shortcuts().findShortcut(config.String());
 	if (result == EShortcut::NONE)
 		logGlobal->error("Invalid hotkey '%s' in interface configuration!", config.String());
 	return result;;
@@ -255,7 +302,8 @@ std::shared_ptr<CToggleGroup> InterfaceObjectConfigurable::buildToggleGroup(cons
 		for(const auto & item : config["items"].Vector())
 		{
 			itemIdx = item["index"].isNull() ? itemIdx + 1 : item["index"].Integer();
-			group->addToggle(itemIdx, std::dynamic_pointer_cast<CToggleBase>(buildWidget(item)));
+			auto newToggle = std::dynamic_pointer_cast<CToggleButton>(buildWidget(item));
+			group->addToggle(itemIdx, newToggle);
 		}
 	}
 	if(!config["selected"].isNull())
@@ -287,15 +335,7 @@ std::shared_ptr<CToggleButton> InterfaceObjectConfigurable::buildToggleButton(co
 		assert(imgOrder.size() >= 4);
 		button->setImageOrder(imgOrder[0].Integer(), imgOrder[1].Integer(), imgOrder[2].Integer(), imgOrder[3].Integer());
 	}
-	if(!config["callback"].isNull())
-	{
-		std::string callbackName = config["callback"].String();
-
-		if (callbacks.count(callbackName))
-			button->addCallback(callbacks.at(callbackName));
-		else
-			logGlobal->error("Invalid callback '%s' in widget", callbackName );
-	}
+	loadToggleButtonCallback(button, config["callback"]);
 	return button;
 }
 
@@ -319,30 +359,67 @@ std::shared_ptr<CButton> InterfaceObjectConfigurable::buildButton(const JsonNode
 		assert(imgOrder.size() >= 4);
 		button->setImageOrder(imgOrder[0].Integer(), imgOrder[1].Integer(), imgOrder[2].Integer(), imgOrder[3].Integer());
 	}
-	if(!config["callback"].isNull())
-	{
-		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_STRING)
-		{
-			button->assignedKey = readHotkey(config["hotkey"]);
+	loadButtonBorderColor(button, config["borderColor"]);
+	loadButtonCallback(button, config["callback"]);
+	loadButtonHotkey(button, config["hotkey"]);
+	return button;
+}
 
-			auto target = shortcuts.find(button->assignedKey);
-			if (target != shortcuts.end())
-			{
-				button->addCallback(target->second.callback);
-				target->second.assignedToButton = true;
-			}
-		}
+void InterfaceObjectConfigurable::loadButtonBorderColor(std::shared_ptr<CButton> button, const JsonNode & config) const
+{
+	if (config.isNull())
+		return;
+
+	auto color = readColor(config);
+	button->setBorderColor(color);
+}
+
+void InterfaceObjectConfigurable::loadToggleButtonCallback(std::shared_ptr<CToggleButton> button, const JsonNode & config) const
+{
+	if(config.isNull())
+		return;
+
+	std::string callbackName = config.String();
+
+	if (callbacks.count(callbackName) > 0)
+		button->addCallback(callbacks.at(callbackName));
+	else
+		logGlobal->error("Invalid callback '%s' in widget", callbackName );
+}
+
+void InterfaceObjectConfigurable::loadButtonCallback(std::shared_ptr<CButton> button, const JsonNode & config) const
+{
+	if(config.isNull())
+		return;
+
+	std::string callbackName = config.String();
+
+	if (callbacks.count(callbackName) > 0)
+		button->addCallback(std::bind(callbacks.at(callbackName), 0));
+	else
+		logGlobal->error("Invalid callback '%s' in widget", callbackName );
+}
+
+void InterfaceObjectConfigurable::loadButtonHotkey(std::shared_ptr<CButton> button, const JsonNode & config) const
+{
+	if(config.isNull())
+		return;
+
+	if(config.getType() != JsonNode::JsonType::DATA_STRING)
+	{
+		logGlobal->error("Invalid shortcut format - string expected!");
+		return;
 	}
-	return button;
+
+	button->assignedKey = readHotkey(config);
+
+	auto target = shortcuts.find(button->assignedKey);
+	if (target == shortcuts.end())
+		return;
+
+	button->addCallback(target->second.callback);
+	target->second.assignedToButton = true;
 }
 
 std::shared_ptr<CLabelGroup> InterfaceObjectConfigurable::buildLabelGroup(const JsonNode & config) const
@@ -374,7 +451,8 @@ std::shared_ptr<CSlider> InterfaceObjectConfigurable::buildSlider(const JsonNode
 	auto itemsTotal = config["itemsTotal"].Integer();
 	auto value = config["selected"].Integer();
 	bool horizontal = config["orientation"].String() == "horizontal";
-	auto const & result = std::make_shared<CSlider>(position, length, callbacks.at(config["callback"].String()), itemsVisible, itemsTotal, value, horizontal, style);
+	const auto & result =
+		std::make_shared<CSlider>(position, length, callbacks.at(config["callback"].String()), itemsVisible, itemsTotal, value, horizontal, style);
 
 	if (!config["scrollBounds"].isNull())
 	{
@@ -403,6 +481,69 @@ std::shared_ptr<CFilledTexture> InterfaceObjectConfigurable::buildTexture(const
 	return std::make_shared<CFilledTexture>(image, rect);
 }
 
+/// Small helper class that provides ownership for shared_ptr's of child elements
+class InterfaceLayoutWidget : public CIntObject
+{
+public:
+	std::vector<std::shared_ptr<CIntObject>> ownedChildren;
+};
+
+std::shared_ptr<CIntObject> InterfaceObjectConfigurable::buildLayout(const JsonNode & config)
+{
+	logGlobal->debug("Building widget Layout");
+	bool vertical = config["vertical"].Bool();
+	bool horizontal = config["horizontal"].Bool();
+	bool dynamic = config["dynamic"].Bool();
+	int distance = config["distance"].Integer();
+	std::string customType = config["customType"].String();
+	auto position = readPosition(config["position"]);
+
+	auto result = std::make_shared<InterfaceLayoutWidget>();
+	result->moveBy(position);
+	Point layoutPosition;
+
+	for(auto item : config["items"].Vector())
+	{
+		if (item["type"].String().empty())
+			item["type"].String() = customType;
+
+		if (!item["created"].isNull())
+		{
+			std::string name = item["created"].String();
+
+			if (conditionals.count(name) != 0)
+			{
+				if (!conditionals.at(name))
+					continue;
+			}
+			else
+			{
+				logMod->warn("Unknown condition %s in widget!", name);
+			}
+		}
+
+		auto widget = buildWidget(item);
+
+		addWidget(item["name"].String(), widget);
+		result->ownedChildren.push_back(widget);
+		result->addChild(widget.get(), false);
+
+		widget->moveBy(position + layoutPosition);
+
+		if (dynamic && vertical)
+			layoutPosition.y += widget->pos.h;
+		if (dynamic && horizontal)
+			layoutPosition.x += widget->pos.w;
+
+		if (vertical)
+			layoutPosition.y += distance;
+		if (horizontal)
+			layoutPosition.x += distance;
+	}
+
+	return result;
+}
+
 std::shared_ptr<CShowableAnim> InterfaceObjectConfigurable::buildAnimation(const JsonNode & config) const
 {
 	logGlobal->debug("Building widget CShowableAnim");

+ 13 - 0
client/gui/InterfaceObjectConfigurable.h

@@ -45,9 +45,15 @@ protected:
 
 	using BuilderFunction = std::function<std::shared_ptr<CIntObject>(const JsonNode &)>;
 	void registerBuilder(const std::string &, BuilderFunction);
+
+	void loadCustomBuilders(const JsonNode & config);
 	
 	//must be called after adding callbacks
 	void build(const JsonNode & config);
+
+	void addConditional(const std::string & name, bool active);
+
+	void addWidget(const std::string & name, std::shared_ptr<CIntObject> widget);
 	
 	void addCallback(const std::string & callbackName, std::function<void(int)> callback);
 	JsonNode variables;
@@ -73,6 +79,11 @@ protected:
 	std::pair<std::string, std::string> readHintText(const JsonNode &) const;
 	EShortcut readHotkey(const JsonNode &) const;
 	
+	void loadToggleButtonCallback(std::shared_ptr<CToggleButton> button, const JsonNode & config) const;
+	void loadButtonCallback(std::shared_ptr<CButton> button, const JsonNode & config) const;
+	void loadButtonHotkey(std::shared_ptr<CButton> button, const JsonNode & config) const;
+	void loadButtonBorderColor(std::shared_ptr<CButton> button, const JsonNode & config) const;
+
 	//basic widgets
 	std::shared_ptr<CPicture> buildPicture(const JsonNode &) const;
 	std::shared_ptr<CLabel> buildLabel(const JsonNode &) const;
@@ -84,6 +95,7 @@ protected:
 	std::shared_ptr<CAnimImage> buildImage(const JsonNode &) const;
 	std::shared_ptr<CShowableAnim> buildAnimation(const JsonNode &) const;
 	std::shared_ptr<CFilledTexture> buildTexture(const JsonNode &) const;
+	std::shared_ptr<CIntObject> buildLayout(const JsonNode &);
 		
 	//composite widgets
 	std::shared_ptr<CIntObject> buildWidget(JsonNode config) const;
@@ -100,5 +112,6 @@ private:
 	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<std::string, bool> conditionals;
 	std::map<EShortcut, ShortcutState> shortcuts;
 };

+ 10 - 3
client/gui/Shortcut.h

@@ -81,7 +81,9 @@ enum class EShortcut
 	// 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_TOGGLE_SLEEP, // Toggles hero sleep status
+	ADVENTURE_SET_HERO_ASLEEP, // Moves hero to sleep state
+	ADVENTURE_SET_HERO_AWAKE, // Move hero to awake state
 	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
@@ -94,12 +96,18 @@ enum class EShortcut
 	ADVENTURE_DIG_GRAIL,
 	ADVENTURE_VIEW_PUZZLE,
 	ADVENTURE_VIEW_WORLD,
+	ADVENTURE_VIEW_WORLD_X1,
+	ADVENTURE_VIEW_WORLD_X2,
+	ADVENTURE_VIEW_WORLD_X4,
 	ADVENTURE_TOGGLE_MAP_LEVEL,
 	ADVENTURE_KINGDOM_OVERVIEW,
 	ADVENTURE_QUEST_LOG,
 	ADVENTURE_CAST_SPELL,
-	ADVENTURE_END_TURN,
 	ADVENTURE_THIEVES_GUILD,
+	ADVENTURE_EXIT_WORLD_VIEW,
+	ADVENTURE_ZOOM_IN,
+	ADVENTURE_ZOOM_OUT,
+	ADVENTURE_ZOOM_RESET,
 
 	// Move hero one tile in specified direction. Bound to cursors & numpad buttons
 	ADVENTURE_MOVE_HERO_SW,
@@ -153,4 +161,3 @@ enum class EShortcut
 
 	AFTER_LAST
 };
-

+ 19 - 86
client/gui/ShortcutHandler.cpp

@@ -64,6 +64,8 @@ std::vector<EShortcut> ShortcutHandler::translateKeycode(SDL_Keycode key) const
 		{SDLK_RETURN,    EShortcut::LOBBY_LOAD_GAME           },
 		{SDLK_KP_ENTER,  EShortcut::LOBBY_LOAD_GAME           },
 		{SDLK_s,         EShortcut::LOBBY_SAVE_GAME           },
+		{SDLK_RETURN,    EShortcut::LOBBY_SAVE_GAME           },
+		{SDLK_KP_ENTER,  EShortcut::LOBBY_SAVE_GAME           },
 		{SDLK_r,         EShortcut::LOBBY_RANDOM_MAP          },
 		{SDLK_h,         EShortcut::LOBBY_HIDE_CHAT           },
 		{SDLK_a,         EShortcut::LOBBY_ADDITIONAL_OPTIONS  },
@@ -79,8 +81,8 @@ std::vector<EShortcut> ShortcutHandler::translateKeycode(SDL_Keycode key) const
 		{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_z,         EShortcut::ADVENTURE_SET_HERO_ASLEEP },
+		{SDLK_w,         EShortcut::ADVENTURE_SET_HERO_AWAKE  },
 		{SDLK_m,         EShortcut::ADVENTURE_MOVE_HERO       },
 		{SDLK_SPACE,     EShortcut::ADVENTURE_VISIT_OBJECT    },
 		{SDLK_KP_1,      EShortcut::ADVENTURE_MOVE_HERO_SW    },
@@ -106,12 +108,17 @@ std::vector<EShortcut> ShortcutHandler::translateKeycode(SDL_Keycode key) const
 		{SDLK_d,         EShortcut::ADVENTURE_DIG_GRAIL       },
 		{SDLK_p,         EShortcut::ADVENTURE_VIEW_PUZZLE     },
 		{SDLK_v,         EShortcut::ADVENTURE_VIEW_WORLD      },
+		{SDLK_1,         EShortcut::ADVENTURE_VIEW_WORLD_X1   },
+		{SDLK_2,         EShortcut::ADVENTURE_VIEW_WORLD_X2   },
+		{SDLK_4,         EShortcut::ADVENTURE_VIEW_WORLD_X4   },
 		{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_KP_PLUS,   EShortcut::ADVENTURE_ZOOM_IN         },
+		{SDLK_KP_MINUS,  EShortcut::ADVENTURE_ZOOM_OUT        },
+		{SDLK_BACKSPACE, EShortcut::ADVENTURE_ZOOM_RESET      },
 		{SDLK_q,         EShortcut::BATTLE_TOGGLE_QUEUE       },
 		{SDLK_c,         EShortcut::BATTLE_USE_CREATURE_SPELL },
 		{SDLK_s,         EShortcut::BATTLE_SURRENDER          },
@@ -218,6 +225,8 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 		{"adventureGameOptions",     EShortcut::ADVENTURE_GAME_OPTIONS    },
 		{"adventureToggleGrid",      EShortcut::ADVENTURE_TOGGLE_GRID     },
 		{"adventureToggleSleep",     EShortcut::ADVENTURE_TOGGLE_SLEEP    },
+		{"adventureSetHeroAsleep",   EShortcut::ADVENTURE_SET_HERO_ASLEEP },
+		{"adventureSetHeroAwake",    EShortcut::ADVENTURE_SET_HERO_AWAKE  },
 		{"adventureMoveHero",        EShortcut::ADVENTURE_MOVE_HERO       },
 		{"adventureVisitObject",     EShortcut::ADVENTURE_VISIT_OBJECT    },
 		{"adventureMoveHeroSW",      EShortcut::ADVENTURE_MOVE_HERO_SW    },
@@ -238,12 +247,18 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 		{"adventureDigGrail",        EShortcut::ADVENTURE_DIG_GRAIL       },
 		{"adventureViewPuzzle",      EShortcut::ADVENTURE_VIEW_PUZZLE     },
 		{"adventureViewWorld",       EShortcut::ADVENTURE_VIEW_WORLD      },
+		{"adventureViewWorld1",      EShortcut::ADVENTURE_VIEW_WORLD_X1   },
+		{"adventureViewWorld2",      EShortcut::ADVENTURE_VIEW_WORLD_X2   },
+		{"adventureViewWorld4",      EShortcut::ADVENTURE_VIEW_WORLD_X4   },
 		{"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   },
+		{"adventureExitWorldView",   EShortcut::ADVENTURE_EXIT_WORLD_VIEW },
+		{"adventureZoomIn",          EShortcut::ADVENTURE_ZOOM_IN         },
+		{"adventureZoomOut",         EShortcut::ADVENTURE_ZOOM_OUT        },
+		{"adventureZoomReset",       EShortcut::ADVENTURE_ZOOM_RESET      },
 		{"battleToggleQueue",        EShortcut::BATTLE_TOGGLE_QUEUE       },
 		{"battleUseCreatureSpell",   EShortcut::BATTLE_USE_CREATURE_SPELL },
 		{"battleSurrender",          EShortcut::BATTLE_SURRENDER          },
@@ -278,85 +293,3 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 		return shortcutNames.at(identifier);
 	return EShortcut::NONE;
 }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

Неке датотеке нису приказане због велике количине промена