瀏覽代碼

Merge branch 'josch/dos2unix' into develop

Ivan Savenko 2 年之前
父節點
當前提交
3880ea58b9
共有 100 個文件被更改,包括 32566 次插入32566 次删除
  1. 292 292
      AI/BattleAI/BattleAI.cpp
  2. 11 11
      AI/BattleAI/StdInc.cpp
  3. 17 17
      AI/BattleAI/StdInc.h
  4. 33 33
      AI/BattleAI/main.cpp
  5. 82 82
      AI/EmptyAI/CEmptyAI.cpp
  6. 38 38
      AI/EmptyAI/CEmptyAI.h
  7. 1 1
      AI/EmptyAI/StdInc.cpp
  8. 9 9
      AI/EmptyAI/StdInc.h
  9. 28 28
      AI/EmptyAI/main.cpp
  10. 736 736
      AI/GeniusAI.brain
  11. 1 1
      AI/StupidAI/StdInc.cpp
  12. 9 9
      AI/StupidAI/StdInc.h
  13. 333 333
      AI/StupidAI/StupidAI.cpp
  14. 56 56
      AI/StupidAI/StupidAI.h
  15. 34 34
      AI/StupidAI/main.cpp
  16. 259 259
      AI/VCAI/AIUtility.cpp
  17. 2912 2912
      AI/VCAI/VCAI.cpp
  18. 407 407
      AI/VCAI/VCAI.h
  19. 53 53
      AUTHORS.h
  20. 423 423
      CCallback.cpp
  21. 197 197
      CCallback.h
  22. 2160 2160
      ChangeLog.md
  23. 692 692
      Global.h
  24. 39 39
      Mods/vcmi/Data/s/std.verm
  25. 13 13
      Mods/vcmi/Data/s/testy.erm
  26. 369 369
      Mods/vcmi/config/vcmi/german.json
  27. 372 372
      Mods/vcmi/config/vcmi/rmg/hdmod/blockbuster.JSON
  28. 239 239
      Mods/vcmi/config/vcmi/rmg/hdmod/coldshadowsFantasy.json
  29. 362 362
      Mods/vcmi/config/vcmi/rmg/hdmod/extreme.JSON
  30. 310 310
      Mods/vcmi/config/vcmi/rmg/hdmod/extreme2.JSON
  31. 83 83
      Mods/vcmi/config/vcmi/rmg/hdmod/poorJebus.JSON
  32. 140 140
      Mods/vcmi/config/vcmi/rmg/hdmod/reckless.JSON
  33. 139 139
      Mods/vcmi/config/vcmi/rmg/hdmod/roadrunner.JSON
  34. 111 111
      Mods/vcmi/config/vcmi/rmg/hdmod/superslam.JSON
  35. 146 146
      Mods/vcmi/config/vcmi/rmg/heroes3/readyOrNot.JSON
  36. 232 232
      Mods/vcmi/config/vcmi/rmg/heroes3/worldsAtWar.JSON
  37. 361 361
      Mods/vcmi/config/vcmi/rmg/heroes3unused/gauntlet.JSON
  38. 24 24
      android/vcmi-app/src/main/java/eu/vcmi/vcmi/Const.java
  39. 115 115
      client/CGameInfo.cpp
  40. 101 101
      client/CGameInfo.h
  41. 529 529
      client/CMT.cpp
  42. 718 718
      client/CMusicHandler.cpp
  43. 167 167
      client/CMusicHandler.h
  44. 1891 1891
      client/CPlayerInterface.cpp
  45. 234 234
      client/CPlayerInterface.h
  46. 665 665
      client/CVideoHandler.cpp
  47. 121 121
      client/CVideoHandler.h
  48. 740 740
      client/Client.cpp
  49. 240 240
      client/Client.h
  50. 1042 1042
      client/NetPacksClient.cpp
  51. 285 285
      client/PlayerLocalState.cpp
  52. 113 113
      client/PlayerLocalState.h
  53. 1 1
      client/StdInc.cpp
  54. 9 9
      client/StdInc.h
  55. 895 895
      client/adventureMap/AdventureMapInterface.cpp
  56. 194 194
      client/adventureMap/AdventureMapInterface.h
  57. 455 455
      client/adventureMap/AdventureMapWidget.cpp
  58. 110 110
      client/adventureMap/AdventureMapWidget.h
  59. 61 61
      client/adventureMap/AdventureOptions.cpp
  60. 31 31
      client/adventureMap/AdventureOptions.h
  61. 90 90
      client/adventureMap/CResDataBar.cpp
  62. 40 40
      client/adventureMap/CResDataBar.h
  63. 252 252
      client/adventureMap/MapAudioPlayer.cpp
  64. 77 77
      client/adventureMap/MapAudioPlayer.h
  65. 1054 1054
      client/battle/BattleActionsController.cpp
  66. 125 125
      client/battle/BattleActionsController.h
  67. 1137 1137
      client/battle/BattleAnimationClasses.cpp
  68. 372 372
      client/battle/BattleAnimationClasses.h
  69. 100 100
      client/battle/BattleConstants.h
  70. 160 160
      client/battle/BattleEffectsController.cpp
  71. 75 75
      client/battle/BattleEffectsController.h
  72. 919 919
      client/battle/BattleFieldController.cpp
  73. 144 144
      client/battle/BattleFieldController.h
  74. 828 828
      client/battle/BattleInterface.cpp
  75. 230 230
      client/battle/BattleInterface.h
  76. 921 921
      client/battle/BattleInterfaceClasses.cpp
  77. 238 238
      client/battle/BattleInterfaceClasses.h
  78. 228 228
      client/battle/BattleObstacleController.cpp
  79. 68 68
      client/battle/BattleObstacleController.h
  80. 386 386
      client/battle/BattleProjectileController.cpp
  81. 125 125
      client/battle/BattleProjectileController.h
  82. 76 76
      client/battle/BattleRenderer.cpp
  83. 53 53
      client/battle/BattleRenderer.h
  84. 367 367
      client/battle/BattleSiegeController.cpp
  85. 111 111
      client/battle/BattleSiegeController.h
  86. 894 894
      client/battle/BattleStacksController.cpp
  87. 147 147
      client/battle/BattleStacksController.h
  88. 687 687
      client/battle/BattleWindow.cpp
  89. 118 118
      client/battle/BattleWindow.h
  90. 433 433
      client/battle/CreatureAnimation.cpp
  91. 158 158
      client/battle/CreatureAnimation.h
  92. 248 248
      client/gui/CGuiHandler.cpp
  93. 130 130
      client/gui/CGuiHandler.h
  94. 346 346
      client/gui/CIntObject.cpp
  95. 191 191
      client/gui/CIntObject.h
  96. 292 292
      client/gui/CursorHandler.cpp
  97. 182 182
      client/gui/CursorHandler.h
  98. 19 19
      client/gui/MouseButton.h
  99. 12 12
      client/gui/TextAlignment.h
  100. 93 93
      client/mapView/IMapRendererContext.h

+ 292 - 292
AI/BattleAI/BattleAI.cpp

@@ -1,292 +1,292 @@
-/*
- * BattleAI.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 "BattleAI.h"
-#include "BattleEvaluator.h"
-#include "BattleExchangeVariant.h"
-
-#include "StackWithBonuses.h"
-#include "EnemyInfo.h"
-#include "tbb/parallel_for.h"
-#include "../../lib/CStopWatch.h"
-#include "../../lib/CThreadHelper.h"
-#include "../../lib/mapObjects/CGTownInstance.h"
-#include "../../lib/spells/CSpellHandler.h"
-#include "../../lib/spells/ISpellMechanics.h"
-#include "../../lib/battle/BattleAction.h"
-#include "../../lib/battle/BattleStateInfoForRetreat.h"
-#include "../../lib/battle/CObstacleInstance.h"
-#include "../../lib/CStack.h" // TODO: remove
-                              // Eventually only IBattleInfoCallback and battle::Unit should be used,
-                              // CUnitState should be private and CStack should be removed completely
-
-#define LOGL(text) print(text)
-#define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl))
-
-CBattleAI::CBattleAI()
-	: side(-1),
-	wasWaitingForRealize(false),
-	wasUnlockingGs(false)
-{
-}
-
-CBattleAI::~CBattleAI()
-{
-	if(cb)
-	{
-		//Restore previous state of CB - it may be shared with the main AI (like VCAI)
-		cb->waitTillRealize = wasWaitingForRealize;
-		cb->unlockGsWhenWaiting = wasUnlockingGs;
-	}
-}
-
-void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
-{
-	setCbc(CB);
-	env = ENV;
-	cb = CB;
-	playerID = *CB->getPlayerID();
-	wasWaitingForRealize = CB->waitTillRealize;
-	wasUnlockingGs = CB->unlockGsWhenWaiting;
-	CB->waitTillRealize = false;
-	CB->unlockGsWhenWaiting = false;
-	movesSkippedByDefense = 0;
-}
-
-void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB, AutocombatPreferences autocombatPreferences)
-{
-	initBattleInterface(ENV, CB);
-	autobattlePreferences = autocombatPreferences;
-}
-
-BattleAction CBattleAI::useHealingTent(const BattleID & battleID, const CStack *stack)
-{
-	auto healingTargets = cb->getBattle(battleID)->battleGetStacks(CBattleInfoEssentials::ONLY_MINE);
-	std::map<int, const CStack*> woundHpToStack;
-	for(const auto * stack : healingTargets)
-	{
-		if(auto woundHp = stack->getMaxHealth() - stack->getFirstHPleft())
-			woundHpToStack[woundHp] = stack;
-	}
-
-	if(woundHpToStack.empty())
-		return BattleAction::makeDefend(stack);
-	else
-		return BattleAction::makeHeal(stack, woundHpToStack.rbegin()->second); //last element of the woundHpToStack is the most wounded stack
-}
-
-void CBattleAI::yourTacticPhase(const BattleID & battleID, int distance)
-{
-	cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide()));
-}
-
-static float getStrengthRatio(std::shared_ptr<CBattleInfoCallback> cb, int side)
-{
-	auto stacks = cb->battleGetAllStacks();
-	auto our = 0, enemy = 0;
-
-	for(auto stack : stacks)
-	{
-		auto creature = stack->creatureId().toCreature();
-
-		if(!creature)
-			continue;
-
-		if(stack->unitSide() == side)
-			our += stack->getCount() * creature->getAIValue();
-		else
-			enemy += stack->getCount() * creature->getAIValue();
-	}
-
-	return enemy == 0 ? 1.0f : static_cast<float>(our) / enemy;
-}
-
-void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
-{
-	LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName());
-
-	auto timeElapsed = [](std::chrono::time_point<std::chrono::high_resolution_clock> start) -> uint64_t
-	{
-		auto end = std::chrono::high_resolution_clock::now();
-
-		return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
-	};
-
-	BattleAction result = BattleAction::makeDefend(stack);
-	setCbc(cb); //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too)
-
-	auto start = std::chrono::high_resolution_clock::now();
-
-	try
-	{
-		if(stack->creatureId() == CreatureID::CATAPULT)
-		{
-			cb->battleMakeUnitAction(battleID, useCatapult(battleID, stack));
-			return;
-		}
-		if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON) && stack->hasBonusOfType(BonusType::HEALER))
-		{
-			cb->battleMakeUnitAction(battleID, useHealingTent(battleID, stack));
-			return;
-		}
-
-#if BATTLE_TRACE_LEVEL>=1
-		logAi->trace("Build evaluator and targets");
-#endif
-
-		BattleEvaluator evaluator(env, cb, stack, playerID, battleID, side, getStrengthRatio(cb->getBattle(battleID), side));
-
-		result = evaluator.selectStackAction(stack);
-
-		if(!skipCastUntilNextBattle && evaluator.canCastSpell())
-		{
-			auto spelCasted = evaluator.attemptCastingSpell(stack);
-
-			if(spelCasted)
-				return;
-			
-			skipCastUntilNextBattle = true;
-		}
-
-		logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start));
-
-		if(auto action = considerFleeingOrSurrendering(battleID))
-		{
-			cb->battleMakeUnitAction(battleID, *action);
-			return;
-		}
-	}
-	catch(boost::thread_interrupted &)
-	{
-		throw;
-	}
-	catch(std::exception &e)
-	{
-		logAi->error("Exception occurred in %s %s",__FUNCTION__, e.what());
-	}
-
-	if(result.actionType == EActionType::DEFEND)
-	{
-		movesSkippedByDefense++;
-	}
-	else if(result.actionType != EActionType::WAIT)
-	{
-		movesSkippedByDefense = 0;
-	}
-
-	logAi->trace("BattleAI decission made in %lld", timeElapsed(start));
-
-	cb->battleMakeUnitAction(battleID, result);
-}
-
-BattleAction CBattleAI::useCatapult(const BattleID & battleID, const CStack * stack)
-{
-	BattleAction attack;
-	BattleHex targetHex = BattleHex::INVALID;
-
-	if(cb->getBattle(battleID)->battleGetGateState() == EGateState::CLOSED)
-	{
-		targetHex = cb->getBattle(battleID)->wallPartToBattleHex(EWallPart::GATE);
-	}
-	else
-	{
-		EWallPart wallParts[] = {
-			EWallPart::KEEP,
-			EWallPart::BOTTOM_TOWER,
-			EWallPart::UPPER_TOWER,
-			EWallPart::BELOW_GATE,
-			EWallPart::OVER_GATE,
-			EWallPart::BOTTOM_WALL,
-			EWallPart::UPPER_WALL
-		};
-
-		for(auto wallPart : wallParts)
-		{
-			auto wallState = cb->getBattle(battleID)->battleGetWallState(wallPart);
-
-			if(wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED)
-			{
-				targetHex = cb->getBattle(battleID)->wallPartToBattleHex(wallPart);
-				break;
-			}
-		}
-	}
-
-	if(!targetHex.isValid())
-	{
-		return BattleAction::makeDefend(stack);
-	}
-
-	attack.aimToHex(targetHex);
-	attack.actionType = EActionType::CATAPULT;
-	attack.side = side;
-	attack.stackNumber = stack->unitId();
-
-	movesSkippedByDefense = 0;
-
-	return attack;
-}
-
-void CBattleAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
-{
-	LOG_TRACE(logAi);
-	side = Side;
-
-	skipCastUntilNextBattle = false;
-}
-
-void CBattleAI::print(const std::string &text) const
-{
-	logAi->trace("%s Battle AI[%p]: %s", playerID.toString(), this, text);
-}
-
-std::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering(const BattleID & battleID)
-{
-	BattleStateInfoForRetreat bs;
-
-	bs.canFlee = cb->getBattle(battleID)->battleCanFlee();
-	bs.canSurrender = cb->getBattle(battleID)->battleCanSurrender(playerID);
-	bs.ourSide = cb->getBattle(battleID)->battleGetMySide();
-	bs.ourHero = cb->getBattle(battleID)->battleGetMyHero();
-	bs.enemyHero = nullptr;
-
-	for(auto stack : cb->getBattle(battleID)->battleGetAllStacks(false))
-	{
-		if(stack->alive())
-		{
-			if(stack->unitSide() == bs.ourSide)
-				bs.ourStacks.push_back(stack);
-			else
-			{
-				bs.enemyStacks.push_back(stack);
-				bs.enemyHero = cb->getBattle(battleID)->battleGetOwnerHero(stack);
-			}
-		}
-	}
-
-	bs.turnsSkippedByDefense = movesSkippedByDefense / bs.ourStacks.size();
-
-	if(!bs.canFlee && !bs.canSurrender)
-	{
-		return std::nullopt;
-	}
-
-	auto result = cb->makeSurrenderRetreatDecision(battleID, bs);
-
-	if(!result && bs.canFlee && bs.turnsSkippedByDefense > 30)
-	{
-		return BattleAction::makeRetreat(bs.ourSide);
-	}
-
-	return result;
-}
-
-
-
+/*
+ * BattleAI.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 "BattleAI.h"
+#include "BattleEvaluator.h"
+#include "BattleExchangeVariant.h"
+
+#include "StackWithBonuses.h"
+#include "EnemyInfo.h"
+#include "tbb/parallel_for.h"
+#include "../../lib/CStopWatch.h"
+#include "../../lib/CThreadHelper.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
+#include "../../lib/spells/CSpellHandler.h"
+#include "../../lib/spells/ISpellMechanics.h"
+#include "../../lib/battle/BattleAction.h"
+#include "../../lib/battle/BattleStateInfoForRetreat.h"
+#include "../../lib/battle/CObstacleInstance.h"
+#include "../../lib/CStack.h" // TODO: remove
+                              // Eventually only IBattleInfoCallback and battle::Unit should be used,
+                              // CUnitState should be private and CStack should be removed completely
+
+#define LOGL(text) print(text)
+#define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl))
+
+CBattleAI::CBattleAI()
+	: side(-1),
+	wasWaitingForRealize(false),
+	wasUnlockingGs(false)
+{
+}
+
+CBattleAI::~CBattleAI()
+{
+	if(cb)
+	{
+		//Restore previous state of CB - it may be shared with the main AI (like VCAI)
+		cb->waitTillRealize = wasWaitingForRealize;
+		cb->unlockGsWhenWaiting = wasUnlockingGs;
+	}
+}
+
+void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
+{
+	setCbc(CB);
+	env = ENV;
+	cb = CB;
+	playerID = *CB->getPlayerID();
+	wasWaitingForRealize = CB->waitTillRealize;
+	wasUnlockingGs = CB->unlockGsWhenWaiting;
+	CB->waitTillRealize = false;
+	CB->unlockGsWhenWaiting = false;
+	movesSkippedByDefense = 0;
+}
+
+void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB, AutocombatPreferences autocombatPreferences)
+{
+	initBattleInterface(ENV, CB);
+	autobattlePreferences = autocombatPreferences;
+}
+
+BattleAction CBattleAI::useHealingTent(const BattleID & battleID, const CStack *stack)
+{
+	auto healingTargets = cb->getBattle(battleID)->battleGetStacks(CBattleInfoEssentials::ONLY_MINE);
+	std::map<int, const CStack*> woundHpToStack;
+	for(const auto * stack : healingTargets)
+	{
+		if(auto woundHp = stack->getMaxHealth() - stack->getFirstHPleft())
+			woundHpToStack[woundHp] = stack;
+	}
+
+	if(woundHpToStack.empty())
+		return BattleAction::makeDefend(stack);
+	else
+		return BattleAction::makeHeal(stack, woundHpToStack.rbegin()->second); //last element of the woundHpToStack is the most wounded stack
+}
+
+void CBattleAI::yourTacticPhase(const BattleID & battleID, int distance)
+{
+	cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide()));
+}
+
+static float getStrengthRatio(std::shared_ptr<CBattleInfoCallback> cb, int side)
+{
+	auto stacks = cb->battleGetAllStacks();
+	auto our = 0, enemy = 0;
+
+	for(auto stack : stacks)
+	{
+		auto creature = stack->creatureId().toCreature();
+
+		if(!creature)
+			continue;
+
+		if(stack->unitSide() == side)
+			our += stack->getCount() * creature->getAIValue();
+		else
+			enemy += stack->getCount() * creature->getAIValue();
+	}
+
+	return enemy == 0 ? 1.0f : static_cast<float>(our) / enemy;
+}
+
+void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
+{
+	LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName());
+
+	auto timeElapsed = [](std::chrono::time_point<std::chrono::high_resolution_clock> start) -> uint64_t
+	{
+		auto end = std::chrono::high_resolution_clock::now();
+
+		return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
+	};
+
+	BattleAction result = BattleAction::makeDefend(stack);
+	setCbc(cb); //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too)
+
+	auto start = std::chrono::high_resolution_clock::now();
+
+	try
+	{
+		if(stack->creatureId() == CreatureID::CATAPULT)
+		{
+			cb->battleMakeUnitAction(battleID, useCatapult(battleID, stack));
+			return;
+		}
+		if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON) && stack->hasBonusOfType(BonusType::HEALER))
+		{
+			cb->battleMakeUnitAction(battleID, useHealingTent(battleID, stack));
+			return;
+		}
+
+#if BATTLE_TRACE_LEVEL>=1
+		logAi->trace("Build evaluator and targets");
+#endif
+
+		BattleEvaluator evaluator(env, cb, stack, playerID, battleID, side, getStrengthRatio(cb->getBattle(battleID), side));
+
+		result = evaluator.selectStackAction(stack);
+
+		if(!skipCastUntilNextBattle && evaluator.canCastSpell())
+		{
+			auto spelCasted = evaluator.attemptCastingSpell(stack);
+
+			if(spelCasted)
+				return;
+			
+			skipCastUntilNextBattle = true;
+		}
+
+		logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start));
+
+		if(auto action = considerFleeingOrSurrendering(battleID))
+		{
+			cb->battleMakeUnitAction(battleID, *action);
+			return;
+		}
+	}
+	catch(boost::thread_interrupted &)
+	{
+		throw;
+	}
+	catch(std::exception &e)
+	{
+		logAi->error("Exception occurred in %s %s",__FUNCTION__, e.what());
+	}
+
+	if(result.actionType == EActionType::DEFEND)
+	{
+		movesSkippedByDefense++;
+	}
+	else if(result.actionType != EActionType::WAIT)
+	{
+		movesSkippedByDefense = 0;
+	}
+
+	logAi->trace("BattleAI decission made in %lld", timeElapsed(start));
+
+	cb->battleMakeUnitAction(battleID, result);
+}
+
+BattleAction CBattleAI::useCatapult(const BattleID & battleID, const CStack * stack)
+{
+	BattleAction attack;
+	BattleHex targetHex = BattleHex::INVALID;
+
+	if(cb->getBattle(battleID)->battleGetGateState() == EGateState::CLOSED)
+	{
+		targetHex = cb->getBattle(battleID)->wallPartToBattleHex(EWallPart::GATE);
+	}
+	else
+	{
+		EWallPart wallParts[] = {
+			EWallPart::KEEP,
+			EWallPart::BOTTOM_TOWER,
+			EWallPart::UPPER_TOWER,
+			EWallPart::BELOW_GATE,
+			EWallPart::OVER_GATE,
+			EWallPart::BOTTOM_WALL,
+			EWallPart::UPPER_WALL
+		};
+
+		for(auto wallPart : wallParts)
+		{
+			auto wallState = cb->getBattle(battleID)->battleGetWallState(wallPart);
+
+			if(wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED)
+			{
+				targetHex = cb->getBattle(battleID)->wallPartToBattleHex(wallPart);
+				break;
+			}
+		}
+	}
+
+	if(!targetHex.isValid())
+	{
+		return BattleAction::makeDefend(stack);
+	}
+
+	attack.aimToHex(targetHex);
+	attack.actionType = EActionType::CATAPULT;
+	attack.side = side;
+	attack.stackNumber = stack->unitId();
+
+	movesSkippedByDefense = 0;
+
+	return attack;
+}
+
+void CBattleAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
+{
+	LOG_TRACE(logAi);
+	side = Side;
+
+	skipCastUntilNextBattle = false;
+}
+
+void CBattleAI::print(const std::string &text) const
+{
+	logAi->trace("%s Battle AI[%p]: %s", playerID.toString(), this, text);
+}
+
+std::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering(const BattleID & battleID)
+{
+	BattleStateInfoForRetreat bs;
+
+	bs.canFlee = cb->getBattle(battleID)->battleCanFlee();
+	bs.canSurrender = cb->getBattle(battleID)->battleCanSurrender(playerID);
+	bs.ourSide = cb->getBattle(battleID)->battleGetMySide();
+	bs.ourHero = cb->getBattle(battleID)->battleGetMyHero();
+	bs.enemyHero = nullptr;
+
+	for(auto stack : cb->getBattle(battleID)->battleGetAllStacks(false))
+	{
+		if(stack->alive())
+		{
+			if(stack->unitSide() == bs.ourSide)
+				bs.ourStacks.push_back(stack);
+			else
+			{
+				bs.enemyStacks.push_back(stack);
+				bs.enemyHero = cb->getBattle(battleID)->battleGetOwnerHero(stack);
+			}
+		}
+	}
+
+	bs.turnsSkippedByDefense = movesSkippedByDefense / bs.ourStacks.size();
+
+	if(!bs.canFlee && !bs.canSurrender)
+	{
+		return std::nullopt;
+	}
+
+	auto result = cb->makeSurrenderRetreatDecision(battleID, bs);
+
+	if(!result && bs.canFlee && bs.turnsSkippedByDefense > 30)
+	{
+		return BattleAction::makeRetreat(bs.ourSide);
+	}
+
+	return result;
+}
+
+
+

+ 11 - 11
AI/BattleAI/StdInc.cpp

@@ -1,11 +1,11 @@
-/*
- * StdInc.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
- *
- */
-// Creates the precompiled header
-#include "StdInc.h"
+/*
+ * StdInc.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
+ *
+ */
+// Creates the precompiled header
+#include "StdInc.h"

+ 17 - 17
AI/BattleAI/StdInc.h

@@ -1,17 +1,17 @@
-/*
- * StdInc.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 "../../Global.h"
-
-// This header should be treated as a pre compiled header file(PCH) in the compiler building settings.
-
-// Here you can add specific libraries and macros which are specific to this project.
-
-VCMI_LIB_USING_NAMESPACE
+/*
+ * StdInc.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 "../../Global.h"
+
+// This header should be treated as a pre compiled header file(PCH) in the compiler building settings.
+
+// Here you can add specific libraries and macros which are specific to this project.
+
+VCMI_LIB_USING_NAMESPACE

+ 33 - 33
AI/BattleAI/main.cpp

@@ -1,33 +1,33 @@
-/*
- * main.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 "../../lib/AI_Base.h"
-#include "BattleAI.h"
-
-#ifdef __GNUC__
-#define strcpy_s(a, b, c) strncpy(a, c, b)
-#endif
-
-static const char *g_cszAiName = "Battle AI";
-
-extern "C" DLL_EXPORT int GetGlobalAiVersion()
-{
-	return AI_INTERFACE_VER;
-}
-
-extern "C" DLL_EXPORT void GetAiName(char* name)
-{
-	strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName);
-}
-
-extern "C" DLL_EXPORT void GetNewBattleAI(std::shared_ptr<CBattleGameInterface> &out)
-{
-	out = std::make_shared<CBattleAI>();
-}
+/*
+ * main.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 "../../lib/AI_Base.h"
+#include "BattleAI.h"
+
+#ifdef __GNUC__
+#define strcpy_s(a, b, c) strncpy(a, c, b)
+#endif
+
+static const char *g_cszAiName = "Battle AI";
+
+extern "C" DLL_EXPORT int GetGlobalAiVersion()
+{
+	return AI_INTERFACE_VER;
+}
+
+extern "C" DLL_EXPORT void GetAiName(char* name)
+{
+	strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName);
+}
+
+extern "C" DLL_EXPORT void GetNewBattleAI(std::shared_ptr<CBattleGameInterface> &out)
+{
+	out = std::make_shared<CBattleAI>();
+}

+ 82 - 82
AI/EmptyAI/CEmptyAI.cpp

@@ -1,82 +1,82 @@
-/*
- * CEmptyAI.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 "CEmptyAI.h"
-
-#include "../../lib/CRandomGenerator.h"
-#include "../../lib/CStack.h"
-#include "../../lib/battle/BattleAction.h"
-
-void CEmptyAI::saveGame(BinarySerializer & h, const int version)
-{
-}
-
-void CEmptyAI::loadGame(BinaryDeserializer & h, const int version)
-{
-}
-
-void CEmptyAI::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB)
-{
-	cb = CB;
-	env = ENV;
-	human=false;
-	playerID = *cb->getPlayerID();
-}
-
-void CEmptyAI::yourTurn(QueryID queryID)
-{
-	cb->selectionMade(0, queryID);
-	cb->endTurn();
-}
-
-void CEmptyAI::activeStack(const BattleID & battleID, const CStack * stack)
-{
-	cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack));
-}
-
-void CEmptyAI::yourTacticPhase(const BattleID & battleID, int distance)
-{
-	cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide()));
-}
-
-void CEmptyAI::heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector<SecondarySkill> &skills, QueryID queryID)
-{
-	cb->selectionMade(CRandomGenerator::getDefault().nextInt((int)skills.size() - 1), queryID);
-}
-
-void CEmptyAI::commanderGotLevel(const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID)
-{
-	cb->selectionMade(CRandomGenerator::getDefault().nextInt((int)skills.size() - 1), queryID);
-}
-
-void CEmptyAI::showBlockingDialog(const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel)
-{
-	cb->selectionMade(0, askID);
-}
-
-void CEmptyAI::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID)
-{
-	cb->selectionMade(0, askID);
-}
-
-void CEmptyAI::showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID)
-{
-	cb->selectionMade(0, queryID);
-}
-
-void CEmptyAI::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects)
-{
-	cb->selectionMade(0, askID);
-}
-
-std::optional<BattleAction> CEmptyAI::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState)
-{
-	return std::nullopt;
-}
+/*
+ * CEmptyAI.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 "CEmptyAI.h"
+
+#include "../../lib/CRandomGenerator.h"
+#include "../../lib/CStack.h"
+#include "../../lib/battle/BattleAction.h"
+
+void CEmptyAI::saveGame(BinarySerializer & h, const int version)
+{
+}
+
+void CEmptyAI::loadGame(BinaryDeserializer & h, const int version)
+{
+}
+
+void CEmptyAI::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB)
+{
+	cb = CB;
+	env = ENV;
+	human=false;
+	playerID = *cb->getPlayerID();
+}
+
+void CEmptyAI::yourTurn(QueryID queryID)
+{
+	cb->selectionMade(0, queryID);
+	cb->endTurn();
+}
+
+void CEmptyAI::activeStack(const BattleID & battleID, const CStack * stack)
+{
+	cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack));
+}
+
+void CEmptyAI::yourTacticPhase(const BattleID & battleID, int distance)
+{
+	cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide()));
+}
+
+void CEmptyAI::heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector<SecondarySkill> &skills, QueryID queryID)
+{
+	cb->selectionMade(CRandomGenerator::getDefault().nextInt((int)skills.size() - 1), queryID);
+}
+
+void CEmptyAI::commanderGotLevel(const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID)
+{
+	cb->selectionMade(CRandomGenerator::getDefault().nextInt((int)skills.size() - 1), queryID);
+}
+
+void CEmptyAI::showBlockingDialog(const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel)
+{
+	cb->selectionMade(0, askID);
+}
+
+void CEmptyAI::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID)
+{
+	cb->selectionMade(0, askID);
+}
+
+void CEmptyAI::showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID)
+{
+	cb->selectionMade(0, queryID);
+}
+
+void CEmptyAI::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects)
+{
+	cb->selectionMade(0, askID);
+}
+
+std::optional<BattleAction> CEmptyAI::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState)
+{
+	return std::nullopt;
+}

+ 38 - 38
AI/EmptyAI/CEmptyAI.h

@@ -1,38 +1,38 @@
-/*
- * CEmptyAI.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/AI_Base.h"
-#include "../../CCallback.h"
-
-struct HeroMoveDetails;
-
-class CEmptyAI : public CGlobalAI
-{
-	std::shared_ptr<CCallback> cb;
-
-public:
-	virtual void saveGame(BinarySerializer & h, const int version) override;
-	virtual void loadGame(BinaryDeserializer & h, const int version) override;
-
-	void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
-	void yourTurn(QueryID queryID) override;
-	void yourTacticPhase(const BattleID & battleID, int distance) override;
-	void activeStack(const BattleID & battleID, const CStack * stack) override;
-	void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector<SecondarySkill> &skills, QueryID queryID) override;
-	void commanderGotLevel (const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID) override;
-	void showBlockingDialog(const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel) override;
-	void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override;
-	void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override;
-	void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects) override;
-	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override;
-};
-
-#define NAME "EmptyAI 0.1"
+/*
+ * CEmptyAI.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/AI_Base.h"
+#include "../../CCallback.h"
+
+struct HeroMoveDetails;
+
+class CEmptyAI : public CGlobalAI
+{
+	std::shared_ptr<CCallback> cb;
+
+public:
+	virtual void saveGame(BinarySerializer & h, const int version) override;
+	virtual void loadGame(BinaryDeserializer & h, const int version) override;
+
+	void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
+	void yourTurn(QueryID queryID) override;
+	void yourTacticPhase(const BattleID & battleID, int distance) override;
+	void activeStack(const BattleID & battleID, const CStack * stack) override;
+	void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector<SecondarySkill> &skills, QueryID queryID) override;
+	void commanderGotLevel (const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID) override;
+	void showBlockingDialog(const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel) override;
+	void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override;
+	void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override;
+	void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects) override;
+	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override;
+};
+
+#define NAME "EmptyAI 0.1"

+ 1 - 1
AI/EmptyAI/StdInc.cpp

@@ -1,2 +1,2 @@
-// Creates the precompiled header
+// Creates the precompiled header
 #include "StdInc.h"

+ 9 - 9
AI/EmptyAI/StdInc.h

@@ -1,9 +1,9 @@
-#pragma once
-
-#include "../../Global.h"
-
-// This header should be treated as a pre compiled header file(PCH) in the compiler building settings.
-
-// Here you can add specific libraries and macros which are specific to this project.
-
-VCMI_LIB_USING_NAMESPACE
+#pragma once
+
+#include "../../Global.h"
+
+// This header should be treated as a pre compiled header file(PCH) in the compiler building settings.
+
+// Here you can add specific libraries and macros which are specific to this project.
+
+VCMI_LIB_USING_NAMESPACE

+ 28 - 28
AI/EmptyAI/main.cpp

@@ -1,28 +1,28 @@
-/*
- * main.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 "CEmptyAI.h"
-
-std::set<CGlobalAI*> ais;
-extern "C" DLL_EXPORT int GetGlobalAiVersion()
-{
-	return AI_INTERFACE_VER;
-}
-
-extern "C" DLL_EXPORT void GetAiName(char* name)
-{
-	strcpy(name,NAME);
-}
-
-extern "C" DLL_EXPORT void GetNewAI(std::shared_ptr<CGlobalAI> &out)
-{
-	out = std::make_shared<CEmptyAI>();
-}
+/*
+ * main.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 "CEmptyAI.h"
+
+std::set<CGlobalAI*> ais;
+extern "C" DLL_EXPORT int GetGlobalAiVersion()
+{
+	return AI_INTERFACE_VER;
+}
+
+extern "C" DLL_EXPORT void GetAiName(char* name)
+{
+	strcpy(name,NAME);
+}
+
+extern "C" DLL_EXPORT void GetNewAI(std::shared_ptr<CGlobalAI> &out)
+{
+	out = std::make_shared<CEmptyAI>();
+}

+ 736 - 736
AI/GeniusAI.brain

@@ -1,736 +1,736 @@
-o 34 16 17
-R
-o 34 16 17
-R
-o 47 16
-R
-o 101 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 24
-R
-o 98 16 17
-R
-o 98 16 17
-R
-o 100 16
-R
-o 38 16
-R
-o 61 16
-R
-o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
-R
-o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
-R
-o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
-R
-o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
-R
-o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
-R
-o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
-R
-o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
-R
-o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
-R
-o 28 16
-R
-o 81 16
-R
-o 83 25
-R
-o 31 16
-R
-o 57 24
-R
-o 23 16
-R
-o 102 16
-R
-o 37 24
-R
-o 51 16
-R
-t 0 0 25
-R
-t 0 1 25
-R
-t 0 2 25
-R
-t 0 3 25
-R
-t 0 4 25
-R
-t 0 5 25
-R
-t 0 6 25
-R
-t 0 7 25
-R
-t 0 8 25
-R
-t 0 9 25
-R
-t 0 10 25
-R
-t 0 11 25
-R
-t 0 12 25
-R
-t 0 13 25
-R
-t 0 14 25
-R
-t 0 15 25
-R
-t 0 16 25
-R
-t 0 17 25
-R
-t 0 18 25
-R
-t 0 19 25
-R
-t 0 20 25
-R
-t 0 21 25
-R
-t 0 22 25
-R
-t 0 23 25
-R
-t 0 30 25
-R
-t 0 31 25
-R
-t 0 32 25
-R
-t 0 33 25
-R
-t 0 34 25
-R
-t 0 35 25
-R
-t 0 36 25
-R
-t 0 37 25
-R
-t 0 38 25
-R
-t 0 39 25
-R
-t 0 40 25
-R
-t 0 41 25
-R
-t 0 42 25
-R
-t 0 43 25
-R
-t 1 0 25
-R
-t 1 1 25
-R
-t 1 2 25
-R
-t 1 3 25
-R
-t 1 4 25
-R
-t 1 5 25
-R
-t 1 6 25
-R
-t 1 7 25
-R
-t 1 8 25
-R
-t 1 9 25
-R
-t 1 10 25
-R
-t 1 11 25
-R
-t 1 12 25
-R
-t 1 13 25
-R
-t 1 14 25
-R
-t 1 15 25
-R
-t 1 16 25
-R
-t 1 17 25
-R
-t 1 18 25
-R
-t 1 19 25
-R
-t 1 20 25
-R
-t 1 21 25
-R
-t 1 22 25
-R
-t 1 23 25
-R
-t 1 30 25
-R
-t 1 31 25
-R
-t 1 32 25
-R
-t 1 33 25
-R
-t 1 34 25
-R
-t 1 35 25
-R
-t 1 36 25
-R
-t 1 37 25
-R
-t 1 38 25
-R
-t 1 39 25
-R
-t 1 40 25
-R
-t 1 41 25
-R
-t 1 42 25
-R
-t 1 43 25
-R
-t 2 0 25
-R
-t 2 1 25
-R
-t 2 2 25
-R
-t 2 3 25
-R
-t 2 4 25
-R
-t 2 5 25
-R
-t 2 6 25
-R
-t 2 7 25
-R
-t 2 8 25
-R
-t 2 9 25
-R
-t 2 10 25
-R
-t 2 11 25
-R
-t 2 12 25
-R
-t 2 13 25
-R
-t 2 14 25
-R
-t 2 15 25
-R
-t 2 16 25
-R
-t 2 17 25
-R
-t 2 18 25
-R
-t 2 19 25
-R
-t 2 20 25
-R
-t 2 21 25
-R
-t 2 22 25
-R
-t 2 23 25
-R
-t 2 30 25
-R
-t 2 31 25
-R
-t 2 32 25
-R
-t 2 33 25
-R
-t 2 34 25
-R
-t 2 35 25
-R
-t 2 36 25
-R
-t 2 37 25
-R
-t 2 38 25
-R
-t 2 39 25
-R
-t 2 40 25
-R
-t 2 41 25
-R
-t 2 42 25
-R
-t 2 43 25
-R
-t 3 0 25
-R
-t 3 1 25
-R
-t 3 2 25
-R
-t 3 3 25
-R
-t 3 4 25
-R
-t 3 5 25
-R
-t 3 6 25
-R
-t 3 7 25
-R
-t 3 8 25
-R
-t 3 9 25
-R
-t 3 10 25
-R
-t 3 11 25
-R
-t 3 12 25
-R
-t 3 13 25
-R
-t 3 14 25
-R
-t 3 15 25
-R
-t 3 16 25
-R
-t 3 17 25
-R
-t 3 18 25
-R
-t 3 19 25
-R
-t 3 20 25
-R
-t 3 21 25
-R
-t 3 22 25
-R
-t 3 23 25
-R
-t 3 30 25
-R
-t 3 31 25
-R
-t 3 32 25
-R
-t 3 33 25
-R
-t 3 34 25
-R
-t 3 35 25
-R
-t 3 36 25
-R
-t 3 37 25
-R
-t 3 38 25
-R
-t 3 39 25
-R
-t 3 40 25
-R
-t 3 41 25
-R
-t 3 42 25
-R
-t 3 43 25
-R
-t 4 0 25
-R
-t 4 1 25
-R
-t 4 2 25
-R
-t 4 3 25
-R
-t 4 4 25
-R
-t 4 5 25
-R
-t 4 6 25
-R
-t 4 7 25
-R
-t 4 8 25
-R
-t 4 9 25
-R
-t 4 10 25
-R
-t 4 11 25
-R
-t 4 12 25
-R
-t 4 13 25
-R
-t 4 14 25
-R
-t 4 15 25
-R
-t 4 16 25
-R
-t 4 17 25
-R
-t 4 18 25
-R
-t 4 19 25
-R
-t 4 20 25
-R
-t 4 21 25
-R
-t 4 22 25
-R
-t 4 23 25
-R
-t 4 30 25
-R
-t 4 31 25
-R
-t 4 32 25
-R
-t 4 33 25
-R
-t 4 34 25
-R
-t 4 35 25
-R
-t 4 36 25
-R
-t 4 37 25
-R
-t 4 38 25
-R
-t 4 39 25
-R
-t 4 40 25
-R
-t 4 41 25
-R
-t 4 42 25
-R
-t 4 43 25
-R
-t 5 0 25
-R
-t 5 1 25
-R
-t 5 2 25
-R
-t 5 3 25
-R
-t 5 4 25
-R
-t 5 5 25
-R
-t 5 6 25
-R
-t 5 7 25
-R
-t 5 8 25
-R
-t 5 9 25
-R
-t 5 10 25
-R
-t 5 11 25
-R
-t 5 12 25
-R
-t 5 13 25
-R
-t 5 14 25
-R
-t 5 15 25
-R
-t 5 16 25
-R
-t 5 17 25
-R
-t 5 18 25
-R
-t 5 19 25
-R
-t 5 20 25
-R
-t 5 21 25
-R
-t 5 22 25
-R
-t 5 23 25
-R
-t 5 30 25
-R
-t 5 31 25
-R
-t 5 32 25
-R
-t 5 33 25
-R
-t 5 34 25
-R
-t 5 35 25
-R
-t 5 36 25
-R
-t 5 37 25
-R
-t 5 38 25
-R
-t 5 39 25
-R
-t 5 40 25
-R
-t 5 41 25
-R
-t 5 42 25
-R
-t 5 43 25
-R
-t 6 0 25
-R
-t 6 1 25
-R
-t 6 2 25
-R
-t 6 3 25
-R
-t 6 4 25
-R
-t 6 5 25
-R
-t 6 6 25
-R
-t 6 7 25
-R
-t 6 8 25
-R
-t 6 9 25
-R
-t 6 10 25
-R
-t 6 11 25
-R
-t 6 12 25
-R
-t 6 13 25
-R
-t 6 14 25
-R
-t 6 15 25
-R
-t 6 16 25
-R
-t 6 17 25
-R
-t 6 18 25
-R
-t 6 19 25
-R
-t 6 20 25
-R
-t 6 21 25
-R
-t 6 22 25
-R
-t 6 23 25
-R
-t 6 30 25
-R
-t 6 31 25
-R
-t 6 32 25
-R
-t 6 33 25
-R
-t 6 34 25
-R
-t 6 35 25
-R
-t 6 36 25
-R
-t 6 37 25
-R
-t 6 38 25
-R
-t 6 39 25
-R
-t 6 40 25
-R
-t 6 41 25
-R
-t 6 42 25
-R
-t 6 43 25
-R
-t 7 0 25
-R
-t 7 1 25
-R
-t 7 2 25
-R
-t 7 3 25
-R
-t 7 4 25
-R
-t 7 5 25
-R
-t 7 6 25
-R
-t 7 7 25
-R
-t 7 8 25
-R
-t 7 9 25
-R
-t 7 10 25
-R
-t 7 11 25
-R
-t 7 12 25
-R
-t 7 13 25
-R
-t 7 14 25
-R
-t 7 15 25
-R
-t 7 16 25
-R
-t 7 17 25
-R
-t 7 18 25
-R
-t 7 19 25
-R
-t 7 20 25
-R
-t 7 21 25
-R
-t 7 22 25
-R
-t 7 23 25
-R
-t 7 30 25
-R
-t 7 31 25
-R
-t 7 32 25
-R
-t 7 33 25
-R
-t 7 34 25
-R
-t 7 35 25
-R
-t 7 36 25
-R
-t 7 37 25
-R
-t 7 38 25
-R
-t 7 39 25
-R
-t 7 40 25
-R
-t 7 41 25
-R
-t 7 42 25
-R
-t 7 43 25
-R
-t 8 0 25
-R
-t 8 1 25
-R
-t 8 2 25
-R
-t 8 3 25
-R
-t 8 4 25
-R
-t 8 5 25
-R
-t 8 6 25
-R
-t 8 7 25
-R
-t 8 8 25
-R
-t 8 9 25
-R
-t 8 10 25
-R
-t 8 11 25
-R
-t 8 12 25
-R
-t 8 13 25
-R
-t 8 14 25
-R
-t 8 15 25
-R
-t 8 16 25
-R
-t 8 17 25
-R
-t 8 18 25
-R
-t 8 19 25
-R
-t 8 20 25
-R
-t 8 21 25
-R
-t 8 22 25
-R
-t 8 23 25
-R
-t 8 30 25
-R
-t 8 31 25
-R
-t 8 32 25
-R
-t 8 33 25
-R
-t 8 34 25
-R
-t 8 35 25
-R
-t 8 36 25
-R
-t 8 37 25
-R
-t 8 38 25
-R
-t 8 39 25
-R
-t 8 40 25
-R
-t 8 41 25
-R
-t 8 42 25
-R
-t 8 43 25
-R
+o 34 16 17
+R
+o 34 16 17
+R
+o 47 16
+R
+o 101 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 24
+R
+o 98 16 17
+R
+o 98 16 17
+R
+o 100 16
+R
+o 38 16
+R
+o 61 16
+R
+o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+R
+o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+R
+o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+R
+o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+R
+o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+R
+o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+R
+o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+R
+o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+R
+o 28 16
+R
+o 81 16
+R
+o 83 25
+R
+o 31 16
+R
+o 57 24
+R
+o 23 16
+R
+o 102 16
+R
+o 37 24
+R
+o 51 16
+R
+t 0 0 25
+R
+t 0 1 25
+R
+t 0 2 25
+R
+t 0 3 25
+R
+t 0 4 25
+R
+t 0 5 25
+R
+t 0 6 25
+R
+t 0 7 25
+R
+t 0 8 25
+R
+t 0 9 25
+R
+t 0 10 25
+R
+t 0 11 25
+R
+t 0 12 25
+R
+t 0 13 25
+R
+t 0 14 25
+R
+t 0 15 25
+R
+t 0 16 25
+R
+t 0 17 25
+R
+t 0 18 25
+R
+t 0 19 25
+R
+t 0 20 25
+R
+t 0 21 25
+R
+t 0 22 25
+R
+t 0 23 25
+R
+t 0 30 25
+R
+t 0 31 25
+R
+t 0 32 25
+R
+t 0 33 25
+R
+t 0 34 25
+R
+t 0 35 25
+R
+t 0 36 25
+R
+t 0 37 25
+R
+t 0 38 25
+R
+t 0 39 25
+R
+t 0 40 25
+R
+t 0 41 25
+R
+t 0 42 25
+R
+t 0 43 25
+R
+t 1 0 25
+R
+t 1 1 25
+R
+t 1 2 25
+R
+t 1 3 25
+R
+t 1 4 25
+R
+t 1 5 25
+R
+t 1 6 25
+R
+t 1 7 25
+R
+t 1 8 25
+R
+t 1 9 25
+R
+t 1 10 25
+R
+t 1 11 25
+R
+t 1 12 25
+R
+t 1 13 25
+R
+t 1 14 25
+R
+t 1 15 25
+R
+t 1 16 25
+R
+t 1 17 25
+R
+t 1 18 25
+R
+t 1 19 25
+R
+t 1 20 25
+R
+t 1 21 25
+R
+t 1 22 25
+R
+t 1 23 25
+R
+t 1 30 25
+R
+t 1 31 25
+R
+t 1 32 25
+R
+t 1 33 25
+R
+t 1 34 25
+R
+t 1 35 25
+R
+t 1 36 25
+R
+t 1 37 25
+R
+t 1 38 25
+R
+t 1 39 25
+R
+t 1 40 25
+R
+t 1 41 25
+R
+t 1 42 25
+R
+t 1 43 25
+R
+t 2 0 25
+R
+t 2 1 25
+R
+t 2 2 25
+R
+t 2 3 25
+R
+t 2 4 25
+R
+t 2 5 25
+R
+t 2 6 25
+R
+t 2 7 25
+R
+t 2 8 25
+R
+t 2 9 25
+R
+t 2 10 25
+R
+t 2 11 25
+R
+t 2 12 25
+R
+t 2 13 25
+R
+t 2 14 25
+R
+t 2 15 25
+R
+t 2 16 25
+R
+t 2 17 25
+R
+t 2 18 25
+R
+t 2 19 25
+R
+t 2 20 25
+R
+t 2 21 25
+R
+t 2 22 25
+R
+t 2 23 25
+R
+t 2 30 25
+R
+t 2 31 25
+R
+t 2 32 25
+R
+t 2 33 25
+R
+t 2 34 25
+R
+t 2 35 25
+R
+t 2 36 25
+R
+t 2 37 25
+R
+t 2 38 25
+R
+t 2 39 25
+R
+t 2 40 25
+R
+t 2 41 25
+R
+t 2 42 25
+R
+t 2 43 25
+R
+t 3 0 25
+R
+t 3 1 25
+R
+t 3 2 25
+R
+t 3 3 25
+R
+t 3 4 25
+R
+t 3 5 25
+R
+t 3 6 25
+R
+t 3 7 25
+R
+t 3 8 25
+R
+t 3 9 25
+R
+t 3 10 25
+R
+t 3 11 25
+R
+t 3 12 25
+R
+t 3 13 25
+R
+t 3 14 25
+R
+t 3 15 25
+R
+t 3 16 25
+R
+t 3 17 25
+R
+t 3 18 25
+R
+t 3 19 25
+R
+t 3 20 25
+R
+t 3 21 25
+R
+t 3 22 25
+R
+t 3 23 25
+R
+t 3 30 25
+R
+t 3 31 25
+R
+t 3 32 25
+R
+t 3 33 25
+R
+t 3 34 25
+R
+t 3 35 25
+R
+t 3 36 25
+R
+t 3 37 25
+R
+t 3 38 25
+R
+t 3 39 25
+R
+t 3 40 25
+R
+t 3 41 25
+R
+t 3 42 25
+R
+t 3 43 25
+R
+t 4 0 25
+R
+t 4 1 25
+R
+t 4 2 25
+R
+t 4 3 25
+R
+t 4 4 25
+R
+t 4 5 25
+R
+t 4 6 25
+R
+t 4 7 25
+R
+t 4 8 25
+R
+t 4 9 25
+R
+t 4 10 25
+R
+t 4 11 25
+R
+t 4 12 25
+R
+t 4 13 25
+R
+t 4 14 25
+R
+t 4 15 25
+R
+t 4 16 25
+R
+t 4 17 25
+R
+t 4 18 25
+R
+t 4 19 25
+R
+t 4 20 25
+R
+t 4 21 25
+R
+t 4 22 25
+R
+t 4 23 25
+R
+t 4 30 25
+R
+t 4 31 25
+R
+t 4 32 25
+R
+t 4 33 25
+R
+t 4 34 25
+R
+t 4 35 25
+R
+t 4 36 25
+R
+t 4 37 25
+R
+t 4 38 25
+R
+t 4 39 25
+R
+t 4 40 25
+R
+t 4 41 25
+R
+t 4 42 25
+R
+t 4 43 25
+R
+t 5 0 25
+R
+t 5 1 25
+R
+t 5 2 25
+R
+t 5 3 25
+R
+t 5 4 25
+R
+t 5 5 25
+R
+t 5 6 25
+R
+t 5 7 25
+R
+t 5 8 25
+R
+t 5 9 25
+R
+t 5 10 25
+R
+t 5 11 25
+R
+t 5 12 25
+R
+t 5 13 25
+R
+t 5 14 25
+R
+t 5 15 25
+R
+t 5 16 25
+R
+t 5 17 25
+R
+t 5 18 25
+R
+t 5 19 25
+R
+t 5 20 25
+R
+t 5 21 25
+R
+t 5 22 25
+R
+t 5 23 25
+R
+t 5 30 25
+R
+t 5 31 25
+R
+t 5 32 25
+R
+t 5 33 25
+R
+t 5 34 25
+R
+t 5 35 25
+R
+t 5 36 25
+R
+t 5 37 25
+R
+t 5 38 25
+R
+t 5 39 25
+R
+t 5 40 25
+R
+t 5 41 25
+R
+t 5 42 25
+R
+t 5 43 25
+R
+t 6 0 25
+R
+t 6 1 25
+R
+t 6 2 25
+R
+t 6 3 25
+R
+t 6 4 25
+R
+t 6 5 25
+R
+t 6 6 25
+R
+t 6 7 25
+R
+t 6 8 25
+R
+t 6 9 25
+R
+t 6 10 25
+R
+t 6 11 25
+R
+t 6 12 25
+R
+t 6 13 25
+R
+t 6 14 25
+R
+t 6 15 25
+R
+t 6 16 25
+R
+t 6 17 25
+R
+t 6 18 25
+R
+t 6 19 25
+R
+t 6 20 25
+R
+t 6 21 25
+R
+t 6 22 25
+R
+t 6 23 25
+R
+t 6 30 25
+R
+t 6 31 25
+R
+t 6 32 25
+R
+t 6 33 25
+R
+t 6 34 25
+R
+t 6 35 25
+R
+t 6 36 25
+R
+t 6 37 25
+R
+t 6 38 25
+R
+t 6 39 25
+R
+t 6 40 25
+R
+t 6 41 25
+R
+t 6 42 25
+R
+t 6 43 25
+R
+t 7 0 25
+R
+t 7 1 25
+R
+t 7 2 25
+R
+t 7 3 25
+R
+t 7 4 25
+R
+t 7 5 25
+R
+t 7 6 25
+R
+t 7 7 25
+R
+t 7 8 25
+R
+t 7 9 25
+R
+t 7 10 25
+R
+t 7 11 25
+R
+t 7 12 25
+R
+t 7 13 25
+R
+t 7 14 25
+R
+t 7 15 25
+R
+t 7 16 25
+R
+t 7 17 25
+R
+t 7 18 25
+R
+t 7 19 25
+R
+t 7 20 25
+R
+t 7 21 25
+R
+t 7 22 25
+R
+t 7 23 25
+R
+t 7 30 25
+R
+t 7 31 25
+R
+t 7 32 25
+R
+t 7 33 25
+R
+t 7 34 25
+R
+t 7 35 25
+R
+t 7 36 25
+R
+t 7 37 25
+R
+t 7 38 25
+R
+t 7 39 25
+R
+t 7 40 25
+R
+t 7 41 25
+R
+t 7 42 25
+R
+t 7 43 25
+R
+t 8 0 25
+R
+t 8 1 25
+R
+t 8 2 25
+R
+t 8 3 25
+R
+t 8 4 25
+R
+t 8 5 25
+R
+t 8 6 25
+R
+t 8 7 25
+R
+t 8 8 25
+R
+t 8 9 25
+R
+t 8 10 25
+R
+t 8 11 25
+R
+t 8 12 25
+R
+t 8 13 25
+R
+t 8 14 25
+R
+t 8 15 25
+R
+t 8 16 25
+R
+t 8 17 25
+R
+t 8 18 25
+R
+t 8 19 25
+R
+t 8 20 25
+R
+t 8 21 25
+R
+t 8 22 25
+R
+t 8 23 25
+R
+t 8 30 25
+R
+t 8 31 25
+R
+t 8 32 25
+R
+t 8 33 25
+R
+t 8 34 25
+R
+t 8 35 25
+R
+t 8 36 25
+R
+t 8 37 25
+R
+t 8 38 25
+R
+t 8 39 25
+R
+t 8 40 25
+R
+t 8 41 25
+R
+t 8 42 25
+R
+t 8 43 25
+R

+ 1 - 1
AI/StupidAI/StdInc.cpp

@@ -1,2 +1,2 @@
-// Creates the precompiled header
+// Creates the precompiled header
 #include "StdInc.h"

+ 9 - 9
AI/StupidAI/StdInc.h

@@ -1,9 +1,9 @@
-#pragma once
-
-#include "../../Global.h"
-
-// This header should be treated as a pre compiled header file(PCH) in the compiler building settings.
-
-// Here you can add specific libraries and macros which are specific to this project.
-
-VCMI_LIB_USING_NAMESPACE
+#pragma once
+
+#include "../../Global.h"
+
+// This header should be treated as a pre compiled header file(PCH) in the compiler building settings.
+
+// Here you can add specific libraries and macros which are specific to this project.
+
+VCMI_LIB_USING_NAMESPACE

+ 333 - 333
AI/StupidAI/StupidAI.cpp

@@ -1,333 +1,333 @@
-/*
- * StupidAI.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 "../../lib/AI_Base.h"
-#include "StupidAI.h"
-#include "../../lib/CStack.h"
-#include "../../CCallback.h"
-#include "../../lib/CCreatureHandler.h"
-#include "../../lib/battle/BattleAction.h"
-#include "../../lib/battle/BattleInfo.h"
-
-static std::shared_ptr<CBattleCallback> cbc;
-
-CStupidAI::CStupidAI()
-	: side(-1)
-	, wasWaitingForRealize(false)
-	, wasUnlockingGs(false)
-{
-	print("created");
-}
-
-CStupidAI::~CStupidAI()
-{
-	print("destroyed");
-	if(cb)
-	{
-		//Restore previous state of CB - it may be shared with the main AI (like VCAI)
-		cb->waitTillRealize = wasWaitingForRealize;
-		cb->unlockGsWhenWaiting = wasUnlockingGs;
-	}
-}
-
-void CStupidAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
-{
-	print("init called, saving ptr to IBattleCallback");
-	env = ENV;
-	cbc = cb = CB;
-
-	wasWaitingForRealize = CB->waitTillRealize;
-	wasUnlockingGs = CB->unlockGsWhenWaiting;
-	CB->waitTillRealize = false;
-	CB->unlockGsWhenWaiting = false;
-}
-
-void CStupidAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB, AutocombatPreferences autocombatPreferences)
-{
-	initBattleInterface(ENV, CB);
-}
-
-void CStupidAI::actionFinished(const BattleID & battleID, const BattleAction &action)
-{
-	print("actionFinished called");
-}
-
-void CStupidAI::actionStarted(const BattleID & battleID, const BattleAction &action)
-{
-	print("actionStarted called");
-}
-
-class EnemyInfo
-{
-public:
-	const CStack * s;
-	int adi, adr;
-	std::vector<BattleHex> attackFrom; //for melee fight
-	EnemyInfo(const CStack * _s) : s(_s), adi(0), adr(0)
-	{}
-	void calcDmg(const BattleID & battleID, const CStack * ourStack)
-	{
-		// FIXME: provide distance info for Jousting bonus
-		DamageEstimation retal;
-		DamageEstimation dmg = cbc->getBattle(battleID)->battleEstimateDamage(ourStack, s, 0, &retal);
-		adi = static_cast<int>((dmg.damage.min + dmg.damage.max) / 2);
-		adr = static_cast<int>((retal.damage.min + retal.damage.max) / 2);
-	}
-
-	bool operator==(const EnemyInfo& ei) const
-	{
-		return s == ei.s;
-	}
-};
-
-bool isMoreProfitable(const EnemyInfo &ei1, const EnemyInfo& ei2)
-{
-	return (ei1.adi-ei1.adr) < (ei2.adi - ei2.adr);
-}
-
-static bool willSecondHexBlockMoreEnemyShooters(const BattleID & battleID, const BattleHex &h1, const BattleHex &h2)
-{
-	int shooters[2] = {0}; //count of shooters on hexes
-
-	for(int i = 0; i < 2; i++)
-	{
-		for (auto & neighbour : (i ? h2 : h1).neighbouringTiles())
-			if(const auto * s = cbc->getBattle(battleID)->battleGetUnitByPos(neighbour))
-				if(s->isShooter())
-					shooters[i]++;
-	}
-
-	return shooters[0] < shooters[1];
-}
-
-void CStupidAI::yourTacticPhase(const BattleID & battleID, int distance)
-{
-	cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide()));
-}
-
-void CStupidAI::activeStack(const BattleID & battleID, const CStack * stack)
-{
-	//boost::this_thread::sleep_for(boost::chrono::seconds(2));
-	print("activeStack called for " + stack->nodeName());
-	ReachabilityInfo dists = cb->getBattle(battleID)->getReachability(stack);
-	std::vector<EnemyInfo> enemiesShootable, enemiesReachable, enemiesUnreachable;
-
-	if(stack->creatureId() == CreatureID::CATAPULT)
-	{
-		BattleAction attack;
-		static const std::vector<int> wallHexes = {50, 183, 182, 130, 78, 29, 12, 95};
-		auto seletectedHex = *RandomGeneratorUtil::nextItem(wallHexes, CRandomGenerator::getDefault());
-		attack.aimToHex(seletectedHex);
-		attack.actionType = EActionType::CATAPULT;
-		attack.side = side;
-		attack.stackNumber = stack->unitId();
-
-		cb->battleMakeUnitAction(battleID, attack);
-		return;
-	}
-	else if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON))
-	{
-		cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack));
-		return;
-	}
-
-	for (const CStack *s : cb->getBattle(battleID)->battleGetStacks(CBattleInfoEssentials::ONLY_ENEMY))
-	{
-		if(cb->getBattle(battleID)->battleCanShoot(stack, s->getPosition()))
-		{
-			enemiesShootable.push_back(s);
-		}
-		else
-		{
-			std::vector<BattleHex> avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(stack, false);
-
-			for (BattleHex hex : avHexes)
-			{
-				if(CStack::isMeleeAttackPossible(stack, s, hex))
-				{
-					std::vector<EnemyInfo>::iterator i = std::find(enemiesReachable.begin(), enemiesReachable.end(), s);
-					if(i == enemiesReachable.end())
-					{
-						enemiesReachable.push_back(s);
-						i = enemiesReachable.begin() + (enemiesReachable.size() - 1);
-					}
-
-					i->attackFrom.push_back(hex);
-				}
-			}
-
-			if(!vstd::contains(enemiesReachable, s) && s->getPosition().isValid())
-				enemiesUnreachable.push_back(s);
-		}
-	}
-
-	for ( auto & enemy : enemiesReachable )
-		enemy.calcDmg(battleID, stack);
-
-	for ( auto & enemy : enemiesShootable )
-		enemy.calcDmg(battleID, stack);
-
-	if(enemiesShootable.size())
-	{
-		const EnemyInfo &ei= *std::max_element(enemiesShootable.begin(), enemiesShootable.end(), isMoreProfitable);
-		cb->battleMakeUnitAction(battleID, BattleAction::makeShotAttack(stack, ei.s));
-		return;
-	}
-	else if(enemiesReachable.size())
-	{
-		const EnemyInfo &ei= *std::max_element(enemiesReachable.begin(), enemiesReachable.end(), &isMoreProfitable);
-		BattleHex targetHex = *std::max_element(ei.attackFrom.begin(), ei.attackFrom.end(), [&](auto a, auto b) { return willSecondHexBlockMoreEnemyShooters(battleID, a, b);});
-
-		cb->battleMakeUnitAction(battleID, BattleAction::makeMeleeAttack(stack, ei.s->getPosition(), targetHex));
-		return;
-	}
-	else if(enemiesUnreachable.size()) //due to #955 - a buggy battle may occur when there are no enemies
-	{
-		auto closestEnemy = vstd::minElementByFun(enemiesUnreachable, [&](const EnemyInfo & ei) -> int
-		{
-			return dists.distToNearestNeighbour(stack, ei.s);
-		});
-
-		if(dists.distToNearestNeighbour(stack, closestEnemy->s) < GameConstants::BFIELD_SIZE)
-		{
-			cb->battleMakeUnitAction(battleID, goTowards(battleID, stack, closestEnemy->s->getAttackableHexes(stack)));
-			return;
-		}
-	}
-
-	cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack));
-	return;
-}
-
-void CStupidAI::battleAttack(const BattleID & battleID, const BattleAttack *ba)
-{
-	print("battleAttack called");
-}
-
-void CStupidAI::battleStacksAttacked(const BattleID & battleID, const std::vector<BattleStackAttacked> & bsa, bool ranged)
-{
-	print("battleStacksAttacked called");
-}
-
-void CStupidAI::battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID)
-{
-	print("battleEnd called");
-}
-
-// void CStupidAI::battleResultsApplied()
-// {
-// 	print("battleResultsApplied called");
-// }
-
-void CStupidAI::battleNewRoundFirst(const BattleID & battleID)
-{
-	print("battleNewRoundFirst called");
-}
-
-void CStupidAI::battleNewRound(const BattleID & battleID)
-{
-	print("battleNewRound called");
-}
-
-void CStupidAI::battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport)
-{
-	print("battleStackMoved called");
-}
-
-void CStupidAI::battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc)
-{
-	print("battleSpellCast called");
-}
-
-void CStupidAI::battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse)
-{
-	print("battleStacksEffectsSet called");
-}
-
-void CStupidAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
-{
-	print("battleStart called");
-	side = Side;
-}
-
-void CStupidAI::battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca)
-{
-	print("battleCatapultAttacked called");
-}
-
-void CStupidAI::print(const std::string &text) const
-{
-	logAi->trace("CStupidAI  [%p]: %s", this, text);
-}
-
-BattleAction CStupidAI::goTowards(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> hexes) const
-{
-	auto reachability = cb->getBattle(battleID)->getReachability(stack);
-	auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false);
-
-	if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked
-	{
-		return BattleAction::makeDefend(stack);
-	}
-
-	std::sort(hexes.begin(), hexes.end(), [&](BattleHex h1, BattleHex h2) -> bool
-	{
-		return reachability.distances[h1] < reachability.distances[h2];
-	});
-
-	for(auto hex : hexes)
-	{
-		if(vstd::contains(avHexes, hex))
-			return BattleAction::makeMove(stack, hex);
-
-		if(stack->coversPos(hex))
-		{
-			logAi->warn("Warning: already standing on neighbouring tile!");
-			//We shouldn't even be here...
-			return BattleAction::makeDefend(stack);
-		}
-	}
-
-	BattleHex bestNeighbor = hexes.front();
-
-	if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE)
-	{
-		return BattleAction::makeDefend(stack);
-	}
-
-	if(stack->hasBonusOfType(BonusType::FLYING))
-	{
-		// Flying stack doesn't go hex by hex, so we can't backtrack using predecessors.
-		// We just check all available hexes and pick the one closest to the target.
-		auto nearestAvailableHex = vstd::minElementByFun(avHexes, [&](BattleHex hex) -> int
-		{
-			return BattleHex::getDistance(bestNeighbor, hex);
-		});
-
-		return BattleAction::makeMove(stack, *nearestAvailableHex);
-	}
-	else
-	{
-		BattleHex currentDest = bestNeighbor;
-		while(1)
-		{
-			if(!currentDest.isValid())
-			{
-				logAi->error("CBattleAI::goTowards: internal error");
-				return BattleAction::makeDefend(stack);
-			}
-
-			if(vstd::contains(avHexes, currentDest))
-				return BattleAction::makeMove(stack, currentDest);
-
-			currentDest = reachability.predecessors[currentDest];
-		}
-	}
-}
+/*
+ * StupidAI.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 "../../lib/AI_Base.h"
+#include "StupidAI.h"
+#include "../../lib/CStack.h"
+#include "../../CCallback.h"
+#include "../../lib/CCreatureHandler.h"
+#include "../../lib/battle/BattleAction.h"
+#include "../../lib/battle/BattleInfo.h"
+
+static std::shared_ptr<CBattleCallback> cbc;
+
+CStupidAI::CStupidAI()
+	: side(-1)
+	, wasWaitingForRealize(false)
+	, wasUnlockingGs(false)
+{
+	print("created");
+}
+
+CStupidAI::~CStupidAI()
+{
+	print("destroyed");
+	if(cb)
+	{
+		//Restore previous state of CB - it may be shared with the main AI (like VCAI)
+		cb->waitTillRealize = wasWaitingForRealize;
+		cb->unlockGsWhenWaiting = wasUnlockingGs;
+	}
+}
+
+void CStupidAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
+{
+	print("init called, saving ptr to IBattleCallback");
+	env = ENV;
+	cbc = cb = CB;
+
+	wasWaitingForRealize = CB->waitTillRealize;
+	wasUnlockingGs = CB->unlockGsWhenWaiting;
+	CB->waitTillRealize = false;
+	CB->unlockGsWhenWaiting = false;
+}
+
+void CStupidAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB, AutocombatPreferences autocombatPreferences)
+{
+	initBattleInterface(ENV, CB);
+}
+
+void CStupidAI::actionFinished(const BattleID & battleID, const BattleAction &action)
+{
+	print("actionFinished called");
+}
+
+void CStupidAI::actionStarted(const BattleID & battleID, const BattleAction &action)
+{
+	print("actionStarted called");
+}
+
+class EnemyInfo
+{
+public:
+	const CStack * s;
+	int adi, adr;
+	std::vector<BattleHex> attackFrom; //for melee fight
+	EnemyInfo(const CStack * _s) : s(_s), adi(0), adr(0)
+	{}
+	void calcDmg(const BattleID & battleID, const CStack * ourStack)
+	{
+		// FIXME: provide distance info for Jousting bonus
+		DamageEstimation retal;
+		DamageEstimation dmg = cbc->getBattle(battleID)->battleEstimateDamage(ourStack, s, 0, &retal);
+		adi = static_cast<int>((dmg.damage.min + dmg.damage.max) / 2);
+		adr = static_cast<int>((retal.damage.min + retal.damage.max) / 2);
+	}
+
+	bool operator==(const EnemyInfo& ei) const
+	{
+		return s == ei.s;
+	}
+};
+
+bool isMoreProfitable(const EnemyInfo &ei1, const EnemyInfo& ei2)
+{
+	return (ei1.adi-ei1.adr) < (ei2.adi - ei2.adr);
+}
+
+static bool willSecondHexBlockMoreEnemyShooters(const BattleID & battleID, const BattleHex &h1, const BattleHex &h2)
+{
+	int shooters[2] = {0}; //count of shooters on hexes
+
+	for(int i = 0; i < 2; i++)
+	{
+		for (auto & neighbour : (i ? h2 : h1).neighbouringTiles())
+			if(const auto * s = cbc->getBattle(battleID)->battleGetUnitByPos(neighbour))
+				if(s->isShooter())
+					shooters[i]++;
+	}
+
+	return shooters[0] < shooters[1];
+}
+
+void CStupidAI::yourTacticPhase(const BattleID & battleID, int distance)
+{
+	cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide()));
+}
+
+void CStupidAI::activeStack(const BattleID & battleID, const CStack * stack)
+{
+	//boost::this_thread::sleep_for(boost::chrono::seconds(2));
+	print("activeStack called for " + stack->nodeName());
+	ReachabilityInfo dists = cb->getBattle(battleID)->getReachability(stack);
+	std::vector<EnemyInfo> enemiesShootable, enemiesReachable, enemiesUnreachable;
+
+	if(stack->creatureId() == CreatureID::CATAPULT)
+	{
+		BattleAction attack;
+		static const std::vector<int> wallHexes = {50, 183, 182, 130, 78, 29, 12, 95};
+		auto seletectedHex = *RandomGeneratorUtil::nextItem(wallHexes, CRandomGenerator::getDefault());
+		attack.aimToHex(seletectedHex);
+		attack.actionType = EActionType::CATAPULT;
+		attack.side = side;
+		attack.stackNumber = stack->unitId();
+
+		cb->battleMakeUnitAction(battleID, attack);
+		return;
+	}
+	else if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON))
+	{
+		cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack));
+		return;
+	}
+
+	for (const CStack *s : cb->getBattle(battleID)->battleGetStacks(CBattleInfoEssentials::ONLY_ENEMY))
+	{
+		if(cb->getBattle(battleID)->battleCanShoot(stack, s->getPosition()))
+		{
+			enemiesShootable.push_back(s);
+		}
+		else
+		{
+			std::vector<BattleHex> avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(stack, false);
+
+			for (BattleHex hex : avHexes)
+			{
+				if(CStack::isMeleeAttackPossible(stack, s, hex))
+				{
+					std::vector<EnemyInfo>::iterator i = std::find(enemiesReachable.begin(), enemiesReachable.end(), s);
+					if(i == enemiesReachable.end())
+					{
+						enemiesReachable.push_back(s);
+						i = enemiesReachable.begin() + (enemiesReachable.size() - 1);
+					}
+
+					i->attackFrom.push_back(hex);
+				}
+			}
+
+			if(!vstd::contains(enemiesReachable, s) && s->getPosition().isValid())
+				enemiesUnreachable.push_back(s);
+		}
+	}
+
+	for ( auto & enemy : enemiesReachable )
+		enemy.calcDmg(battleID, stack);
+
+	for ( auto & enemy : enemiesShootable )
+		enemy.calcDmg(battleID, stack);
+
+	if(enemiesShootable.size())
+	{
+		const EnemyInfo &ei= *std::max_element(enemiesShootable.begin(), enemiesShootable.end(), isMoreProfitable);
+		cb->battleMakeUnitAction(battleID, BattleAction::makeShotAttack(stack, ei.s));
+		return;
+	}
+	else if(enemiesReachable.size())
+	{
+		const EnemyInfo &ei= *std::max_element(enemiesReachable.begin(), enemiesReachable.end(), &isMoreProfitable);
+		BattleHex targetHex = *std::max_element(ei.attackFrom.begin(), ei.attackFrom.end(), [&](auto a, auto b) { return willSecondHexBlockMoreEnemyShooters(battleID, a, b);});
+
+		cb->battleMakeUnitAction(battleID, BattleAction::makeMeleeAttack(stack, ei.s->getPosition(), targetHex));
+		return;
+	}
+	else if(enemiesUnreachable.size()) //due to #955 - a buggy battle may occur when there are no enemies
+	{
+		auto closestEnemy = vstd::minElementByFun(enemiesUnreachable, [&](const EnemyInfo & ei) -> int
+		{
+			return dists.distToNearestNeighbour(stack, ei.s);
+		});
+
+		if(dists.distToNearestNeighbour(stack, closestEnemy->s) < GameConstants::BFIELD_SIZE)
+		{
+			cb->battleMakeUnitAction(battleID, goTowards(battleID, stack, closestEnemy->s->getAttackableHexes(stack)));
+			return;
+		}
+	}
+
+	cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack));
+	return;
+}
+
+void CStupidAI::battleAttack(const BattleID & battleID, const BattleAttack *ba)
+{
+	print("battleAttack called");
+}
+
+void CStupidAI::battleStacksAttacked(const BattleID & battleID, const std::vector<BattleStackAttacked> & bsa, bool ranged)
+{
+	print("battleStacksAttacked called");
+}
+
+void CStupidAI::battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID)
+{
+	print("battleEnd called");
+}
+
+// void CStupidAI::battleResultsApplied()
+// {
+// 	print("battleResultsApplied called");
+// }
+
+void CStupidAI::battleNewRoundFirst(const BattleID & battleID)
+{
+	print("battleNewRoundFirst called");
+}
+
+void CStupidAI::battleNewRound(const BattleID & battleID)
+{
+	print("battleNewRound called");
+}
+
+void CStupidAI::battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport)
+{
+	print("battleStackMoved called");
+}
+
+void CStupidAI::battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc)
+{
+	print("battleSpellCast called");
+}
+
+void CStupidAI::battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse)
+{
+	print("battleStacksEffectsSet called");
+}
+
+void CStupidAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
+{
+	print("battleStart called");
+	side = Side;
+}
+
+void CStupidAI::battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca)
+{
+	print("battleCatapultAttacked called");
+}
+
+void CStupidAI::print(const std::string &text) const
+{
+	logAi->trace("CStupidAI  [%p]: %s", this, text);
+}
+
+BattleAction CStupidAI::goTowards(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> hexes) const
+{
+	auto reachability = cb->getBattle(battleID)->getReachability(stack);
+	auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false);
+
+	if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked
+	{
+		return BattleAction::makeDefend(stack);
+	}
+
+	std::sort(hexes.begin(), hexes.end(), [&](BattleHex h1, BattleHex h2) -> bool
+	{
+		return reachability.distances[h1] < reachability.distances[h2];
+	});
+
+	for(auto hex : hexes)
+	{
+		if(vstd::contains(avHexes, hex))
+			return BattleAction::makeMove(stack, hex);
+
+		if(stack->coversPos(hex))
+		{
+			logAi->warn("Warning: already standing on neighbouring tile!");
+			//We shouldn't even be here...
+			return BattleAction::makeDefend(stack);
+		}
+	}
+
+	BattleHex bestNeighbor = hexes.front();
+
+	if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE)
+	{
+		return BattleAction::makeDefend(stack);
+	}
+
+	if(stack->hasBonusOfType(BonusType::FLYING))
+	{
+		// Flying stack doesn't go hex by hex, so we can't backtrack using predecessors.
+		// We just check all available hexes and pick the one closest to the target.
+		auto nearestAvailableHex = vstd::minElementByFun(avHexes, [&](BattleHex hex) -> int
+		{
+			return BattleHex::getDistance(bestNeighbor, hex);
+		});
+
+		return BattleAction::makeMove(stack, *nearestAvailableHex);
+	}
+	else
+	{
+		BattleHex currentDest = bestNeighbor;
+		while(1)
+		{
+			if(!currentDest.isValid())
+			{
+				logAi->error("CBattleAI::goTowards: internal error");
+				return BattleAction::makeDefend(stack);
+			}
+
+			if(vstd::contains(avHexes, currentDest))
+				return BattleAction::makeMove(stack, currentDest);
+
+			currentDest = reachability.predecessors[currentDest];
+		}
+	}
+}

+ 56 - 56
AI/StupidAI/StupidAI.h

@@ -1,56 +1,56 @@
-/*
- * StupidAI.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/battle/BattleHex.h"
-#include "../../lib/battle/ReachabilityInfo.h"
-#include "../../lib/CGameInterface.h"
-
-class EnemyInfo;
-
-class CStupidAI : public CBattleGameInterface
-{
-	int side;
-	std::shared_ptr<CBattleCallback> cb;
-	std::shared_ptr<Environment> env;
-
-	bool wasWaitingForRealize;
-	bool wasUnlockingGs;
-
-	void print(const std::string &text) const;
-public:
-	CStupidAI();
-	~CStupidAI();
-
-	void initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB) override;
-	void initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB, AutocombatPreferences autocombatPreferences) override;
-
-	void actionFinished(const BattleID & battleID, const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero
-	void actionStarted(const BattleID & battleID, const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero
-	void activeStack(const BattleID & battleID, const CStack * stack) override; //called when it's turn of that stack
-	void yourTacticPhase(const BattleID & battleID, int distance) override;
-
-	void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; //called when stack is performing attack
-	void battleStacksAttacked(const BattleID & battleID, const std::vector<BattleStackAttacked> & bsa, bool ranged) override; //called when stack receives damage (after battleAttack())
-	void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override;
-	//void battleResultsApplied() override; //called when all effects of last battle are applied
-	void battleNewRoundFirst(const BattleID & battleID) override; //called at the beginning of each turn before changes are applied;
-	void battleNewRound(const BattleID & battleID) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn
-	void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport) override;
-	void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override;
-	void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override;//called when a specific effect is set to stacks
-	//void battleTriggerEffect(const BattleTriggerEffect & bte) override;
-	void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right
-	void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack
-
-private:
-	BattleAction goTowards(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> hexes) const;
-};
-
+/*
+ * StupidAI.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/battle/BattleHex.h"
+#include "../../lib/battle/ReachabilityInfo.h"
+#include "../../lib/CGameInterface.h"
+
+class EnemyInfo;
+
+class CStupidAI : public CBattleGameInterface
+{
+	int side;
+	std::shared_ptr<CBattleCallback> cb;
+	std::shared_ptr<Environment> env;
+
+	bool wasWaitingForRealize;
+	bool wasUnlockingGs;
+
+	void print(const std::string &text) const;
+public:
+	CStupidAI();
+	~CStupidAI();
+
+	void initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB) override;
+	void initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB, AutocombatPreferences autocombatPreferences) override;
+
+	void actionFinished(const BattleID & battleID, const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero
+	void actionStarted(const BattleID & battleID, const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero
+	void activeStack(const BattleID & battleID, const CStack * stack) override; //called when it's turn of that stack
+	void yourTacticPhase(const BattleID & battleID, int distance) override;
+
+	void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; //called when stack is performing attack
+	void battleStacksAttacked(const BattleID & battleID, const std::vector<BattleStackAttacked> & bsa, bool ranged) override; //called when stack receives damage (after battleAttack())
+	void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override;
+	//void battleResultsApplied() override; //called when all effects of last battle are applied
+	void battleNewRoundFirst(const BattleID & battleID) override; //called at the beginning of each turn before changes are applied;
+	void battleNewRound(const BattleID & battleID) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn
+	void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport) override;
+	void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override;
+	void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override;//called when a specific effect is set to stacks
+	//void battleTriggerEffect(const BattleTriggerEffect & bte) override;
+	void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right
+	void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack
+
+private:
+	BattleAction goTowards(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> hexes) const;
+};
+

+ 34 - 34
AI/StupidAI/main.cpp

@@ -1,34 +1,34 @@
-/*
- * main.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 "../../lib/AI_Base.h"
-#include "StupidAI.h"
-
-#ifdef __GNUC__
-#define strcpy_s(a, b, c) strncpy(a, c, b)
-#endif
-
-static const char *g_cszAiName = "Stupid AI 0.1";
-
-extern "C" DLL_EXPORT int GetGlobalAiVersion()
-{
-	return AI_INTERFACE_VER;
-}
-
-extern "C" DLL_EXPORT void GetAiName(char* name)
-{
-	strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName);
-}
-
-extern "C" DLL_EXPORT void GetNewBattleAI(std::shared_ptr<CBattleGameInterface> &out)
-{
-	out = std::make_shared<CStupidAI>();
-}
+/*
+ * main.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 "../../lib/AI_Base.h"
+#include "StupidAI.h"
+
+#ifdef __GNUC__
+#define strcpy_s(a, b, c) strncpy(a, c, b)
+#endif
+
+static const char *g_cszAiName = "Stupid AI 0.1";
+
+extern "C" DLL_EXPORT int GetGlobalAiVersion()
+{
+	return AI_INTERFACE_VER;
+}
+
+extern "C" DLL_EXPORT void GetAiName(char* name)
+{
+	strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName);
+}
+
+extern "C" DLL_EXPORT void GetNewBattleAI(std::shared_ptr<CBattleGameInterface> &out)
+{
+	out = std::make_shared<CStupidAI>();
+}

+ 259 - 259
AI/VCAI/AIUtility.cpp

@@ -1,259 +1,259 @@
-/*
- * AIUtility.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 "AIUtility.h"
-#include "VCAI.h"
-#include "FuzzyHelper.h"
-#include "Goals/Goals.h"
-
-#include "../../lib/UnlockGuard.h"
-#include "../../lib/CConfigHandler.h"
-#include "../../lib/CHeroHandler.h"
-#include "../../lib/mapObjects/CBank.h"
-#include "../../lib/mapObjects/CGTownInstance.h"
-#include "../../lib/mapObjects/CQuest.h"
-#include "../../lib/mapping/CMapDefines.h"
-
-extern FuzzyHelper * fh;
-
-const CGObjectInstance * ObjectIdRef::operator->() const
-{
-	return cb->getObj(id, false);
-}
-
-ObjectIdRef::operator const CGObjectInstance *() const
-{
-	return cb->getObj(id, false);
-}
-
-ObjectIdRef::operator bool() const
-{
-	return cb->getObj(id, false);
-}
-
-ObjectIdRef::ObjectIdRef(ObjectInstanceID _id)
-	: id(_id)
-{
-
-}
-
-ObjectIdRef::ObjectIdRef(const CGObjectInstance * obj)
-	: id(obj->id)
-{
-
-}
-
-bool ObjectIdRef::operator<(const ObjectIdRef & rhs) const
-{
-	return id < rhs.id;
-}
-
-HeroPtr::HeroPtr(const CGHeroInstance * H)
-{
-	if(!H)
-	{
-		//init from nullptr should equal to default init
-		*this = HeroPtr();
-		return;
-	}
-
-	h = H;
-	name = h->getNameTranslated();
-	hid = H->id;
-//	infosCount[ai->playerID][hid]++;
-}
-
-HeroPtr::HeroPtr()
-{
-	h = nullptr;
-	hid = ObjectInstanceID();
-}
-
-HeroPtr::~HeroPtr()
-{
-//	if(hid >= 0)
-//		infosCount[ai->playerID][hid]--;
-}
-
-bool HeroPtr::operator<(const HeroPtr & rhs) const
-{
-	return hid < rhs.hid;
-}
-
-const CGHeroInstance * HeroPtr::get(bool doWeExpectNull) const
-{
-	//TODO? check if these all assertions every time we get info about hero affect efficiency
-	//
-	//behave terribly when attempting unauthorized access to hero that is not ours (or was lost)
-	assert(doWeExpectNull || h);
-
-	if(h)
-	{
-		auto obj = cb->getObj(hid);
-		const bool owned = obj && obj->tempOwner == ai->playerID;
-
-		if(doWeExpectNull && !owned)
-		{
-			return nullptr;
-		}
-		else
-		{
-			assert(obj);
-			assert(owned);
-		}
-	}
-
-	return h;
-}
-
-const CGHeroInstance * HeroPtr::operator->() const
-{
-	return get();
-}
-
-bool HeroPtr::validAndSet() const
-{
-	return get(true);
-}
-
-const CGHeroInstance * HeroPtr::operator*() const
-{
-	return get();
-}
-
-bool HeroPtr::operator==(const HeroPtr & rhs) const
-{
-	return h == rhs.get(true);
-}
-
-bool CDistanceSorter::operator()(const CGObjectInstance * lhs, const CGObjectInstance * rhs) const
-{
-	const CGPathNode * ln = ai->myCb->getPathsInfo(hero)->getPathInfo(lhs->visitablePos());
-	const CGPathNode * rn = ai->myCb->getPathsInfo(hero)->getPathInfo(rhs->visitablePos());
-
-	return ln->getCost() < rn->getCost();
-}
-
-bool isSafeToVisit(HeroPtr h, crint3 tile)
-{
-	return isSafeToVisit(h, fh->evaluateDanger(tile, h.get()));
-}
-
-bool isSafeToVisit(HeroPtr h, uint64_t dangerStrength)
-{
-	const ui64 heroStrength = h->getTotalStrength();
-
-	if(dangerStrength)
-	{
-		return heroStrength / SAFE_ATTACK_CONSTANT > dangerStrength;
-	}
-
-	return true; //there's no danger
-}
-
-bool isObjectRemovable(const CGObjectInstance * obj)
-{
-	//FIXME: move logic to object property!
-	switch (obj->ID)
-	{
-	case Obj::MONSTER:
-	case Obj::RESOURCE:
-	case Obj::CAMPFIRE:
-	case Obj::TREASURE_CHEST:
-	case Obj::ARTIFACT:
-	case Obj::BORDERGUARD:
-	case Obj::FLOTSAM:
-	case Obj::PANDORAS_BOX:
-	case Obj::OCEAN_BOTTLE:
-	case Obj::SEA_CHEST:
-	case Obj::SHIPWRECK_SURVIVOR:
-	case Obj::SPELL_SCROLL:
-		return true;
-		break;
-	default:
-		return false;
-		break;
-	}
-
-}
-
-bool canBeEmbarkmentPoint(const TerrainTile * t, bool fromWater)
-{
-	// TODO: Such information should be provided by pathfinder
-	// Tile must be free or with unoccupied boat
-	if(!t->blocked)
-	{
-		return true;
-	}
-	else if(!fromWater) // do not try to board when in water sector
-	{
-		if(t->visitableObjects.size() == 1 && t->topVisitableId() == Obj::BOAT)
-			return true;
-	}
-	return false;
-}
-
-bool isBlockedBorderGate(int3 tileToHit) //TODO: is that function needed? should be handled by pathfinder
-{
-	if(cb->getTile(tileToHit)->topVisitableId() != Obj::BORDER_GATE)
-		return false;
-	auto gate = dynamic_cast<const CGKeys *>(cb->getTile(tileToHit)->topVisitableObj());
-	return !gate->passableFor(ai->playerID);
-}
-
-bool isBlockVisitObj(const int3 & pos)
-{
-	if(auto obj = cb->getTopObj(pos))
-	{
-		if(obj->isBlockedVisitable()) //we can't stand on that object
-			return true;
-	}
-
-	return false;
-}
-
-creInfo infoFromDC(const dwellingContent & dc)
-{
-	creInfo ci;
-	ci.count = dc.first;
-	ci.creID = dc.second.size() ? dc.second.back() : CreatureID(-1); //should never be accessed
-	if (ci.creID != CreatureID::NONE)
-	{
-		ci.cre = VLC->creatures()->getById(ci.creID);
-		ci.level = ci.cre->getLevel(); //this is creature tier, while tryRealize expects dwelling level. Ignore.
-	}
-	else
-	{
-		ci.cre = nullptr;
-		ci.level = 0;
-	}
-	return ci;
-}
-
-bool compareHeroStrength(HeroPtr h1, HeroPtr h2)
-{
-	return h1->getTotalStrength() < h2->getTotalStrength();
-}
-
-bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2)
-{
-	return a1->getArmyStrength() < a2->getArmyStrength();
-}
-
-bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2)
-{
-	auto art1 = a1->artType;
-	auto art2 = a2->artType;
-
-	if(art1->getPrice() == art2->getPrice())
-		return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL);
-	else
-		return art1->getPrice() > art2->getPrice();
-}
+/*
+ * AIUtility.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 "AIUtility.h"
+#include "VCAI.h"
+#include "FuzzyHelper.h"
+#include "Goals/Goals.h"
+
+#include "../../lib/UnlockGuard.h"
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/CHeroHandler.h"
+#include "../../lib/mapObjects/CBank.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
+#include "../../lib/mapObjects/CQuest.h"
+#include "../../lib/mapping/CMapDefines.h"
+
+extern FuzzyHelper * fh;
+
+const CGObjectInstance * ObjectIdRef::operator->() const
+{
+	return cb->getObj(id, false);
+}
+
+ObjectIdRef::operator const CGObjectInstance *() const
+{
+	return cb->getObj(id, false);
+}
+
+ObjectIdRef::operator bool() const
+{
+	return cb->getObj(id, false);
+}
+
+ObjectIdRef::ObjectIdRef(ObjectInstanceID _id)
+	: id(_id)
+{
+
+}
+
+ObjectIdRef::ObjectIdRef(const CGObjectInstance * obj)
+	: id(obj->id)
+{
+
+}
+
+bool ObjectIdRef::operator<(const ObjectIdRef & rhs) const
+{
+	return id < rhs.id;
+}
+
+HeroPtr::HeroPtr(const CGHeroInstance * H)
+{
+	if(!H)
+	{
+		//init from nullptr should equal to default init
+		*this = HeroPtr();
+		return;
+	}
+
+	h = H;
+	name = h->getNameTranslated();
+	hid = H->id;
+//	infosCount[ai->playerID][hid]++;
+}
+
+HeroPtr::HeroPtr()
+{
+	h = nullptr;
+	hid = ObjectInstanceID();
+}
+
+HeroPtr::~HeroPtr()
+{
+//	if(hid >= 0)
+//		infosCount[ai->playerID][hid]--;
+}
+
+bool HeroPtr::operator<(const HeroPtr & rhs) const
+{
+	return hid < rhs.hid;
+}
+
+const CGHeroInstance * HeroPtr::get(bool doWeExpectNull) const
+{
+	//TODO? check if these all assertions every time we get info about hero affect efficiency
+	//
+	//behave terribly when attempting unauthorized access to hero that is not ours (or was lost)
+	assert(doWeExpectNull || h);
+
+	if(h)
+	{
+		auto obj = cb->getObj(hid);
+		const bool owned = obj && obj->tempOwner == ai->playerID;
+
+		if(doWeExpectNull && !owned)
+		{
+			return nullptr;
+		}
+		else
+		{
+			assert(obj);
+			assert(owned);
+		}
+	}
+
+	return h;
+}
+
+const CGHeroInstance * HeroPtr::operator->() const
+{
+	return get();
+}
+
+bool HeroPtr::validAndSet() const
+{
+	return get(true);
+}
+
+const CGHeroInstance * HeroPtr::operator*() const
+{
+	return get();
+}
+
+bool HeroPtr::operator==(const HeroPtr & rhs) const
+{
+	return h == rhs.get(true);
+}
+
+bool CDistanceSorter::operator()(const CGObjectInstance * lhs, const CGObjectInstance * rhs) const
+{
+	const CGPathNode * ln = ai->myCb->getPathsInfo(hero)->getPathInfo(lhs->visitablePos());
+	const CGPathNode * rn = ai->myCb->getPathsInfo(hero)->getPathInfo(rhs->visitablePos());
+
+	return ln->getCost() < rn->getCost();
+}
+
+bool isSafeToVisit(HeroPtr h, crint3 tile)
+{
+	return isSafeToVisit(h, fh->evaluateDanger(tile, h.get()));
+}
+
+bool isSafeToVisit(HeroPtr h, uint64_t dangerStrength)
+{
+	const ui64 heroStrength = h->getTotalStrength();
+
+	if(dangerStrength)
+	{
+		return heroStrength / SAFE_ATTACK_CONSTANT > dangerStrength;
+	}
+
+	return true; //there's no danger
+}
+
+bool isObjectRemovable(const CGObjectInstance * obj)
+{
+	//FIXME: move logic to object property!
+	switch (obj->ID)
+	{
+	case Obj::MONSTER:
+	case Obj::RESOURCE:
+	case Obj::CAMPFIRE:
+	case Obj::TREASURE_CHEST:
+	case Obj::ARTIFACT:
+	case Obj::BORDERGUARD:
+	case Obj::FLOTSAM:
+	case Obj::PANDORAS_BOX:
+	case Obj::OCEAN_BOTTLE:
+	case Obj::SEA_CHEST:
+	case Obj::SHIPWRECK_SURVIVOR:
+	case Obj::SPELL_SCROLL:
+		return true;
+		break;
+	default:
+		return false;
+		break;
+	}
+
+}
+
+bool canBeEmbarkmentPoint(const TerrainTile * t, bool fromWater)
+{
+	// TODO: Such information should be provided by pathfinder
+	// Tile must be free or with unoccupied boat
+	if(!t->blocked)
+	{
+		return true;
+	}
+	else if(!fromWater) // do not try to board when in water sector
+	{
+		if(t->visitableObjects.size() == 1 && t->topVisitableId() == Obj::BOAT)
+			return true;
+	}
+	return false;
+}
+
+bool isBlockedBorderGate(int3 tileToHit) //TODO: is that function needed? should be handled by pathfinder
+{
+	if(cb->getTile(tileToHit)->topVisitableId() != Obj::BORDER_GATE)
+		return false;
+	auto gate = dynamic_cast<const CGKeys *>(cb->getTile(tileToHit)->topVisitableObj());
+	return !gate->passableFor(ai->playerID);
+}
+
+bool isBlockVisitObj(const int3 & pos)
+{
+	if(auto obj = cb->getTopObj(pos))
+	{
+		if(obj->isBlockedVisitable()) //we can't stand on that object
+			return true;
+	}
+
+	return false;
+}
+
+creInfo infoFromDC(const dwellingContent & dc)
+{
+	creInfo ci;
+	ci.count = dc.first;
+	ci.creID = dc.second.size() ? dc.second.back() : CreatureID(-1); //should never be accessed
+	if (ci.creID != CreatureID::NONE)
+	{
+		ci.cre = VLC->creatures()->getById(ci.creID);
+		ci.level = ci.cre->getLevel(); //this is creature tier, while tryRealize expects dwelling level. Ignore.
+	}
+	else
+	{
+		ci.cre = nullptr;
+		ci.level = 0;
+	}
+	return ci;
+}
+
+bool compareHeroStrength(HeroPtr h1, HeroPtr h2)
+{
+	return h1->getTotalStrength() < h2->getTotalStrength();
+}
+
+bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2)
+{
+	return a1->getArmyStrength() < a2->getArmyStrength();
+}
+
+bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2)
+{
+	auto art1 = a1->artType;
+	auto art2 = a2->artType;
+
+	if(art1->getPrice() == art2->getPrice())
+		return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL);
+	else
+		return art1->getPrice() > art2->getPrice();
+}

+ 2912 - 2912
AI/VCAI/VCAI.cpp

@@ -1,2912 +1,2912 @@
-/*
- * VCAI.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 "VCAI.h"
-#include "FuzzyHelper.h"
-#include "ResourceManager.h"
-#include "BuildingManager.h"
-#include "Goals/Goals.h"
-
-#include "../../lib/UnlockGuard.h"
-#include "../../lib/mapObjects/MapObjects.h"
-#include "../../lib/mapObjects/ObjectTemplate.h"
-#include "../../lib/CConfigHandler.h"
-#include "../../lib/CHeroHandler.h"
-#include "../../lib/GameSettings.h"
-#include "../../lib/gameState/CGameState.h"
-#include "../../lib/NetPacksBase.h"
-#include "../../lib/NetPacks.h"
-#include "../../lib/bonuses/CBonusSystemNode.h"
-#include "../../lib/bonuses/Limiters.h"
-#include "../../lib/bonuses/Updaters.h"
-#include "../../lib/bonuses/Propagators.h"
-#include "../../lib/serializer/CTypeList.h"
-#include "../../lib/serializer/BinarySerializer.h"
-#include "../../lib/serializer/BinaryDeserializer.h"
-
-#include "AIhelper.h"
-
-extern FuzzyHelper * fh;
-
-const double SAFE_ATTACK_CONSTANT = 1.5;
-
-//one thread may be turn of AI and another will be handling a side effect for AI2
-thread_local CCallback * cb = nullptr;
-thread_local VCAI * ai = nullptr;
-
-//std::map<int, std::map<int, int> > HeroView::infosCount;
-
-//helper RAII to manage global ai/cb ptrs
-struct SetGlobalState
-{
-	SetGlobalState(VCAI * AI)
-	{
-		assert(!ai);
-		assert(!cb);
-
-		ai = AI;
-		cb = AI->myCb.get();
-	}
-	~SetGlobalState()
-	{
-		//TODO: how to handle rm? shouldn't be called after ai is destroyed, hopefully
-		//TODO: to ensure that, make rm unique_ptr
-		ai = nullptr;
-		cb = nullptr;
-	}
-};
-
-
-#define SET_GLOBAL_STATE(ai) SetGlobalState _hlpSetState(ai);
-
-#define NET_EVENT_HANDLER SET_GLOBAL_STATE(this)
-#define MAKING_TURN SET_GLOBAL_STATE(this)
-
-VCAI::VCAI()
-{
-	LOG_TRACE(logAi);
-	makingTurn = nullptr;
-	destinationTeleport = ObjectInstanceID();
-	destinationTeleportPos = int3(-1);
-
-	ah = new AIhelper();
-	ah->setAI(this);
-}
-
-VCAI::~VCAI()
-{
-	delete ah;
-	LOG_TRACE(logAi);
-	finish();
-}
-
-void VCAI::availableCreaturesChanged(const CGDwelling * town)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::heroMoved(const TryMoveHero & details, bool verbose)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-
-	//enemy hero may have left visible area
-	validateObject(details.id);
-	auto hero = cb->getHero(details.id);
-
-	const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));;
-	const int3 to   = hero ? hero->convertToVisitablePos(details.end)   : (details.end   - int3(0,1,0));
-
-	const CGObjectInstance * o1 = vstd::frontOrNull(cb->getVisitableObjs(from, verbose));
-	const CGObjectInstance * o2 = vstd::frontOrNull(cb->getVisitableObjs(to, verbose));
-
-	if(details.result == TryMoveHero::TELEPORTATION)
-	{
-		auto t1 = dynamic_cast<const CGTeleport *>(o1);
-		auto t2 = dynamic_cast<const CGTeleport *>(o2);
-		if(t1 && t2)
-		{
-			if(cb->isTeleportChannelBidirectional(t1->channel))
-			{
-				if(o1->ID == Obj::SUBTERRANEAN_GATE && o1->ID == o2->ID) // We need to only add subterranean gates in knownSubterraneanGates. Used for features not yet ported to use teleport channels
-				{
-					knownSubterraneanGates[o1] = o2;
-					knownSubterraneanGates[o2] = o1;
-					logAi->debug("Found a pair of subterranean gates between %s and %s!", from.toString(), to.toString());
-				}
-			}
-		}
-		//FIXME: teleports are not correctly visited
-		unreserveObject(hero, t1);
-		unreserveObject(hero, t2);
-	}
-	else if(details.result == TryMoveHero::EMBARK && hero)
-	{
-		//make sure AI not attempt to visit used boat
-		validateObject(hero->boat);
-	}
-	else if(details.result == TryMoveHero::DISEMBARK && o1)
-	{
-		auto boat = dynamic_cast<const CGBoat *>(o1);
-		if(boat)
-			addVisitableObj(boat);
-	}
-}
-
-void VCAI::heroInGarrisonChange(const CGTownInstance * town)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::centerView(int3 pos, int focusTime)
-{
-	LOG_TRACE_PARAMS(logAi, "focusTime '%i'", focusTime);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::artifactAssembled(const ArtifactLocation & al)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-
-	status.addQuery(queryID, "TavernWindow");
-	requestActionASAP([=](){ answerQuery(queryID, 0); });
-}
-
-void VCAI::showThievesGuildWindow(const CGObjectInstance * obj)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::playerBlocked(int reason, bool start)
-{
-	LOG_TRACE_PARAMS(logAi, "reason '%i', start '%i'", reason % start);
-	NET_EVENT_HANDLER;
-	if(start && reason == PlayerBlocked::UPCOMING_BATTLE)
-		status.setBattle(UPCOMING_BATTLE);
-
-	if(reason == PlayerBlocked::ONGOING_MOVEMENT)
-		status.setMove(start);
-}
-
-void VCAI::showPuzzleMap()
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::showShipyardDialog(const IShipyard * obj)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult)
-{
-	LOG_TRACE_PARAMS(logAi, "victoryLossCheckResult '%s'", victoryLossCheckResult.messageToSelf.toString());
-	NET_EVENT_HANDLER;
-	logAi->debug("Player %d (%s): I heard that player %d (%s) %s.", playerID, playerID.toString(), player, player.toString(), (victoryLossCheckResult.victory() ? "won" : "lost"));
-	if(player == playerID)
-	{
-		if(victoryLossCheckResult.victory())
-		{
-			logAi->debug("VCAI: I won! Incredible!");
-			logAi->debug("Turn nr %d", myCb->getDate());
-		}
-		else
-		{
-			logAi->debug("VCAI: Player %d (%s) lost. It's me. What a disappointment! :(", player, player.toString());
-		}
-
-		finish();
-	}
-}
-
-void VCAI::artifactPut(const ArtifactLocation & al)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::artifactRemoved(const ArtifactLocation & al)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::artifactDisassembled(const ArtifactLocation & al)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start)
-{
-	LOG_TRACE_PARAMS(logAi, "start '%i'; obj '%s'", start % (visitedObj ? visitedObj->getObjectName() : std::string("n/a")));
-	NET_EVENT_HANDLER;
-
-	if(start && visitedObj) //we can end visit with null object, anyway
-	{
-		markObjectVisited(visitedObj);
-		unreserveObject(visitor, visitedObj);
-		completeGoal(sptr(Goals::VisitObj(visitedObj->id.getNum()).sethero(visitor))); //we don't need to visit it anymore
-		//TODO: what if we visited one-time visitable object that was reserved by another hero (shouldn't, but..)
-		if (visitedObj->ID == Obj::HERO)
-		{
-			visitedHeroes[visitor].insert(HeroPtr(dynamic_cast<const CGHeroInstance *>(visitedObj)));
-		}
-	}
-
-	status.heroVisit(visitedObj, start);
-}
-
-void VCAI::availableArtifactsChanged(const CGBlackMarket * bm)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-	//buildArmyIn(town);
-	//moveCreaturesToHero(town);
-}
-
-void VCAI::tileHidden(const std::unordered_set<int3> & pos)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-
-	validateVisitableObjs();
-	clearPathsInfo();
-}
-
-void VCAI::tileRevealed(const std::unordered_set<int3> & pos)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-	for(int3 tile : pos)
-	{
-		for(const CGObjectInstance * obj : myCb->getVisitableObjs(tile))
-			addVisitableObj(obj);
-	}
-
-	clearPathsInfo();
-}
-
-void VCAI::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-
-	auto firstHero = cb->getHero(hero1);
-	auto secondHero = cb->getHero(hero2);
-
-	status.addQuery(query, boost::str(boost::format("Exchange between heroes %s (%d) and %s (%d)") % firstHero->getNameTranslated() % firstHero->tempOwner % secondHero->getNameTranslated() % secondHero->tempOwner));
-
-	requestActionASAP([=]()
-	{
-		float goalpriority1 = 0, goalpriority2 = 0;
-
-		auto firstGoal = getGoal(firstHero);
-		if(firstGoal->goalType == Goals::GATHER_ARMY)
-			goalpriority1 = firstGoal->priority;
-		auto secondGoal = getGoal(secondHero);
-		if(secondGoal->goalType == Goals::GATHER_ARMY)
-			goalpriority2 = secondGoal->priority;
-
-		auto transferFrom2to1 = [this](const CGHeroInstance * h1, const CGHeroInstance * h2) -> void
-		{
-			this->pickBestCreatures(h1, h2);
-			this->pickBestArtifacts(h1, h2);
-		};
-
-		//Do not attempt army or artifacts exchange if we visited ally player
-		//Visits can still be useful if hero have skills like Scholar
-		if(firstHero->tempOwner != secondHero->tempOwner)
-		{
-			logAi->debug("Heroes owned by different players. Do not exchange army or artifacts.");
-		}
-		else if(goalpriority1 > goalpriority2)
-		{
-			transferFrom2to1(firstHero, secondHero);
-		}
-		else if(goalpriority1 < goalpriority2)
-		{
-			transferFrom2to1(secondHero, firstHero);
-		}
-		else //regular criteria
-		{
-			if(firstHero->getFightingStrength() > secondHero->getFightingStrength() && ah->canGetArmy(firstHero, secondHero))
-				transferFrom2to1(firstHero, secondHero);
-			else if(ah->canGetArmy(secondHero, firstHero))
-				transferFrom2to1(secondHero, firstHero);
-		}
-
-		completeGoal(sptr(Goals::VisitHero(firstHero->id.getNum()))); //TODO: what if we were visited by other hero in the meantime?
-		completeGoal(sptr(Goals::VisitHero(secondHero->id.getNum())));
-
-		answerQuery(query, 0);
-	});
-}
-
-void VCAI::heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val)
-{
-	LOG_TRACE_PARAMS(logAi, "which '%i', val '%i'", static_cast<int>(which) % val);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level, QueryID queryID)
-{
-	LOG_TRACE_PARAMS(logAi, "level '%i'", level);
-	NET_EVENT_HANDLER;
-
-	status.addQuery(queryID, "RecruitmentDialog");
-	requestActionASAP([=](){
-		recruitCreatures(dwelling, dst);
-		checkHeroArmy(dynamic_cast<const CGHeroInstance*>(dst));
-		answerQuery(queryID, 0);
-	});
-}
-
-void VCAI::heroMovePointsChanged(const CGHeroInstance * hero)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::newObject(const CGObjectInstance * obj)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-	if(obj->isVisitable())
-		addVisitableObj(obj);
-}
-
-//to prevent AI from accessing objects that got deleted while they became invisible (Cover of Darkness, enemy hero moved etc.) below code allows AI to know deletion of objects out of sight
-//see: RemoveObject::applyFirstCl, to keep AI "not cheating" do not use advantage of this and use this function just to prevent crashes
-void VCAI::objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-
-	vstd::erase_if_present(visitableObjs, obj);
-	vstd::erase_if_present(alreadyVisited, obj);
-
-	for(auto h : cb->getHeroesInfo())
-		unreserveObject(h, obj);
-
-	std::function<bool(const Goals::TSubgoal &)> checkRemovalValidity = [&](const Goals::TSubgoal & x) -> bool
-	{
-		if((x->goalType == Goals::VISIT_OBJ) && (x->objid == obj->id.getNum()))
-			return true;
-		else if(x->parent && checkRemovalValidity(x->parent)) //repeat this lambda check recursively on parent goal
-			return true;
-		else
-			return false;
-	};
-
-	//clear VCAI / main loop caches
-	vstd::erase_if(lockedHeroes, [&](const std::pair<HeroPtr, Goals::TSubgoal> & x) -> bool
-	{
-		return checkRemovalValidity(x.second);
-	});
-
-	vstd::erase_if(ultimateGoalsFromBasic, [&](const std::pair<Goals::TSubgoal, Goals::TGoalVec> & x) -> bool
-	{
-		return checkRemovalValidity(x.first);
-	});
-
-	vstd::erase_if(basicGoals, checkRemovalValidity);
-	vstd::erase_if(goalsToAdd, checkRemovalValidity);
-	vstd::erase_if(goalsToRemove, checkRemovalValidity);
-
-	for(auto goal : ultimateGoalsFromBasic)
-		vstd::erase_if(goal.second, checkRemovalValidity);
-
-	//clear resource manager goal cache
-	ah->removeOutdatedObjectives(checkRemovalValidity);
-
-	//TODO: Find better way to handle hero boat removal
-	if(auto hero = dynamic_cast<const CGHeroInstance *>(obj))
-	{
-		if(hero->boat)
-		{
-			vstd::erase_if_present(visitableObjs, hero->boat);
-			vstd::erase_if_present(alreadyVisited, hero->boat);
-
-			for(auto h : cb->getHeroesInfo())
-				unreserveObject(h, hero->boat);
-		}
-	}
-
-	//TODO
-	//there are other places where CGObjectinstance ptrs are stored...
-	//
-
-	if(obj->ID == Obj::HERO && obj->tempOwner == playerID)
-	{
-		lostHero(cb->getHero(obj->id)); //we can promote, since objectRemoved is called just before actual deletion
-	}
-}
-
-void VCAI::showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-
-	requestActionASAP([=]()
-	{
-		makePossibleUpgrades(visitor);
-	});
-}
-
-void VCAI::playerBonusChanged(const Bonus & bonus, bool gain)
-{
-	LOG_TRACE_PARAMS(logAi, "gain '%i'", gain);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::heroCreated(const CGHeroInstance * h)
-{
-	LOG_TRACE(logAi);
-	if(h->visitedTown)
-		townVisitsThisWeek[HeroPtr(h)].insert(h->visitedTown);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::advmapSpellCast(const CGHeroInstance * caster, SpellID spellID)
-{
-	LOG_TRACE_PARAMS(logAi, "spellID '%i", spellID);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector<Component> & components, int soundID)
-{
-	LOG_TRACE_PARAMS(logAi, "soundID '%i'", soundID);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::requestRealized(PackageApplied * pa)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-	if(status.haveTurn())
-	{
-		if(pa->packType == typeList.getTypeID<EndTurn>())
-		{
-			if(pa->result)
-				status.madeTurn();
-		}
-	}
-
-	if(pa->packType == typeList.getTypeID<QueryReply>())
-	{
-		status.receivedAnswerConfirmation(pa->requestID, pa->result);
-	}
-}
-
-void VCAI::receivedResource()
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-
-	status.addQuery(queryID, "UniversityWindow");
-	requestActionASAP([=](){ answerQuery(queryID, 0); });
-}
-
-void VCAI::heroManaPointsChanged(const CGHeroInstance * hero)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val)
-{
-	LOG_TRACE_PARAMS(logAi, "which '%d', val '%d'", which % val);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::battleResultsApplied()
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-	assert(status.getBattle() == ENDING_BATTLE);
-	status.setBattle(NO_BATTLE);
-}
-
-void VCAI::beforeObjectPropertyChanged(const SetObjectProperty * sop)
-{
-
-}
-
-void VCAI::objectPropertyChanged(const SetObjectProperty * sop)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-	if(sop->what == ObjProperty::OWNER)
-	{
-		if(myCb->getPlayerRelations(playerID, (PlayerColor)sop->val) == PlayerRelations::ENEMIES)
-		{
-			//we want to visit objects owned by oppponents
-			auto obj = myCb->getObj(sop->id, false);
-			if(obj)
-			{
-				addVisitableObj(obj); // TODO: Remove once save compatability broken. In past owned objects were removed from this set
-				vstd::erase_if_present(alreadyVisited, obj);
-			}
-		}
-	}
-}
-
-void VCAI::buildChanged(const CGTownInstance * town, BuildingID buildingID, int what)
-{
-	LOG_TRACE_PARAMS(logAi, "what '%i'", what);
-	NET_EVENT_HANDLER;
-
-	if(town->getOwner() == playerID && what == 1) //built
-		completeGoal(sptr(Goals::BuildThis(buildingID, town)));
-}
-
-void VCAI::heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain)
-{
-	LOG_TRACE_PARAMS(logAi, "gain '%i'", gain);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID)
-{
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-
-	status.addQuery(queryID, "MarketWindow");
-	requestActionASAP([=](){ answerQuery(queryID, 0); });
-}
-
-void VCAI::showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain)
-{
-	//TODO: AI support for ViewXXX spell
-	LOG_TRACE(logAi);
-	NET_EVENT_HANDLER;
-}
-
-void VCAI::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB)
-{
-	LOG_TRACE(logAi);
-	env = ENV;
-	myCb = CB;
-	cbc = CB;
-
-	ah->init(CB.get());
-
-	NET_EVENT_HANDLER; //sets ah->rm->cb
-	playerID = *myCb->getPlayerID();
-	myCb->waitTillRealize = true;
-	myCb->unlockGsWhenWaiting = true;
-
-	if(!fh)
-		fh = new FuzzyHelper();
-
-	retrieveVisitableObjs();
-}
-
-void VCAI::yourTurn(QueryID queryID)
-{
-	LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID);
-	NET_EVENT_HANDLER;
-	status.addQuery(queryID, "YourTurn");
-	requestActionASAP([=](){ answerQuery(queryID, 0); });
-	status.startedTurn();
-	makingTurn = std::make_unique<boost::thread>(&VCAI::makeTurn, this);
-}
-
-void VCAI::heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector<SecondarySkill> & skills, QueryID queryID)
-{
-	LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID);
-	NET_EVENT_HANDLER;
-	status.addQuery(queryID, boost::str(boost::format("Hero %s got level %d") % hero->getNameTranslated() % hero->level));
-	requestActionASAP([=](){ answerQuery(queryID, 0); });
-}
-
-void VCAI::commanderGotLevel(const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID)
-{
-	LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID);
-	NET_EVENT_HANDLER;
-	status.addQuery(queryID, boost::str(boost::format("Commander %s of %s got level %d") % commander->name % commander->armyObj->nodeName() % (int)commander->level));
-	requestActionASAP([=](){ answerQuery(queryID, 0); });
-}
-
-void VCAI::showBlockingDialog(const std::string & text, const std::vector<Component> & components, QueryID askID, const int soundID, bool selection, bool cancel)
-{
-	LOG_TRACE_PARAMS(logAi, "text '%s', askID '%i', soundID '%i', selection '%i', cancel '%i'", text % askID % soundID % selection % cancel);
-	NET_EVENT_HANDLER;
-	int sel = 0;
-	status.addQuery(askID, boost::str(boost::format("Blocking dialog query with %d components - %s")
-									  % components.size() % text));
-
-	if(selection) //select from multiple components -> take the last one (they're indexed [1-size])
-		sel = static_cast<int>(components.size());
-
-	if(!selection && cancel) //yes&no -> always answer yes, we are a brave AI :)
-		sel = 1;
-
-	requestActionASAP([=]()
-	{
-		answerQuery(askID, sel);
-	});
-}
-
-void VCAI::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID)
-{
-//	LOG_TRACE_PARAMS(logAi, "askID '%i', exits '%s'", askID % exits);
-	NET_EVENT_HANDLER;
-	status.addQuery(askID, boost::str(boost::format("Teleport dialog query with %d exits")
-																			% exits.size()));
-
-	int choosenExit = -1;
-	if(impassable)
-	{
-		knownTeleportChannels[channel]->passability = TeleportChannel::IMPASSABLE;
-	}
-	else if(destinationTeleport != ObjectInstanceID() && destinationTeleportPos.valid())
-	{
-		auto neededExit = std::make_pair(destinationTeleport, destinationTeleportPos);
-		if(destinationTeleport != ObjectInstanceID() && vstd::contains(exits, neededExit))
-			choosenExit = vstd::find_pos(exits, neededExit);
-	}
-
-	for(auto exit : exits)
-	{
-		if(status.channelProbing() && exit.first == destinationTeleport)
-		{
-			choosenExit = vstd::find_pos(exits, exit);
-			break;
-		}
-		else
-		{
-			// TODO: Implement checking if visiting that teleport will uncovert any FoW
-			// So far this is the best option to handle decision about probing
-			auto obj = cb->getObj(exit.first, false);
-			if(obj == nullptr && !vstd::contains(teleportChannelProbingList, exit.first))
-			{
-				if(exit.first != destinationTeleport)
-					teleportChannelProbingList.push_back(exit.first);
-			}
-		}
-	}
-
-	requestActionASAP([=]()
-	{
-		answerQuery(askID, choosenExit);
-	});
-}
-
-void VCAI::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID)
-{
-	LOG_TRACE_PARAMS(logAi, "removableUnits '%i', queryID '%i'", removableUnits % queryID);
-	NET_EVENT_HANDLER;
-
-	std::string s1 = up ? up->nodeName() : "NONE";
-	std::string s2 = down ? down->nodeName() : "NONE";
-
-	status.addQuery(queryID, boost::str(boost::format("Garrison dialog with %s and %s") % s1 % s2));
-
-	//you can't request action from action-response thread
-	requestActionASAP([=]()
-	{
-		if(removableUnits)
-			pickBestCreatures(down, up);
-
-		answerQuery(queryID, 0);
-	});
-}
-
-void VCAI::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects)
-{
-	NET_EVENT_HANDLER;
-	status.addQuery(askID, "Map object select query");
-	requestActionASAP([=](){ answerQuery(askID, selectedObject.getNum()); });
-}
-
-void VCAI::saveGame(BinarySerializer & h, const int version)
-{
-	LOG_TRACE_PARAMS(logAi, "version '%i'", version);
-	NET_EVENT_HANDLER;
-	validateVisitableObjs();
-
-	#if 0
-	//disabled due to issue 2890
-	registerGoals(h);
-	#endif // 0
-	CAdventureAI::saveGame(h, version);
-	serializeInternal(h, version);
-}
-
-void VCAI::loadGame(BinaryDeserializer & h, const int version)
-{
-	LOG_TRACE_PARAMS(logAi, "version '%i'", version);
-	//NET_EVENT_HANDLER;
-
-	#if 0
-	//disabled due to issue 2890
-	registerGoals(h);
-	#endif // 0
-	CAdventureAI::loadGame(h, version);
-	serializeInternal(h, version);
-}
-
-void makePossibleUpgrades(const CArmedInstance * obj)
-{
-	if(!obj)
-		return;
-
-	for(int i = 0; i < GameConstants::ARMY_SIZE; i++)
-	{
-		if(const CStackInstance * s = obj->getStackPtr(SlotID(i)))
-		{
-			UpgradeInfo ui;
-			cb->fillUpgradeInfo(obj, SlotID(i), ui);
-			if(ui.oldID != CreatureID::NONE && cb->getResourceAmount().canAfford(ui.cost[0] * s->count))
-			{
-				cb->upgradeCreature(obj, SlotID(i), ui.newID[0]);
-			}
-		}
-	}
-}
-
-void VCAI::makeTurn()
-{
-	MAKING_TURN;
-
-	auto day = cb->getDate(Date::DAY);
-	logAi->info("Player %d (%s) starting turn, day %d", playerID, playerID.toString(), day);
-
-	boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
-	setThreadName("VCAI::makeTurn");
-
-	switch(cb->getDate(Date::DAY_OF_WEEK))
-	{
-	case 1:
-	{
-		townVisitsThisWeek.clear();
-		std::vector<const CGObjectInstance *> objs;
-		retrieveVisitableObjs(objs, true);
-		for(const CGObjectInstance * obj : objs)
-		{
-			if(isWeeklyRevisitable(obj))
-			{
-				addVisitableObj(obj);
-				vstd::erase_if_present(alreadyVisited, obj);
-			}
-		}
-		break;
-	}
-	}
-	markHeroAbleToExplore(primaryHero());
-	visitedHeroes.clear();
-
-	try
-	{
-		//it looks messy here, but it's better to have armed heroes before attempting realizing goals
-		for (const CGTownInstance * t : cb->getTownsInfo())
-			moveCreaturesToHero(t);
-
-		mainLoop();
-
-		/*Below function is also responsible for hero movement via internal wander function. By design it is separate logic for heroes that have nothing to do.
-		Heroes that were not picked by striveToGoal(sptr(Goals::Win())); recently (so they do not have new goals and cannot continue/reevaluate previously locked goals) will do logic in wander().*/
-		performTypicalActions();
-
-		//for debug purpose
-		for (auto h : cb->getHeroesInfo())
-		{
-			if (h->movementPointsRemaining())
-				logAi->info("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining());
-		}
-	}
-	catch (boost::thread_interrupted & e)
-	{
-	(void)e;
-		logAi->debug("Making turn thread has been interrupted. We'll end without calling endTurn.");
-		return;
-	}
-	catch (std::exception & e)
-	{
-		logAi->debug("Making turn thread has caught an exception: %s", e.what());
-	}
-
-	endTurn();
-}
-
-std::vector<HeroPtr> VCAI::getMyHeroes() const
-{
-	std::vector<HeroPtr> ret;
-
-	for(auto h : cb->getHeroesInfo())
-	{
-		ret.push_back(h);
-	}
-
-	return ret;
-}
-
-void VCAI::mainLoop()
-{
-	std::vector<Goals::TSubgoal> elementarGoals; //no duplicates allowed (operator ==)
-	basicGoals.clear();
-
-	validateVisitableObjs();
-
-	//get all potential and saved goals
-	//TODO: not lose
-	basicGoals.push_back(sptr(Goals::Win()));
-	for (auto goalPair : lockedHeroes)
-	{
-		fh->setPriority(goalPair.second);  //re-evaluate, as heroes moved in the meantime
-		basicGoals.push_back(goalPair.second);
-	}
-	if (ah->hasTasksLeft())
-		basicGoals.push_back(ah->whatToDo());
-	for (auto quest : myCb->getMyQuests())
-	{
-		basicGoals.push_back(sptr(Goals::CompleteQuest(quest)));
-	}
-	basicGoals.push_back(sptr(Goals::Build()));
-
-	invalidPathHeroes.clear();
-
-	for (int pass = 0; pass< 30 && basicGoals.size(); pass++)
-	{
-		vstd::removeDuplicates(basicGoals); //TODO: container which does this automagically without has would be nice
-		goalsToAdd.clear();
-		goalsToRemove.clear();
-		elementarGoals.clear();
-		ultimateGoalsFromBasic.clear();
-
-		ah->updatePaths(getMyHeroes());
-
-		logAi->debug("Main loop: decomposing %i basic goals", basicGoals.size());
-
-		for (auto basicGoal : basicGoals)
-		{
-			logAi->debug("Main loop: decomposing basic goal %s", basicGoal->name());
-
-			auto goalToDecompose = basicGoal;
-			Goals::TSubgoal elementarGoal = sptr(Goals::Invalid());
-			int maxAbstractGoals = 10;
-			while (!elementarGoal->isElementar && maxAbstractGoals)
-			{
-				try
-				{
-					elementarGoal = decomposeGoal(goalToDecompose);
-				}
-				catch (goalFulfilledException & e)
-				{
-					//it is impossible to continue some goals (like exploration, for example)
-					//complete abstract goal for now, but maybe main goal finds another path
-					logAi->debug("Goal %s decomposition failed: goal was completed as much as possible", e.goal->name());
-					completeGoal(e.goal); //put in goalsToRemove
-					break;
-				}
-				catch(cannotFulfillGoalException & e)
-				{
-					//it is impossible to continue some goals (like exploration, for example)
-					//complete abstract goal for now, but maybe main goal finds another path
-					goalsToRemove.push_back(basicGoal);
-					logAi->debug("Goal %s decomposition failed: %s", goalToDecompose->name(), e.what());
-					break;
-				}
-				catch (std::exception & e) //decomposition failed, which means we can't decompose entire tree
-				{
-					goalsToRemove.push_back(basicGoal);
-					logAi->debug("Goal %s decomposition failed: %s", basicGoal->name(), e.what());
-					break;
-				}
-				if (elementarGoal->isAbstract) //we can decompose it further
-				{
-					goalsToAdd.push_back(elementarGoal);
-					//decompose further now - this is necesssary if we can't add over 10 goals in the pool
-					goalToDecompose = elementarGoal;
-					//there is a risk of infinite abstract goal loop, though it indicates failed logic
-					maxAbstractGoals--;
-				}
-				else if (elementarGoal->isElementar) //should be
-				{
-					logAi->debug("Found elementar goal %s", elementarGoal->name());
-					elementarGoals.push_back(elementarGoal);
-					ultimateGoalsFromBasic[elementarGoal].push_back(goalToDecompose); //TODO: how about indirect basicGoal?
-					break;
-				}
-				else //should never be here
-					throw cannotFulfillGoalException("Goal %s is neither abstract nor elementar!" + basicGoal->name());
-			}
-		}
-
-		logAi->trace("Main loop: selecting best elementar goal");
-
-		//now choose one elementar goal to realize
-		Goals::TGoalVec possibleGoals(elementarGoals.begin(), elementarGoals.end()); //copy to vector
-		Goals::TSubgoal goalToRealize = sptr(Goals::Invalid());
-		while (possibleGoals.size())
-		{
-			//allow assign goals to heroes with 0 movement, but don't realize them
-			//maybe there are beter ones left
-
-			auto bestGoal = fh->chooseSolution(possibleGoals);
-			if (bestGoal->hero) //lock this hero to fulfill goal
-			{
-				setGoal(bestGoal->hero, bestGoal);
-				if (!bestGoal->hero->movementPointsRemaining() || vstd::contains(invalidPathHeroes, bestGoal->hero))
-				{
-					if (!vstd::erase_if_present(possibleGoals, bestGoal))
-					{
-						logAi->error("erase_if_preset failed? Something very wrong!");
-						break;
-					}
-					continue; //chose next from the list
-				}
-			}
-			goalToRealize = bestGoal; //we found our goal to execute
-			break;
-		}
-
-		//realize best goal
-		if (!goalToRealize->invalid())
-		{
-			logAi->debug("Trying to realize %s (value %2.3f)", goalToRealize->name(), goalToRealize->priority);
-
-			try
-			{
-				boost::this_thread::interruption_point();
-				goalToRealize->accept(this); //visitor pattern
-				boost::this_thread::interruption_point();
-			}
-			catch (boost::thread_interrupted & e)
-			{
-				(void)e;
-				logAi->debug("Player %d: Making turn thread received an interruption!", playerID);
-				throw; //rethrow, we want to truly end this thread
-			}
-			catch (goalFulfilledException & e)
-			{
-				//the sub-goal was completed successfully
-				completeGoal(e.goal);
-				//local goal was also completed?
-				completeGoal(goalToRealize);
-
-				// remove abstract visit tile if we completed the elementar one
-				vstd::erase_if_present(goalsToAdd, goalToRealize);
-			}
-			catch (std::exception & e)
-			{
-				logAi->debug("Failed to realize subgoal of type %s, I will stop.", goalToRealize->name());
-				logAi->debug("The error message was: %s", e.what());
-
-				//erase base goal if we failed to execute decomposed goal
-				for (auto basicGoal : ultimateGoalsFromBasic[goalToRealize])
-					goalsToRemove.push_back(basicGoal);
-
-				// sometimes resource manager contains an elementar goal which is not able to execute anymore and just fails each turn.
-				ai->ah->notifyGoalCompleted(goalToRealize);
-
-				//we failed to realize best goal, but maybe others are still possible?
-			}
-
-			//remove goals we couldn't decompose
-			for (auto goal : goalsToRemove)
-				vstd::erase_if_present(basicGoals, goal);
-
-			//add abstract goals
-			boost::sort(goalsToAdd, [](const Goals::TSubgoal & lhs, const Goals::TSubgoal & rhs) -> bool
-			{
-				return lhs->priority > rhs->priority; //highest priority at the beginning
-			});
-
-			//max number of goals = 10
-			int i = 0;
-			while (basicGoals.size() < 10 && goalsToAdd.size() > i)
-			{
-				if (!vstd::contains(basicGoals, goalsToAdd[i])) //don't add duplicates
-					basicGoals.push_back(goalsToAdd[i]);
-				i++;
-			}
-		}
-		else //no elementar goals possible
-		{
-			logAi->debug("Goal decomposition exhausted");
-			break;
-		}
-	}
-}
-
-void VCAI::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h)
-{
-	LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->getNameTranslated() % obj->getObjectName() % obj->pos.toString());
-	switch(obj->ID)
-	{
-	case Obj::TOWN:
-		moveCreaturesToHero(dynamic_cast<const CGTownInstance *>(obj));
-		if(h->visitedTown) //we are inside, not just attacking
-		{
-			townVisitsThisWeek[h].insert(h->visitedTown);
-			if(!h->hasSpellbook() && ah->freeGold() >= GameConstants::SPELLBOOK_GOLD_COST)
-			{
-				if(h->visitedTown->hasBuilt(BuildingID::MAGES_GUILD_1))
-					cb->buyArtifact(h.get(), ArtifactID::SPELLBOOK);
-			}
-		}
-		break;
-	}
-	completeGoal(sptr(Goals::VisitObj(obj->id.getNum()).sethero(h)));
-}
-
-void VCAI::moveCreaturesToHero(const CGTownInstance * t)
-{
-	if(t->visitingHero && t->armedGarrison() && t->visitingHero->tempOwner == t->tempOwner)
-	{
-		pickBestCreatures(t->visitingHero, t);
-	}
-}
-
-void VCAI::pickBestCreatures(const CArmedInstance * destinationArmy, const CArmedInstance * source)
-{
-	const CArmedInstance * armies[] = {destinationArmy, source};
-
-	auto bestArmy = ah->getSortedSlots(destinationArmy, source);
-
-	//foreach best type -> iterate over slots in both armies and if it's the appropriate type, send it to the slot where it belongs
-	for(SlotID i = SlotID(0); i.getNum() < bestArmy.size() && i.validSlot(); i.advance(1)) //i-th strongest creature type will go to i-th slot
-	{
-		const CCreature * targetCreature = bestArmy[i.getNum()].creature;
-
-		for(auto armyPtr : armies)
-		{
-			for(SlotID j = SlotID(0); j.validSlot(); j.advance(1))
-			{
-				if(armyPtr->getCreature(j) == targetCreature && (i != j || armyPtr != destinationArmy)) //it's a searched creature not in dst SLOT
-				{
-					//can't take away last creature without split. generate a new stack with 1 creature which is weak but fast
-					if(armyPtr == source
-						&& source->needsLastStack()
-						&& source->stacksCount() == 1
-						&& (!destinationArmy->hasStackAtSlot(i) || destinationArmy->getCreature(i) == targetCreature))
-					{
-						auto weakest = ah->getWeakestCreature(bestArmy);
-
-						if(weakest->creature == targetCreature)
-						{
-							if(1 == source->getStackCount(j))
-								break;
-
-							// move all except 1 of weakest creature from source to destination
-							cb->splitStack(
-								source,
-								destinationArmy,
-								j,
-								destinationArmy->getSlotFor(targetCreature),
-								destinationArmy->getStackCount(i) + source->getStackCount(j) - 1);
-
-							break;
-						}
-						else
-						{
-							// Source last stack is not weakest. Move 1 of weakest creature from destination to source
-							cb->splitStack(
-								destinationArmy,
-								source,
-								destinationArmy->getSlotFor(weakest->creature),
-								source->getFreeSlot(),
-								1);
-						}
-					}
-
-					cb->mergeOrSwapStacks(armyPtr, destinationArmy, j, i);
-				}
-			}
-		}
-	}
-
-	//TODO - having now strongest possible army, we may want to think about arranging stacks
-
-	auto hero = dynamic_cast<const CGHeroInstance *>(destinationArmy);
-	if(hero)
-		checkHeroArmy(hero);
-}
-
-void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * other)
-{
-	auto equipBest = [](const CGHeroInstance * h, const CGHeroInstance * otherh, bool giveStuffToFirstHero) -> void
-	{
-		bool changeMade = false;
-		do
-		{
-			changeMade = false;
-
-			//we collect gear always in same order
-			std::vector<ArtifactLocation> allArtifacts;
-			if(giveStuffToFirstHero)
-			{
-				for(auto p : h->artifactsWorn)
-				{
-					if(p.second.artifact)
-						allArtifacts.push_back(ArtifactLocation(h, p.first));
-				}
-			}
-			for(auto slot : h->artifactsInBackpack)
-				allArtifacts.push_back(ArtifactLocation(h, h->getArtPos(slot.artifact)));
-
-			if(otherh)
-			{
-				for(auto p : otherh->artifactsWorn)
-				{
-					if(p.second.artifact)
-						allArtifacts.push_back(ArtifactLocation(otherh, p.first));
-				}
-				for(auto slot : otherh->artifactsInBackpack)
-					allArtifacts.push_back(ArtifactLocation(otherh, otherh->getArtPos(slot.artifact)));
-			}
-			//we give stuff to one hero or another, depending on giveStuffToFirstHero
-
-			const CGHeroInstance * target = nullptr;
-			if(giveStuffToFirstHero || !otherh)
-				target = h;
-			else
-				target = otherh;
-
-			for(auto location : allArtifacts)
-			{
-				if(location.slot == ArtifactPosition::MACH4 || location.slot == ArtifactPosition::SPELLBOOK)
-					continue; // don't attempt to move catapult and spellbook
-
-				if(location.relatedObj() == target && location.slot < ArtifactPosition::AFTER_LAST)
-					continue; //don't reequip artifact we already wear
-
-				auto s = location.getSlot();
-				if(!s || s->locked) //we can't move locks
-					continue;
-				auto artifact = s->artifact;
-				if(!artifact)
-					continue;
-				//FIXME: why are the above possible to be null?
-
-				bool emptySlotFound = false;
-				for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType()))
-				{
-					ArtifactLocation destLocation(target, slot);
-					if(target->isPositionFree(slot) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move
-					{
-						cb->swapArtifacts(location, destLocation); //just put into empty slot
-						emptySlotFound = true;
-						changeMade = true;
-						break;
-					}
-				}
-				if(!emptySlotFound) //try to put that atifact in already occupied slot
-				{
-					for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType()))
-					{
-						auto otherSlot = target->getSlot(slot);
-						if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one
-						{
-							ArtifactLocation destLocation(target, slot);
-							//if that artifact is better than what we have, pick it
-							if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move
-							{
-								cb->swapArtifacts(location, ArtifactLocation(target, target->getArtPos(otherSlot->artifact)));
-								changeMade = true;
-								break;
-							}
-						}
-					}
-				}
-				if(changeMade)
-					break; //start evaluating artifacts from scratch
-			}
-		}
-		while(changeMade);
-	};
-
-	equipBest(h, other, true);
-
-	if(other)
-		equipBest(h, other, false);
-}
-
-void VCAI::recruitCreatures(const CGDwelling * d, const CArmedInstance * recruiter)
-{
-	//now used only for visited dwellings / towns, not BuyArmy goal
-	for(int i = 0; i < d->creatures.size(); i++)
-	{
-		if(!d->creatures[i].second.size())
-			continue;
-
-		int count = d->creatures[i].first;
-		CreatureID creID = d->creatures[i].second.back();
-
-		vstd::amin(count, ah->freeResources() / VLC->creatures()->getById(creID)->getFullRecruitCost());
-		if(count > 0)
-			cb->recruitCreatures(d, recruiter, creID, count, i);
-	}
-}
-
-bool VCAI::isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, std::optional<float> movementCostLimit)
-{
-	int3 op = obj->visitablePos();
-	auto paths = ah->getPathsToTile(h, op);
-
-	for(const auto & path : paths)
-	{
-		if(movementCostLimit && movementCostLimit.value() < path.movementCost())
-			return false;
-
-		if(isGoodForVisit(obj, h, path))
-			return true;
-	}
-
-	return false;
-}
-
-bool VCAI::isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, const AIPath & path) const
-{
-	const int3 pos = obj->visitablePos();
-	const int3 targetPos = path.firstTileToGet();
-	if (!targetPos.valid())
-		return false;
-	if (!isTileNotReserved(h.get(), targetPos))
-		return false;
-	if (obj->wasVisited(playerID))
-		return false;
-	if (cb->getPlayerRelations(playerID, obj->tempOwner) != PlayerRelations::ENEMIES && !isWeeklyRevisitable(obj))
-		return false; // Otherwise we flag or get weekly resources / creatures
-	if (!isSafeToVisit(h, pos))
-		return false;
-	if (!shouldVisit(h, obj))
-		return false;
-	if (vstd::contains(alreadyVisited, obj))
-		return false;
-	if (vstd::contains(reservedObjs, obj))
-		return false;
-
-	// TODO: looks extra if we already have AIPath
-	//if (!isAccessibleForHero(targetPos, h))
-	//	return false;
-
-	const CGObjectInstance * topObj = cb->getVisitableObjs(obj->visitablePos()).back(); //it may be hero visiting this obj
-																						//we don't try visiting object on which allied or owned hero stands
-																						// -> it will just trigger exchange windows and AI will be confused that obj behind doesn't get visited
-	return !(topObj->ID == Obj::HERO && cb->getPlayerRelations(h->tempOwner, topObj->tempOwner) != PlayerRelations::ENEMIES); //all of the following is met
-}
-
-bool VCAI::isTileNotReserved(const CGHeroInstance * h, int3 t) const
-{
-	if(t.valid())
-	{
-		auto obj = cb->getTopObj(t);
-		if(obj && vstd::contains(ai->reservedObjs, obj)
-			&& vstd::contains(reservedHeroesMap, h)
-			&& !vstd::contains(reservedHeroesMap.at(h), obj))
-			return false; //do not capture object reserved by another hero
-		else
-			return true;
-	}
-	else
-	{
-		return false;
-	}
-}
-
-bool VCAI::canRecruitAnyHero(const CGTownInstance * t) const
-{
-	//TODO: make gathering gold, building tavern or conquering town (?) possible subgoals
-	if(!t)
-		t = findTownWithTavern();
-	if(!t)
-		return false;
-	if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //TODO: use ResourceManager
-		return false;
-	if(cb->getHeroesInfo().size() >= ALLOWED_ROAMING_HEROES)
-		return false;
-	if(cb->getHeroesInfo().size() >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
-		return false;
-	if(!cb->getAvailableHeroes(t).size())
-		return false;
-
-	return true;
-}
-
-void VCAI::wander(HeroPtr h)
-{
-	auto visitTownIfAny = [this](HeroPtr h) -> bool
-	{
-		if (h->visitedTown)
-		{
-			townVisitsThisWeek[h].insert(h->visitedTown);
-			buildArmyIn(h->visitedTown);
-			return true;
-		}
-		return false;
-	};
-
-	//unclaim objects that are now dangerous for us
-	auto reservedObjsSetCopy = reservedHeroesMap[h];
-	for(auto obj : reservedObjsSetCopy)
-	{
-		if(!isSafeToVisit(h, obj->visitablePos()))
-			unreserveObject(h, obj);
-	}
-
-	TimeCheck tc("looking for wander destination");
-
-	for(int k = 0; k < 10 && h->movementPointsRemaining(); k++)
-	{
-		validateVisitableObjs();
-		ah->updatePaths(getMyHeroes());
-
-		std::vector<ObjectIdRef> dests;
-
-		//also visit our reserved objects - but they are not prioritized to avoid running back and forth
-		vstd::copy_if(reservedHeroesMap[h], std::back_inserter(dests), [&](ObjectIdRef obj) -> bool
-		{
-			return ah->isTileAccessible(h, obj->visitablePos());
-		});
-
-		int pass = 0;
-		std::vector<std::optional<float>> distanceLimits = {1.0, 2.0, std::nullopt};
-
-		while(!dests.size() && pass < distanceLimits.size())
-		{
-			auto & distanceLimit = distanceLimits[pass];
-
-			logAi->debug("Looking for wander destination pass=%i, cost limit=%f", pass, distanceLimit.value_or(-1.0));
-
-			vstd::copy_if(visitableObjs, std::back_inserter(dests), [&](ObjectIdRef obj) -> bool
-			{
-				return isGoodForVisit(obj, h, distanceLimit);
-			});
-
-			pass++;
-		}
-
-		if(!dests.size())
-		{
-			logAi->debug("Looking for town destination");
-
-			if(cb->getVisitableObjs(h->visitablePos()).size() > 1)
-				moveHeroToTile(h->visitablePos(), h); //just in case we're standing on blocked subterranean gate
-
-			auto compareReinforcements = [&](const CGTownInstance * lhs, const CGTownInstance * rhs) -> bool
-			{
-				const CGHeroInstance * hptr = h.get();
-				auto r1 = ah->howManyReinforcementsCanGet(hptr, lhs),
-					r2 = ah->howManyReinforcementsCanGet(hptr, rhs);
-				if (r1 != r2)
-					return r1 < r2;
-				else
-					return ah->howManyReinforcementsCanBuy(hptr, lhs) < ah->howManyReinforcementsCanBuy(hptr, rhs);
-			};
-
-			std::vector<const CGTownInstance *> townsReachable;
-			std::vector<const CGTownInstance *> townsNotReachable;
-			for(const CGTownInstance * t : cb->getTownsInfo())
-			{
-				if(!t->visitingHero && !vstd::contains(townVisitsThisWeek[h], t))
-				{
-					if(isAccessibleForHero(t->visitablePos(), h))
-						townsReachable.push_back(t);
-					else
-						townsNotReachable.push_back(t);
-				}
-			}
-			if(townsReachable.size()) //travel to town with largest garrison, or empty - better than nothing
-			{
-				dests.push_back(*boost::max_element(townsReachable, compareReinforcements));
-			}
-			else if(townsNotReachable.size())
-			{
-				//TODO pick the truly best
-				const CGTownInstance * t = *boost::max_element(townsNotReachable, compareReinforcements);
-				logAi->debug("%s can't reach any town, we'll try to make our way to %s at %s", h->getNameTranslated(), t->getNameTranslated(), t->visitablePos().toString());
-				int3 pos1 = h->pos;
-				striveToGoal(sptr(Goals::ClearWayTo(t->visitablePos()).sethero(h))); //TODO: drop "strive", add to mainLoop
-				//if out hero is stuck, we may need to request another hero to clear the way we see
-
-				if(pos1 == h->pos && h == primaryHero()) //hero can't move
-				{
-					if(canRecruitAnyHero(t))
-						recruitHero(t);
-				}
-				break;
-			}
-			else if(cb->getResourceAmount(EGameResID::GOLD) >= GameConstants::HERO_GOLD_COST)
-			{
-				std::vector<const CGTownInstance *> towns = cb->getTownsInfo();
-				vstd::erase_if(towns, [&](const CGTownInstance * t) -> bool
-				{
-					for(const CGHeroInstance * h : cb->getHeroesInfo())
-					{
-						if(!t->getArmyStrength() || ah->howManyReinforcementsCanGet(h, t))
-							return true;
-					}
-					return false;
-				});
-				if (towns.size())
-				{
-					recruitHero(*boost::max_element(towns, compareArmyStrength));
-				}
-				break;
-			}
-			else
-			{
-				logAi->debug("Nowhere more to go...");
-				break;
-			}
-		}
-		//end of objs empty
-
-		if(dests.size()) //performance improvement
-		{
-			Goals::TGoalVec targetObjectGoals;
-			for(auto destination : dests)
-			{
-				vstd::concatenate(targetObjectGoals, ah->howToVisitObj(h, destination, false));
-			}
-
-			if(targetObjectGoals.size())
-			{
-				auto bestObjectGoal = fh->chooseSolution(targetObjectGoals);
-
-				//wander should not cause heroes to be reserved - they are always considered free
-				if(bestObjectGoal->goalType == Goals::VISIT_OBJ)
-				{
-					auto chosenObject = cb->getObjInstance(ObjectInstanceID(bestObjectGoal->objid));
-					if(chosenObject != nullptr)
-						logAi->debug("Of all %d destinations, object %s at pos=%s seems nice", dests.size(), chosenObject->getObjectName(), chosenObject->pos.toString());
-				}
-				else
-					logAi->debug("Trying to realize goal of type %s as part of wandering.", bestObjectGoal->name());
-
-				try
-				{
-					decomposeGoal(bestObjectGoal)->accept(this);
-				}
-				catch(const goalFulfilledException & e)
-				{
-					if(e.goal->goalType == Goals::EGoals::VISIT_TILE || e.goal->goalType == Goals::EGoals::VISIT_OBJ)
-						continue;
-
-					throw e;
-				}
-			}
-			else
-			{
-				logAi->debug("Nowhere more to go...");
-				break;
-			}
-
-			visitTownIfAny(h);
-		}
-	}
-
-	visitTownIfAny(h); //in case hero is just sitting in town
-}
-
-void VCAI::setGoal(HeroPtr h, Goals::TSubgoal goal)
-{
-	if(goal->invalid())
-	{
-		vstd::erase_if_present(lockedHeroes, h);
-	}
-	else
-	{
-		lockedHeroes[h] = goal;
-		goal->setisElementar(false); //Force always evaluate goals before realizing
-	}
-}
-void VCAI::evaluateGoal(HeroPtr h)
-{
-	if(vstd::contains(lockedHeroes, h))
-		fh->setPriority(lockedHeroes[h]);
-}
-
-void VCAI::completeGoal(Goals::TSubgoal goal)
-{
-	if (goal->goalType == Goals::WIN) //we can never complete this goal - unless we already won
-		return;
-
-	logAi->debug("Completing goal: %s", goal->name());
-
-	//notify Managers
-	ah->notifyGoalCompleted(goal);
-	//notify mainLoop()
-	goalsToRemove.push_back(goal); //will be removed from mainLoop() goals
-	for (auto basicGoal : basicGoals) //we could luckily fulfill any of our goals
-	{
-		if (basicGoal->fulfillsMe(goal))
-			goalsToRemove.push_back(basicGoal);
-	}
-
-	//unreserve heroes
-	if(const CGHeroInstance * h = goal->hero.get(true))
-	{
-		auto it = lockedHeroes.find(h);
-		if(it != lockedHeroes.end())
-		{
-			if(it->second == goal || it->second->fulfillsMe(goal)) //FIXME this is overspecified, fulfillsMe shoudl be complete
-			{
-				logAi->debug(goal->completeMessage());
-				lockedHeroes.erase(it); //goal fulfilled, free hero
-			}
-		}
-	}
-	else //complete goal for all heroes maybe?
-	{
-		vstd::erase_if(lockedHeroes, [goal](std::pair<HeroPtr, Goals::TSubgoal> p)
-		{
-			if(p.second == goal || p.second->fulfillsMe(goal)) //we could have fulfilled goals of other heroes by chance
-			{
-				logAi->debug(p.second->completeMessage());
-				return true;
-			}
-			return false;
-		});
-	}
-
-}
-
-void VCAI::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed)
-{
-	NET_EVENT_HANDLER;
-	assert(!playerID.isValidPlayer() || status.getBattle() == UPCOMING_BATTLE);
-	status.setBattle(ONGOING_BATTLE);
-	const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit
-	battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString());
-	CAdventureAI::battleStart(battleID, army1, army2, tile, hero1, hero2, side, replayAllowed);
-}
-
-void VCAI::battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID)
-{
-	NET_EVENT_HANDLER;
-	assert(status.getBattle() == ONGOING_BATTLE);
-	status.setBattle(ENDING_BATTLE);
-	bool won = br->winner == myCb->getBattle(battleID)->battleGetMySide();
-	logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.toString(), (won ? "won" : "lost"), battlename);
-	battlename.clear();
-
-	if (queryID != QueryID::NONE)
-	{
-		status.addQuery(queryID, "Combat result dialog");
-		const int confirmAction = 0;
-		requestActionASAP([=]()
-		{
-			answerQuery(queryID, confirmAction);
-		});
-	}
-	CAdventureAI::battleEnd(battleID, br, queryID);
-}
-
-void VCAI::waitTillFree()
-{
-	auto unlock = vstd::makeUnlockSharedGuard(CGameState::mutex);
-	status.waitTillFree();
-}
-
-void VCAI::markObjectVisited(const CGObjectInstance * obj)
-{
-	if(!obj)
-		return;
-
-	if(const auto * rewardable = dynamic_cast<const CRewardableObject *>(obj)) //we may want to visit it with another hero
-	{
-		if (rewardable->configuration.getVisitMode() == Rewardable::VISIT_HERO) //we may want to visit it with another hero
-			return;
-
-		if (rewardable->configuration.getVisitMode() == Rewardable::VISIT_BONUS) //or another time
-			return;
-	}
-
-	if(obj->ID == Obj::MONSTER)
-		return;
-
-	alreadyVisited.insert(obj);
-}
-
-void VCAI::reserveObject(HeroPtr h, const CGObjectInstance * obj)
-{
-	reservedObjs.insert(obj);
-	reservedHeroesMap[h].insert(obj);
-	logAi->debug("reserved object id=%d; address=%p; name=%s", obj->id, obj, obj->getObjectName());
-}
-
-void VCAI::unreserveObject(HeroPtr h, const CGObjectInstance * obj)
-{
-	vstd::erase_if_present(reservedObjs, obj); //unreserve objects
-	vstd::erase_if_present(reservedHeroesMap[h], obj);
-}
-
-void VCAI::markHeroUnableToExplore(HeroPtr h)
-{
-	heroesUnableToExplore.insert(h);
-}
-void VCAI::markHeroAbleToExplore(HeroPtr h)
-{
-	vstd::erase_if_present(heroesUnableToExplore, h);
-}
-bool VCAI::isAbleToExplore(HeroPtr h)
-{
-	return !vstd::contains(heroesUnableToExplore, h);
-}
-void VCAI::clearPathsInfo()
-{
-	heroesUnableToExplore.clear();
-}
-
-void VCAI::validateVisitableObjs()
-{
-	std::string errorMsg;
-	auto shouldBeErased = [&](const CGObjectInstance * obj) -> bool
-	{
-		if(obj)
-			return !cb->getObj(obj->id, false); // no verbose output needed as we check object visibility
-		else
-			return true;
-	};
-
-	//errorMsg is captured by ref so lambda will take the new text
-	errorMsg = " shouldn't be on the visitable objects list!";
-	vstd::erase_if(visitableObjs, shouldBeErased);
-
-	//FIXME: how comes our own heroes become inaccessible?
-	vstd::erase_if(reservedHeroesMap, [](std::pair<HeroPtr, std::set<const CGObjectInstance *>> hp) -> bool
-	{
-		return !hp.first.get(true);
-	});
-	for(auto & p : reservedHeroesMap)
-	{
-		errorMsg = " shouldn't be on list for hero " + p.first->getNameTranslated() + "!";
-		vstd::erase_if(p.second, shouldBeErased);
-	}
-
-	errorMsg = " shouldn't be on the reserved objs list!";
-	vstd::erase_if(reservedObjs, shouldBeErased);
-
-	//TODO overkill, hidden object should not be removed. However, we can't know if hidden object is erased from game.
-	errorMsg = " shouldn't be on the already visited objs list!";
-	vstd::erase_if(alreadyVisited, shouldBeErased);
-}
-
-void VCAI::retrieveVisitableObjs(std::vector<const CGObjectInstance *> & out, bool includeOwned) const
-{
-	foreach_tile_pos([&](const int3 & pos)
-	{
-		for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false))
-		{
-			if(includeOwned || obj->tempOwner != playerID)
-				out.push_back(obj);
-		}
-	});
-}
-
-void VCAI::retrieveVisitableObjs()
-{
-	foreach_tile_pos([&](const int3 & pos)
-	{
-		for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false))
-		{
-			if(obj->tempOwner != playerID)
-				addVisitableObj(obj);
-		}
-	});
-}
-
-std::vector<const CGObjectInstance *> VCAI::getFlaggedObjects() const
-{
-	std::vector<const CGObjectInstance *> ret;
-	for(const CGObjectInstance * obj : visitableObjs)
-	{
-		if(obj->tempOwner == playerID)
-			ret.push_back(obj);
-	}
-	return ret;
-}
-
-void VCAI::addVisitableObj(const CGObjectInstance * obj)
-{
-	if(obj->ID == Obj::EVENT)
-		return;
-
-	visitableObjs.insert(obj);
-
-	// All teleport objects seen automatically assigned to appropriate channels
-	auto teleportObj = dynamic_cast<const CGTeleport *>(obj);
-	if(teleportObj)
-		CGTeleport::addToChannel(knownTeleportChannels, teleportObj);
-}
-
-const CGObjectInstance * VCAI::lookForArt(int aid) const
-{
-	for(const CGObjectInstance * obj : ai->visitableObjs)
-	{
-		if(obj->ID == Obj::ARTIFACT && obj->subID == aid)
-			return obj;
-	}
-
-	return nullptr;
-
-	//TODO what if more than one artifact is available? return them all or some slection criteria
-}
-
-bool VCAI::isAccessible(const int3 & pos) const
-{
-	//TODO precalculate for speed
-
-	for(const CGHeroInstance * h : cb->getHeroesInfo())
-	{
-		if(isAccessibleForHero(pos, h))
-			return true;
-	}
-
-	return false;
-}
-
-HeroPtr VCAI::getHeroWithGrail() const
-{
-	for(const CGHeroInstance * h : cb->getHeroesInfo())
-	{
-		if(h->hasArt(ArtifactID::GRAIL))
-			return h;
-	}
-	return nullptr;
-}
-
-const CGObjectInstance * VCAI::getUnvisitedObj(const std::function<bool(const CGObjectInstance *)> & predicate)
-{
-	//TODO smarter definition of unvisited
-	for(const CGObjectInstance * obj : visitableObjs)
-	{
-		if(predicate(obj) && !vstd::contains(alreadyVisited, obj))
-			return obj;
-	}
-	return nullptr;
-}
-
-bool VCAI::isAccessibleForHero(const int3 & pos, HeroPtr h, bool includeAllies) const
-{
-	// Don't visit tile occupied by allied hero
-	if(!includeAllies)
-	{
-		for(auto obj : cb->getVisitableObjs(pos))
-		{
-			if(obj->ID == Obj::HERO && cb->getPlayerRelations(ai->playerID, obj->tempOwner) != PlayerRelations::ENEMIES)
-			{
-				if(obj != h.get())
-					return false;
-			}
-		}
-	}
-	return cb->getPathsInfo(h.get())->getPathInfo(pos)->reachable();
-}
-
-bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
-{
-	//TODO: consider if blockVisit objects change something in our checks: AIUtility::isBlockVisitObj()
-
-	auto afterMovementCheck = [&]() -> void
-	{
-		waitTillFree(); //movement may cause battle or blocking dialog
-		if(!h)
-		{
-			lostHero(h);
-			teleportChannelProbingList.clear();
-			if(status.channelProbing()) // if hero lost during channel probing we need to switch this mode off
-				status.setChannelProbing(false);
-			throw cannotFulfillGoalException("Hero was lost!");
-		}
-	};
-
-	logAi->debug("Moving hero %s to tile %s", h->getNameTranslated(), dst.toString());
-	int3 startHpos = h->visitablePos();
-	bool ret = false;
-	if(startHpos == dst)
-	{
-		//FIXME: this assertion fails also if AI moves onto defeated guarded object
-		assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object
-		cb->moveHero(*h, h->convertFromVisitablePos(dst));
-		afterMovementCheck(); // TODO: is it feasible to hero get killed there if game work properly?
-		// If revisiting, teleport probing is never done, and so the entries into the list would remain unused and uncleared
-		teleportChannelProbingList.clear();
-		// not sure if AI can currently reconsider to attack bank while staying on it. Check issue 2084 on mantis for more information.
-		ret = true;
-	}
-	else
-	{
-		CGPath path;
-		cb->getPathsInfo(h.get())->getPath(path, dst);
-		if(path.nodes.empty())
-		{
-			logAi->error("Hero %s cannot reach %s.", h->getNameTranslated(), dst.toString());
-			throw goalFulfilledException(sptr(Goals::VisitTile(dst).sethero(h)));
-		}
-		int i = (int)path.nodes.size() - 1;
-
-		auto getObj = [&](int3 coord, bool ignoreHero)
-		{
-			auto tile = cb->getTile(coord, false);
-			assert(tile);
-			return tile->topVisitableObj(ignoreHero);
-			//return cb->getTile(coord,false)->topVisitableObj(ignoreHero);
-		};
-
-		auto isTeleportAction = [&](EPathNodeAction action) -> bool
-		{
-			if(action != EPathNodeAction::TELEPORT_NORMAL && action != EPathNodeAction::TELEPORT_BLOCKING_VISIT)
-			{
-				if(action != EPathNodeAction::TELEPORT_BATTLE)
-				{
-					return false;
-				}
-			}
-
-			return true;
-		};
-
-		auto getDestTeleportObj = [&](const CGObjectInstance * currentObject, const CGObjectInstance * nextObjectTop, const CGObjectInstance * nextObject) -> const CGObjectInstance *
-		{
-			if(CGTeleport::isConnected(currentObject, nextObjectTop))
-				return nextObjectTop;
-			if(nextObjectTop && nextObjectTop->ID == Obj::HERO)
-			{
-				if(CGTeleport::isConnected(currentObject, nextObject))
-					return nextObject;
-			}
-
-			return nullptr;
-		};
-
-		auto doMovement = [&](int3 dst, bool transit)
-		{
-			cb->moveHero(*h, h->convertFromVisitablePos(dst), transit);
-		};
-
-		auto doTeleportMovement = [&](ObjectInstanceID exitId, int3 exitPos)
-		{
-			destinationTeleport = exitId;
-			if(exitPos.valid())
-				destinationTeleportPos = h->convertFromVisitablePos(exitPos);
-			cb->moveHero(*h, h->pos);
-			destinationTeleport = ObjectInstanceID();
-			destinationTeleportPos = int3(-1);
-			afterMovementCheck();
-		};
-
-		auto doChannelProbing = [&]() -> void
-		{
-			auto currentPos = h->visitablePos();
-			auto currentExit = getObj(currentPos, true)->id;
-
-			status.setChannelProbing(true);
-			for(auto exit : teleportChannelProbingList)
-				doTeleportMovement(exit, int3(-1));
-			teleportChannelProbingList.clear();
-			status.setChannelProbing(false);
-
-			doTeleportMovement(currentExit, currentPos);
-		};
-
-		for(; i > 0; i--)
-		{
-			int3 currentCoord = path.nodes[i].coord;
-			int3 nextCoord = path.nodes[i - 1].coord;
-
-			auto currentObject = getObj(currentCoord, currentCoord == h->visitablePos());
-			auto nextObjectTop = getObj(nextCoord, false);
-			auto nextObject = getObj(nextCoord, true);
-			auto destTeleportObj = getDestTeleportObj(currentObject, nextObjectTop, nextObject);
-			if(isTeleportAction(path.nodes[i - 1].action) && destTeleportObj != nullptr)
-			{
-				//we use special login if hero standing on teleporter it's mean we need
-				doTeleportMovement(destTeleportObj->id, nextCoord);
-				if(teleportChannelProbingList.size())
-					doChannelProbing();
-				markObjectVisited(destTeleportObj); //FIXME: Monoliths are not correctly visited
-
-				continue;
-			}
-
-			//stop sending move requests if the next node can't be reached at the current turn (hero exhausted his move points)
-			if(path.nodes[i - 1].turns)
-			{
-				//blockedHeroes.insert(h); //to avoid attempts of moving heroes with very little MPs
-				break;
-			}
-
-			int3 endpos = path.nodes[i - 1].coord;
-			if(endpos == h->visitablePos())
-				continue;
-
-			bool isConnected = false;
-			bool isNextObjectTeleport = false;
-			// Check there is node after next one; otherwise transit is pointless
-			if(i - 2 >= 0)
-			{
-				isConnected = CGTeleport::isConnected(nextObjectTop, getObj(path.nodes[i - 2].coord, false));
-				isNextObjectTeleport = CGTeleport::isTeleport(nextObjectTop);
-			}
-			if(isConnected || isNextObjectTeleport)
-			{
-				// Hero should be able to go through object if it's allow transit
-				doMovement(endpos, true);
-			}
-			else if(path.nodes[i - 1].layer == EPathfindingLayer::AIR)
-			{
-				doMovement(endpos, true);
-			}
-			else
-			{
-				doMovement(endpos, false);
-			}
-
-			afterMovementCheck();
-
-			if(teleportChannelProbingList.size())
-				doChannelProbing();
-		}
-
-		if(path.nodes[0].action == EPathNodeAction::BLOCKING_VISIT)
-		{
-			ret = h && i == 0; // when we take resource we do not reach its position. We even might not move
-		}
-	}
-	if(h)
-	{
-		if(auto visitedObject = vstd::frontOrNull(cb->getVisitableObjs(h->visitablePos()))) //we stand on something interesting
-		{
-			if(visitedObject != *h)
-				performObjectInteraction(visitedObject, h);
-		}
-	}
-	if(h) //we could have lost hero after last move
-	{
-		completeGoal(sptr(Goals::VisitTile(dst).sethero(h))); //we stepped on some tile, anyway
-		completeGoal(sptr(Goals::ClearWayTo(dst).sethero(h)));
-
-		ret = ret || (dst == h->visitablePos());
-
-		if(!ret) //reserve object we are heading towards
-		{
-			auto obj = vstd::frontOrNull(cb->getVisitableObjs(dst));
-			if(obj && obj != *h)
-				reserveObject(h, obj);
-		}
-
-		if(startHpos == h->visitablePos() && !ret) //we didn't move and didn't reach the target
-		{
-			vstd::erase_if_present(lockedHeroes, h); //hero seemingly is confused or has only 95mp which is not enough to move
-			invalidPathHeroes.insert(h);
-			throw cannotFulfillGoalException("Invalid path found!");
-		}
-		evaluateGoal(h); //new hero position means new game situation
-		logAi->debug("Hero %s moved from %s to %s. Returning %d.", h->getNameTranslated(), startHpos.toString(), h->visitablePos().toString(), ret);
-	}
-	return ret;
-}
-
-void VCAI::buildStructure(const CGTownInstance * t, BuildingID building)
-{
-	auto name = t->town->buildings.at(building)->getNameTranslated();
-	logAi->debug("Player %d will build %s in town of %s at %s", ai->playerID, name, t->getNameTranslated(), t->pos.toString());
-	cb->buildBuilding(t, building); //just do this;
-}
-
-void VCAI::tryRealize(Goals::Explore & g)
-{
-	throw cannotFulfillGoalException("EXPLORE is not an elementar goal!");
-}
-
-void VCAI::tryRealize(Goals::RecruitHero & g)
-{
-	if(const CGTownInstance * t = findTownWithTavern())
-	{
-		recruitHero(t, true);
-		//TODO try to free way to blocked town
-		//TODO: adventure map tavern or prison?
-	}
-	else
-	{
-		throw cannotFulfillGoalException("No town to recruit hero!");
-	}
-}
-
-void VCAI::tryRealize(Goals::VisitTile & g)
-{
-	if(!g.hero->movementPointsRemaining())
-		throw cannotFulfillGoalException("Cannot visit tile: hero is out of MPs!");
-	if(g.tile == g.hero->visitablePos() && cb->getVisitableObjs(g.hero->visitablePos()).size() < 2)
-	{
-		logAi->warn("Why do I want to move hero %s to tile %s? Already standing on that tile! ", g.hero->getNameTranslated(), g.tile.toString());
-		throw goalFulfilledException(sptr(g));
-	}
-	if(ai->moveHeroToTile(g.tile, g.hero.get()))
-	{
-		throw goalFulfilledException(sptr(g));
-	}
-}
-
-void VCAI::tryRealize(Goals::VisitObj & g)
-{
-	auto position = g.tile;
-	if(!g.hero->movementPointsRemaining())
-		throw cannotFulfillGoalException("Cannot visit object: hero is out of MPs!");
-	if(position == g.hero->visitablePos() && cb->getVisitableObjs(g.hero->visitablePos()).size() < 2)
-	{
-		logAi->warn("Why do I want to move hero %s to tile %s? Already standing on that tile! ", g.hero->getNameTranslated(), g.tile.toString());
-		throw goalFulfilledException(sptr(g));
-	}
-	if(ai->moveHeroToTile(position, g.hero.get()))
-	{
-		throw goalFulfilledException(sptr(g));
-	}
-}
-
-void VCAI::tryRealize(Goals::VisitHero & g)
-{
-	if(!g.hero->movementPointsRemaining())
-		throw cannotFulfillGoalException("Cannot visit target hero: hero is out of MPs!");
-
-	const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid));
-	if(obj)
-	{
-		if(ai->moveHeroToTile(obj->visitablePos(), g.hero.get()))
-		{
-			throw goalFulfilledException(sptr(g));
-		}
-	}
-	else
-	{
-		throw cannotFulfillGoalException("Cannot visit hero: object not found!");
-	}
-}
-
-void VCAI::tryRealize(Goals::BuildThis & g)
-{
-	auto b = BuildingID(g.bid);
-	auto t = g.town;
-
-	if (t)
-	{
-		if (cb->canBuildStructure(t, b) == EBuildingState::ALLOWED)
-		{
-			logAi->debug("Player %d will build %s in town of %s at %s",
-				playerID, t->town->buildings.at(b)->getNameTranslated(), t->getNameTranslated(), t->pos.toString());
-			cb->buildBuilding(t, b);
-			throw goalFulfilledException(sptr(g));
-		}
-	}
-	throw cannotFulfillGoalException("Cannot build a given structure!");
-}
-
-void VCAI::tryRealize(Goals::DigAtTile & g)
-{
-	assert(g.hero->visitablePos() == g.tile); //surely we want to crash here?
-	if(g.hero->diggingStatus() == EDiggingStatus::CAN_DIG)
-	{
-		cb->dig(g.hero.get());
-		completeGoal(sptr(g)); // finished digging
-	}
-	else
-	{
-		ai->lockedHeroes[g.hero] = sptr(g); //hero who tries to dig shouldn't do anything else
-		throw cannotFulfillGoalException("A hero can't dig!\n");
-	}
-}
-
-void VCAI::tryRealize(Goals::Trade & g) //trade
-{
-	if(ah->freeResources()[g.resID] >= g.value) //goal is already fulfilled. Why we need this check, anyway?
-		throw goalFulfilledException(sptr(g));
-
-	int accquiredResources = 0;
-	if(const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid), false))
-	{
-		if(const IMarket * m = IMarket::castFrom(obj, false))
-		{
-			auto freeRes = ah->freeResources(); //trade only resources which are not reserved
-			for(auto it = ResourceSet::nziterator(freeRes); it.valid(); it++)
-			{
-				auto res = it->resType;
-				if(res.getNum() == g.resID) //sell any other resource
-					continue;
-
-				int toGive, toGet;
-				m->getOffer(res, g.resID, toGive, toGet, EMarketMode::RESOURCE_RESOURCE);
-				toGive = static_cast<int>(toGive * (it->resVal / toGive)); //round down
-				//TODO trade only as much as needed
-				if (toGive) //don't try to sell 0 resources
-				{
-					cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, g.resID, toGive);
-					accquiredResources = static_cast<int>(toGet * (it->resVal / toGive));
-					logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, accquiredResources, g.resID, obj->getObjectName());
-				}
-				if (ah->freeResources()[g.resID] >= g.value)
-					throw goalFulfilledException(sptr(g)); //we traded all we needed
-			}
-
-			throw cannotFulfillGoalException("I cannot get needed resources by trade!");
-		}
-		else
-		{
-			throw cannotFulfillGoalException("I don't know how to use this object to raise resources!");
-		}
-	}
-	else
-	{
-		throw cannotFulfillGoalException("No object that could be used to raise resources!");
-	}
-}
-
-void VCAI::tryRealize(Goals::BuyArmy & g)
-{
-	auto t = g.town;
-
-	ui64 valueBought = 0;
-	//buy the stacks with largest AI value
-
-	makePossibleUpgrades(t);
-
-	while (valueBought < g.value)
-	{
-		auto res = ah->allResources();
-		std::vector<creInfo> creaturesInDwellings;
-
-		for (int i = 0; i < t->creatures.size(); i++)
-		{
-			auto ci = infoFromDC(t->creatures[i]);
-
-			if(!ci.count
-				|| ci.creID == CreatureID::NONE
-				|| (g.objid != -1 && ci.creID.getNum() != g.objid)
-				|| t->getUpperArmy()->getSlotFor(ci.creID) == SlotID())
-				continue;
-
-			vstd::amin(ci.count, res / ci.cre->getFullRecruitCost()); //max count we can afford
-
-			if(!ci.count)
-				continue;
-
-			ci.level = i; //this is important for Dungeon Summoning Portal
-			creaturesInDwellings.push_back(ci);
-		}
-
-		if (creaturesInDwellings.empty())
-			throw cannotFulfillGoalException("Can't buy any more creatures!");
-
-		creInfo ci =
-			*boost::max_element(creaturesInDwellings, [](const creInfo & lhs, const creInfo & rhs)
-		{
-			//max value of creatures we can buy with our res
-			int value1 = lhs.cre->getAIValue() * lhs.count,
-				value2 = rhs.cre->getAIValue() * rhs.count;
-
-			return value1 < value2;
-		});
-
-
-		cb->recruitCreatures(t, t->getUpperArmy(), ci.creID, ci.count, ci.level);
-		valueBought += ci.count * ci.cre->getAIValue();
-	}
-
-	throw goalFulfilledException(sptr(g)); //we bought as many creatures as we wanted
-}
-
-void VCAI::tryRealize(Goals::Invalid & g)
-{
-	throw cannotFulfillGoalException("I don't know how to fulfill this!");
-}
-
-void VCAI::tryRealize(Goals::AbstractGoal & g)
-{
-	logAi->debug("Attempting realizing goal with code %s", g.name());
-	throw cannotFulfillGoalException("Unknown type of goal !");
-}
-
-const CGTownInstance * VCAI::findTownWithTavern() const
-{
-	for(const CGTownInstance * t : cb->getTownsInfo())
-		if(t->hasBuilt(BuildingID::TAVERN) && !t->visitingHero)
-			return t;
-
-	return nullptr;
-}
-
-Goals::TSubgoal VCAI::getGoal(HeroPtr h) const
-{
-	auto it = lockedHeroes.find(h);
-	if(it != lockedHeroes.end())
-		return it->second;
-	else
-		return sptr(Goals::Invalid());
-}
-
-
-std::vector<HeroPtr> VCAI::getUnblockedHeroes() const
-{
-	std::vector<HeroPtr> ret;
-	for(auto h : cb->getHeroesInfo())
-	{
-		//&& !vstd::contains(lockedHeroes, h)
-		//at this point we assume heroes exhausted their locked goals
-		if(canAct(h))
-			ret.push_back(h);
-	}
-	return ret;
-}
-
-bool VCAI::canAct(HeroPtr h) const
-{
-	auto mission = lockedHeroes.find(h);
-	if(mission != lockedHeroes.end())
-	{
-		//FIXME: I'm afraid there can be other conditions when heroes can act but not move :?
-		if(mission->second->goalType == Goals::DIG_AT_TILE && !mission->second->isElementar)
-			return false;
-	}
-
-	return h->movementPointsRemaining();
-}
-
-HeroPtr VCAI::primaryHero() const
-{
-	auto hs = cb->getHeroesInfo();
-	if (hs.empty())
-		return nullptr;
-	else
-		return *boost::max_element(hs, compareHeroStrength);
-}
-
-void VCAI::endTurn()
-{
-	logAi->info("Player %d (%s) ends turn", playerID, playerID.toString());
-	if(!status.haveTurn())
-	{
-		logAi->error("Not having turn at the end of turn???");
-	}
-	logAi->debug("Resources at the end of turn: %s", cb->getResourceAmount().toString());
-	do
-	{
-		cb->endTurn();
-	}
-	while(status.haveTurn()); //for some reasons, our request may fail -> stop requesting end of turn only after we've received a confirmation that it's over
-
-	logGlobal->info("Player %d (%s) ended turn", playerID, playerID.toString());
-}
-
-void VCAI::striveToGoal(Goals::TSubgoal basicGoal)
-{
-	//TODO: this function is deprecated and should be dropped altogether
-
-	auto goalToDecompose = basicGoal;
-	Goals::TSubgoal elementarGoal = sptr(Goals::Invalid());
-	int maxAbstractGoals = 10;
-	while (!elementarGoal->isElementar && maxAbstractGoals)
-	{
-		try
-		{
-			elementarGoal = decomposeGoal(goalToDecompose);
-		}
-		catch (goalFulfilledException & e)
-		{
-			//it is impossible to continue some goals (like exploration, for example)
-			completeGoal(e.goal); //put in goalsToRemove
-			logAi->debug("Goal %s decomposition failed: goal was completed as much as possible", e.goal->name());
-			return;
-		}
-		catch (std::exception & e)
-		{
-			goalsToRemove.push_back(basicGoal);
-			logAi->debug("Goal %s decomposition failed: %s", basicGoal->name(), e.what());
-			return;
-		}
-		if (elementarGoal->isAbstract) //we can decompose it further
-		{
-			goalsToAdd.push_back(elementarGoal);
-			//decompose further now - this is necesssary if we can't add over 10 goals in the pool
-			goalToDecompose = elementarGoal;
-			//there is a risk of infinite abstract goal loop, though it indicates failed logic
-			maxAbstractGoals--;
-		}
-		else if (elementarGoal->isElementar) //should be
-		{
-			logAi->debug("Found elementar goal %s", elementarGoal->name());
-			ultimateGoalsFromBasic[elementarGoal].push_back(goalToDecompose); //TODO: how about indirect basicGoal?
-			break;
-		}
-		else //should never be here
-			throw cannotFulfillGoalException("Goal %s is neither abstract nor elementar!" + basicGoal->name());
-	}
-
-	//realize best goal
-	if (!elementarGoal->invalid())
-	{
-		logAi->debug("Trying to realize %s (value %2.3f)", elementarGoal->name(), elementarGoal->priority);
-
-		try
-		{
-			boost::this_thread::interruption_point();
-			elementarGoal->accept(this); //visitor pattern
-			boost::this_thread::interruption_point();
-		}
-		catch (boost::thread_interrupted & e)
-		{
-			(void)e;
-			logAi->debug("Player %d: Making turn thread received an interruption!", playerID);
-			throw; //rethrow, we want to truly end this thread
-		}
-		catch (goalFulfilledException & e)
-		{
-			//the sub-goal was completed successfully
-			completeGoal(e.goal);
-			//local goal was also completed
-			completeGoal(elementarGoal);
-		}
-		catch (std::exception & e)
-		{
-			logAi->debug("Failed to realize subgoal of type %s, I will stop.", elementarGoal->name());
-			logAi->debug("The error message was: %s", e.what());
-
-			//erase base goal if we failed to execute decomposed goal
-			for (auto basicGoalToRemove : ultimateGoalsFromBasic[elementarGoal])
-				goalsToRemove.push_back(basicGoalToRemove);
-		}
-	}
-}
-
-Goals::TSubgoal VCAI::decomposeGoal(Goals::TSubgoal ultimateGoal)
-{
-	if(ultimateGoal->isElementar)
-	{
-		logAi->warn("Trying to decompose elementar goal %s", ultimateGoal->name());
-
-		return ultimateGoal;
-	}
-
-	const int searchDepth = 30;
-
-	Goals::TSubgoal goal = ultimateGoal;
-	logAi->debug("Decomposing goal %s", ultimateGoal->name());
-	int maxGoals = searchDepth; //preventing deadlock for mutually dependent goals
-	while (maxGoals)
-	{
-		boost::this_thread::interruption_point();
-
-		goal = goal->whatToDoToAchieve(); //may throw if decomposition fails
-		--maxGoals;
-		if (goal == ultimateGoal) //compare objects by value
-			if (goal->isElementar == ultimateGoal->isElementar)
-				throw cannotFulfillGoalException((boost::format("Goal dependency loop detected for %s!")
-												% ultimateGoal->name()).str());
-		if (goal->isAbstract || goal->isElementar)
-			return goal;
-		else
-			logAi->debug("Considering: %s", goal->name());
-	}
-
-	throw cannotFulfillGoalException("Too many subgoals, don't know what to do");
-}
-
-void VCAI::performTypicalActions()
-{
-	for(auto h : getUnblockedHeroes())
-	{
-		if(!h) //hero might be lost. getUnblockedHeroes() called once on start of turn
-			continue;
-
-		logAi->debug("Hero %s started wandering, MP=%d", h->getNameTranslated(), h->movementPointsRemaining());
-		makePossibleUpgrades(*h);
-		pickBestArtifacts(*h);
-		try
-		{
-			wander(h);
-		}
-		catch(std::exception & e)
-		{
-			logAi->debug("Cannot use this hero anymore, received exception: %s", e.what());
-			continue;
-		}
-	}
-}
-
-void VCAI::buildArmyIn(const CGTownInstance * t)
-{
-	makePossibleUpgrades(t->visitingHero);
-	makePossibleUpgrades(t);
-	recruitCreatures(t, t->getUpperArmy());
-	moveCreaturesToHero(t);
-}
-
-void VCAI::checkHeroArmy(HeroPtr h)
-{
-	auto it = lockedHeroes.find(h);
-	if(it != lockedHeroes.end())
-	{
-		if(it->second->goalType == Goals::GATHER_ARMY && it->second->value <= h->getArmyStrength())
-			completeGoal(sptr(Goals::GatherArmy(it->second->value).sethero(h)));
-	}
-}
-
-void VCAI::recruitHero(const CGTownInstance * t, bool throwing)
-{
-	logAi->debug("Trying to recruit a hero in %s at %s", t->getNameTranslated(), t->visitablePos().toString());
-
-	auto heroes = cb->getAvailableHeroes(t);
-	if(heroes.size())
-	{
-		auto hero = heroes[0];
-		if(heroes.size() >= 2) //makes sense to recruit two heroes with starting amries in first week
-		{
-			if(heroes[1]->getTotalStrength() > hero->getTotalStrength())
-				hero = heroes[1];
-		}
-		cb->recruitHero(t, hero);
-		throw goalFulfilledException(sptr(Goals::RecruitHero().settown(t)));
-	}
-	else if(throwing)
-	{
-		throw cannotFulfillGoalException("No available heroes in tavern in " + t->nodeName());
-	}
-}
-
-void VCAI::finish()
-{
-	//we want to lock to avoid multiple threads from calling makingTurn->join() at same time
-	boost::lock_guard<boost::mutex> multipleCleanupGuard(turnInterruptionMutex);
-	if(makingTurn)
-	{
-		makingTurn->interrupt();
-		makingTurn->join();
-		makingTurn.reset();
-	}
-}
-
-void VCAI::requestActionASAP(std::function<void()> whatToDo)
-{
-	boost::thread newThread([this, whatToDo]()
-	{
-		setThreadName("VCAI::requestActionASAP::whatToDo");
-		SET_GLOBAL_STATE(this);
-		boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
-		whatToDo();
-	});
-
-	newThread.detach();
-}
-
-void VCAI::lostHero(HeroPtr h)
-{
-	logAi->debug("I lost my hero %s. It's best to forget and move on.", h.name);
-
-	vstd::erase_if_present(lockedHeroes, h);
-	for(auto obj : reservedHeroesMap[h])
-	{
-		vstd::erase_if_present(reservedObjs, obj); //unreserve all objects for that hero
-	}
-	vstd::erase_if_present(reservedHeroesMap, h);
-	vstd::erase_if_present(visitedHeroes, h);
-	for (auto heroVec : visitedHeroes)
-	{
-		vstd::erase_if_present(heroVec.second, h);
-	}
-
-	//remove goals with removed hero assigned from main loop
-	vstd::erase_if(ultimateGoalsFromBasic, [&](const std::pair<Goals::TSubgoal, Goals::TGoalVec> & x) -> bool
-	{
-		if(x.first->hero == h)
-			return true;
-		else
-			return false;
-	});
-
-	auto removedHeroGoalPredicate = [&](const Goals::TSubgoal & x) ->bool
-	{
-		if(x->hero == h)
-			return true;
-		else
-			return false;
-	};
-
-	vstd::erase_if(basicGoals, removedHeroGoalPredicate);
-	vstd::erase_if(goalsToAdd, removedHeroGoalPredicate);
-	vstd::erase_if(goalsToRemove, removedHeroGoalPredicate);
-
-	for(auto goal : ultimateGoalsFromBasic)
-		vstd::erase_if(goal.second, removedHeroGoalPredicate);
-}
-
-void VCAI::answerQuery(QueryID queryID, int selection)
-{
-	logAi->debug("I'll answer the query %d giving the choice %d", queryID, selection);
-	if(queryID != QueryID(-1))
-	{
-		cb->selectionMade(selection, queryID);
-	}
-	else
-	{
-		logAi->debug("Since the query ID is %d, the answer won't be sent. This is not a real query!", queryID);
-		//do nothing
-	}
-}
-
-void VCAI::requestSent(const CPackForServer * pack, int requestID)
-{
-	//BNLOG("I have sent request of type %s", typeid(*pack).name());
-	if(auto reply = dynamic_cast<const QueryReply *>(pack))
-	{
-		status.attemptedAnsweringQuery(reply->qid, requestID);
-	}
-}
-
-std::string VCAI::getBattleAIName() const
-{
-	if(settings["server"]["enemyAI"].getType() == JsonNode::JsonType::DATA_STRING)
-		return settings["server"]["enemyAI"].String();
-	else
-		return "BattleAI";
-}
-
-void VCAI::validateObject(const CGObjectInstance * obj)
-{
-	validateObject(obj->id);
-}
-
-void VCAI::validateObject(ObjectIdRef obj)
-{
-	auto matchesId = [&](const CGObjectInstance * hlpObj) -> bool
-	{
-		return hlpObj->id == obj.id;
-	};
-	if(!obj)
-	{
-		vstd::erase_if(visitableObjs, matchesId);
-
-		for(auto & p : reservedHeroesMap)
-			vstd::erase_if(p.second, matchesId);
-
-		vstd::erase_if(reservedObjs, matchesId);
-	}
-}
-
-AIStatus::AIStatus()
-{
-	battle = NO_BATTLE;
-	havingTurn = false;
-	ongoingHeroMovement = false;
-	ongoingChannelProbing = false;
-}
-
-AIStatus::~AIStatus()
-{
-
-}
-
-void AIStatus::setBattle(BattleState BS)
-{
-	boost::unique_lock<boost::mutex> lock(mx);
-	LOG_TRACE_PARAMS(logAi, "battle state=%d", (int)BS);
-	battle = BS;
-	cv.notify_all();
-}
-
-BattleState AIStatus::getBattle()
-{
-	boost::unique_lock<boost::mutex> lock(mx);
-	return battle;
-}
-
-void AIStatus::addQuery(QueryID ID, std::string description)
-{
-	if(ID == QueryID(-1))
-	{
-		logAi->debug("The \"query\" has an id %d, it'll be ignored as non-query. Description: %s", ID, description);
-		return;
-	}
-
-	assert(ID.getNum() >= 0);
-	boost::unique_lock<boost::mutex> lock(mx);
-
-	assert(!vstd::contains(remainingQueries, ID));
-
-	remainingQueries[ID] = description;
-
-	cv.notify_all();
-	logAi->debug("Adding query %d - %s. Total queries count: %d", ID, description, remainingQueries.size());
-}
-
-void AIStatus::removeQuery(QueryID ID)
-{
-	boost::unique_lock<boost::mutex> lock(mx);
-	assert(vstd::contains(remainingQueries, ID));
-
-	std::string description = remainingQueries[ID];
-	remainingQueries.erase(ID);
-
-	cv.notify_all();
-	logAi->debug("Removing query %d - %s. Total queries count: %d", ID, description, remainingQueries.size());
-}
-
-int AIStatus::getQueriesCount()
-{
-	boost::unique_lock<boost::mutex> lock(mx);
-	return static_cast<int>(remainingQueries.size());
-}
-
-void AIStatus::startedTurn()
-{
-	boost::unique_lock<boost::mutex> lock(mx);
-	havingTurn = true;
-	cv.notify_all();
-}
-
-void AIStatus::madeTurn()
-{
-	boost::unique_lock<boost::mutex> lock(mx);
-	havingTurn = false;
-	cv.notify_all();
-}
-
-void AIStatus::waitTillFree()
-{
-	boost::unique_lock<boost::mutex> lock(mx);
-	while(battle != NO_BATTLE || !remainingQueries.empty() || !objectsBeingVisited.empty() || ongoingHeroMovement)
-		cv.wait_for(lock, boost::chrono::milliseconds(100));
-}
-
-bool AIStatus::haveTurn()
-{
-	boost::unique_lock<boost::mutex> lock(mx);
-	return havingTurn;
-}
-
-void AIStatus::attemptedAnsweringQuery(QueryID queryID, int answerRequestID)
-{
-	boost::unique_lock<boost::mutex> lock(mx);
-	assert(vstd::contains(remainingQueries, queryID));
-	std::string description = remainingQueries[queryID];
-	logAi->debug("Attempted answering query %d - %s. Request id=%d. Waiting for results...", queryID, description, answerRequestID);
-	requestToQueryID[answerRequestID] = queryID;
-}
-
-void AIStatus::receivedAnswerConfirmation(int answerRequestID, int result)
-{
-	QueryID query;
-
-	{
-		boost::unique_lock<boost::mutex> lock(mx);
-
-		assert(vstd::contains(requestToQueryID, answerRequestID));
-		query = requestToQueryID[answerRequestID];
-		assert(vstd::contains(remainingQueries, query));
-		requestToQueryID.erase(answerRequestID);
-	}
-
-	if(result)
-	{
-		removeQuery(query);
-	}
-	else
-	{
-		logAi->error("Something went really wrong, failed to answer query %d : %s", query.getNum(), remainingQueries[query]);
-		//TODO safely retry
-	}
-}
-
-void AIStatus::heroVisit(const CGObjectInstance * obj, bool started)
-{
-	boost::unique_lock<boost::mutex> lock(mx);
-	if(started)
-	{
-		objectsBeingVisited.push_back(obj);
-	}
-	else
-	{
-		// There can be more than one object visited at the time (eg. hero visits Subterranean Gate
-		// causing visit to hero on the other side.
-		// However, we are guaranteed that start/end visit notification maintain stack order.
-		assert(!objectsBeingVisited.empty());
-		objectsBeingVisited.pop_back();
-	}
-	cv.notify_all();
-}
-
-void AIStatus::setMove(bool ongoing)
-{
-	boost::unique_lock<boost::mutex> lock(mx);
-	ongoingHeroMovement = ongoing;
-	cv.notify_all();
-}
-
-void AIStatus::setChannelProbing(bool ongoing)
-{
-	boost::unique_lock<boost::mutex> lock(mx);
-	ongoingChannelProbing = ongoing;
-	cv.notify_all();
-}
-
-bool AIStatus::channelProbing()
-{
-	return ongoingChannelProbing;
-}
-
-
-
-bool isWeeklyRevisitable(const CGObjectInstance * obj)
-{
-	//TODO: allow polling of remaining creatures in dwelling
-	if(const auto * rewardable = dynamic_cast<const CRewardableObject *>(obj))
-		return rewardable->configuration.getResetDuration() == 7;
-
-	if(dynamic_cast<const CGDwelling *>(obj))
-		return true;
-	if(dynamic_cast<const CBank *>(obj)) //banks tend to respawn often in mods
-		return true;
-
-	switch(obj->ID)
-	{
-	case Obj::STABLES:
-	case Obj::MAGIC_WELL:
-	case Obj::HILL_FORT:
-		return true;
-	case Obj::BORDER_GATE:
-	case Obj::BORDERGUARD:
-		return (dynamic_cast<const CGKeys *>(obj))->wasMyColorVisited(ai->playerID); //FIXME: they could be revisited sooner than in a week
-	}
-	return false;
-}
-
-bool shouldVisit(HeroPtr h, const CGObjectInstance * obj)
-{
-	switch(obj->ID)
-	{
-	case Obj::TOWN:
-	case Obj::HERO: //never visit our heroes at random
-		return obj->tempOwner != h->tempOwner; //do not visit our towns at random
-	case Obj::BORDER_GATE:
-	{
-		for(auto q : ai->myCb->getMyQuests())
-		{
-			if(q.obj == obj)
-			{
-				return false; // do not visit guards or gates when wandering
-			}
-		}
-		return true; //we don't have this quest yet
-	}
-	case Obj::BORDERGUARD: //open borderguard if possible
-		return (dynamic_cast<const CGKeys *>(obj))->wasMyColorVisited(ai->playerID);
-	case Obj::SEER_HUT:
-	case Obj::QUEST_GUARD:
-	{
-		for(auto q : ai->myCb->getMyQuests())
-		{
-			if(q.obj == obj)
-			{
-				if(q.quest->checkQuest(h.h))
-					return true; //we completed the quest
-				else
-					return false; //we can't complete this quest
-			}
-		}
-		return true; //we don't have this quest yet
-	}
-	case Obj::CREATURE_GENERATOR1:
-	{
-		if(obj->tempOwner != h->tempOwner)
-			return true; //flag just in case
-		bool canRecruitCreatures = false;
-		const CGDwelling * d = dynamic_cast<const CGDwelling *>(obj);
-		for(auto level : d->creatures)
-		{
-			for(auto c : level.second)
-			{
-				if(h->getSlotFor(CreatureID(c)) != SlotID())
-					canRecruitCreatures = true;
-			}
-		}
-		return canRecruitCreatures;
-	}
-	case Obj::HILL_FORT:
-	{
-		for(auto slot : h->Slots())
-		{
-			if(slot.second->type->hasUpgrades())
-				return true; //TODO: check price?
-		}
-		return false;
-	}
-	case Obj::MONOLITH_ONE_WAY_ENTRANCE:
-	case Obj::MONOLITH_ONE_WAY_EXIT:
-	case Obj::MONOLITH_TWO_WAY:
-	case Obj::WHIRLPOOL:
-		return false;
-	case Obj::SCHOOL_OF_MAGIC:
-	case Obj::SCHOOL_OF_WAR:
-	{
-		if (ai->ah->freeGold() < 1000)
-			return false;
-		break;
-	}
-	case Obj::LIBRARY_OF_ENLIGHTENMENT:
-		if(h->level < 12)
-			return false;
-		break;
-	case Obj::TREE_OF_KNOWLEDGE:
-	{
-		TResources myRes = ai->ah->freeResources();
-		if(myRes[EGameResID::GOLD] < 2000 || myRes[EGameResID::GEMS] < 10)
-			return false;
-		break;
-	}
-	case Obj::MAGIC_WELL:
-		return h->mana < h->manaLimit();
-	case Obj::PRISON:
-		return ai->myCb->getHeroesInfo().size() < VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP);
-	case Obj::TAVERN:
-	{
-		//TODO: make AI actually recruit heroes
-		//TODO: only on request
-		if(ai->myCb->getHeroesInfo().size() >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
-			return false;
-		else if(ai->ah->freeGold() < GameConstants::HERO_GOLD_COST)
-			return false;
-		break;
-	}
-	case Obj::BOAT:
-		return false;
-	//Boats are handled by pathfinder
-	case Obj::EYE_OF_MAGI:
-		return false; //this object is useless to visit, but could be visited indefinitely
-	}
-
-	if(obj->wasVisited(*h)) //it must pointer to hero instance, heroPtr calls function wasVisited(ui8 player);
-		return false;
-
-	return true;
-}
-
-std::optional<BattleAction> VCAI::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState)
-{
-	return std::nullopt;
-}
+/*
+ * VCAI.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 "VCAI.h"
+#include "FuzzyHelper.h"
+#include "ResourceManager.h"
+#include "BuildingManager.h"
+#include "Goals/Goals.h"
+
+#include "../../lib/UnlockGuard.h"
+#include "../../lib/mapObjects/MapObjects.h"
+#include "../../lib/mapObjects/ObjectTemplate.h"
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/CHeroHandler.h"
+#include "../../lib/GameSettings.h"
+#include "../../lib/gameState/CGameState.h"
+#include "../../lib/NetPacksBase.h"
+#include "../../lib/NetPacks.h"
+#include "../../lib/bonuses/CBonusSystemNode.h"
+#include "../../lib/bonuses/Limiters.h"
+#include "../../lib/bonuses/Updaters.h"
+#include "../../lib/bonuses/Propagators.h"
+#include "../../lib/serializer/CTypeList.h"
+#include "../../lib/serializer/BinarySerializer.h"
+#include "../../lib/serializer/BinaryDeserializer.h"
+
+#include "AIhelper.h"
+
+extern FuzzyHelper * fh;
+
+const double SAFE_ATTACK_CONSTANT = 1.5;
+
+//one thread may be turn of AI and another will be handling a side effect for AI2
+thread_local CCallback * cb = nullptr;
+thread_local VCAI * ai = nullptr;
+
+//std::map<int, std::map<int, int> > HeroView::infosCount;
+
+//helper RAII to manage global ai/cb ptrs
+struct SetGlobalState
+{
+	SetGlobalState(VCAI * AI)
+	{
+		assert(!ai);
+		assert(!cb);
+
+		ai = AI;
+		cb = AI->myCb.get();
+	}
+	~SetGlobalState()
+	{
+		//TODO: how to handle rm? shouldn't be called after ai is destroyed, hopefully
+		//TODO: to ensure that, make rm unique_ptr
+		ai = nullptr;
+		cb = nullptr;
+	}
+};
+
+
+#define SET_GLOBAL_STATE(ai) SetGlobalState _hlpSetState(ai);
+
+#define NET_EVENT_HANDLER SET_GLOBAL_STATE(this)
+#define MAKING_TURN SET_GLOBAL_STATE(this)
+
+VCAI::VCAI()
+{
+	LOG_TRACE(logAi);
+	makingTurn = nullptr;
+	destinationTeleport = ObjectInstanceID();
+	destinationTeleportPos = int3(-1);
+
+	ah = new AIhelper();
+	ah->setAI(this);
+}
+
+VCAI::~VCAI()
+{
+	delete ah;
+	LOG_TRACE(logAi);
+	finish();
+}
+
+void VCAI::availableCreaturesChanged(const CGDwelling * town)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::heroMoved(const TryMoveHero & details, bool verbose)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+
+	//enemy hero may have left visible area
+	validateObject(details.id);
+	auto hero = cb->getHero(details.id);
+
+	const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));;
+	const int3 to   = hero ? hero->convertToVisitablePos(details.end)   : (details.end   - int3(0,1,0));
+
+	const CGObjectInstance * o1 = vstd::frontOrNull(cb->getVisitableObjs(from, verbose));
+	const CGObjectInstance * o2 = vstd::frontOrNull(cb->getVisitableObjs(to, verbose));
+
+	if(details.result == TryMoveHero::TELEPORTATION)
+	{
+		auto t1 = dynamic_cast<const CGTeleport *>(o1);
+		auto t2 = dynamic_cast<const CGTeleport *>(o2);
+		if(t1 && t2)
+		{
+			if(cb->isTeleportChannelBidirectional(t1->channel))
+			{
+				if(o1->ID == Obj::SUBTERRANEAN_GATE && o1->ID == o2->ID) // We need to only add subterranean gates in knownSubterraneanGates. Used for features not yet ported to use teleport channels
+				{
+					knownSubterraneanGates[o1] = o2;
+					knownSubterraneanGates[o2] = o1;
+					logAi->debug("Found a pair of subterranean gates between %s and %s!", from.toString(), to.toString());
+				}
+			}
+		}
+		//FIXME: teleports are not correctly visited
+		unreserveObject(hero, t1);
+		unreserveObject(hero, t2);
+	}
+	else if(details.result == TryMoveHero::EMBARK && hero)
+	{
+		//make sure AI not attempt to visit used boat
+		validateObject(hero->boat);
+	}
+	else if(details.result == TryMoveHero::DISEMBARK && o1)
+	{
+		auto boat = dynamic_cast<const CGBoat *>(o1);
+		if(boat)
+			addVisitableObj(boat);
+	}
+}
+
+void VCAI::heroInGarrisonChange(const CGTownInstance * town)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::centerView(int3 pos, int focusTime)
+{
+	LOG_TRACE_PARAMS(logAi, "focusTime '%i'", focusTime);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::artifactAssembled(const ArtifactLocation & al)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+
+	status.addQuery(queryID, "TavernWindow");
+	requestActionASAP([=](){ answerQuery(queryID, 0); });
+}
+
+void VCAI::showThievesGuildWindow(const CGObjectInstance * obj)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::playerBlocked(int reason, bool start)
+{
+	LOG_TRACE_PARAMS(logAi, "reason '%i', start '%i'", reason % start);
+	NET_EVENT_HANDLER;
+	if(start && reason == PlayerBlocked::UPCOMING_BATTLE)
+		status.setBattle(UPCOMING_BATTLE);
+
+	if(reason == PlayerBlocked::ONGOING_MOVEMENT)
+		status.setMove(start);
+}
+
+void VCAI::showPuzzleMap()
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::showShipyardDialog(const IShipyard * obj)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult)
+{
+	LOG_TRACE_PARAMS(logAi, "victoryLossCheckResult '%s'", victoryLossCheckResult.messageToSelf.toString());
+	NET_EVENT_HANDLER;
+	logAi->debug("Player %d (%s): I heard that player %d (%s) %s.", playerID, playerID.toString(), player, player.toString(), (victoryLossCheckResult.victory() ? "won" : "lost"));
+	if(player == playerID)
+	{
+		if(victoryLossCheckResult.victory())
+		{
+			logAi->debug("VCAI: I won! Incredible!");
+			logAi->debug("Turn nr %d", myCb->getDate());
+		}
+		else
+		{
+			logAi->debug("VCAI: Player %d (%s) lost. It's me. What a disappointment! :(", player, player.toString());
+		}
+
+		finish();
+	}
+}
+
+void VCAI::artifactPut(const ArtifactLocation & al)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::artifactRemoved(const ArtifactLocation & al)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::artifactDisassembled(const ArtifactLocation & al)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start)
+{
+	LOG_TRACE_PARAMS(logAi, "start '%i'; obj '%s'", start % (visitedObj ? visitedObj->getObjectName() : std::string("n/a")));
+	NET_EVENT_HANDLER;
+
+	if(start && visitedObj) //we can end visit with null object, anyway
+	{
+		markObjectVisited(visitedObj);
+		unreserveObject(visitor, visitedObj);
+		completeGoal(sptr(Goals::VisitObj(visitedObj->id.getNum()).sethero(visitor))); //we don't need to visit it anymore
+		//TODO: what if we visited one-time visitable object that was reserved by another hero (shouldn't, but..)
+		if (visitedObj->ID == Obj::HERO)
+		{
+			visitedHeroes[visitor].insert(HeroPtr(dynamic_cast<const CGHeroInstance *>(visitedObj)));
+		}
+	}
+
+	status.heroVisit(visitedObj, start);
+}
+
+void VCAI::availableArtifactsChanged(const CGBlackMarket * bm)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+	//buildArmyIn(town);
+	//moveCreaturesToHero(town);
+}
+
+void VCAI::tileHidden(const std::unordered_set<int3> & pos)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+
+	validateVisitableObjs();
+	clearPathsInfo();
+}
+
+void VCAI::tileRevealed(const std::unordered_set<int3> & pos)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+	for(int3 tile : pos)
+	{
+		for(const CGObjectInstance * obj : myCb->getVisitableObjs(tile))
+			addVisitableObj(obj);
+	}
+
+	clearPathsInfo();
+}
+
+void VCAI::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+
+	auto firstHero = cb->getHero(hero1);
+	auto secondHero = cb->getHero(hero2);
+
+	status.addQuery(query, boost::str(boost::format("Exchange between heroes %s (%d) and %s (%d)") % firstHero->getNameTranslated() % firstHero->tempOwner % secondHero->getNameTranslated() % secondHero->tempOwner));
+
+	requestActionASAP([=]()
+	{
+		float goalpriority1 = 0, goalpriority2 = 0;
+
+		auto firstGoal = getGoal(firstHero);
+		if(firstGoal->goalType == Goals::GATHER_ARMY)
+			goalpriority1 = firstGoal->priority;
+		auto secondGoal = getGoal(secondHero);
+		if(secondGoal->goalType == Goals::GATHER_ARMY)
+			goalpriority2 = secondGoal->priority;
+
+		auto transferFrom2to1 = [this](const CGHeroInstance * h1, const CGHeroInstance * h2) -> void
+		{
+			this->pickBestCreatures(h1, h2);
+			this->pickBestArtifacts(h1, h2);
+		};
+
+		//Do not attempt army or artifacts exchange if we visited ally player
+		//Visits can still be useful if hero have skills like Scholar
+		if(firstHero->tempOwner != secondHero->tempOwner)
+		{
+			logAi->debug("Heroes owned by different players. Do not exchange army or artifacts.");
+		}
+		else if(goalpriority1 > goalpriority2)
+		{
+			transferFrom2to1(firstHero, secondHero);
+		}
+		else if(goalpriority1 < goalpriority2)
+		{
+			transferFrom2to1(secondHero, firstHero);
+		}
+		else //regular criteria
+		{
+			if(firstHero->getFightingStrength() > secondHero->getFightingStrength() && ah->canGetArmy(firstHero, secondHero))
+				transferFrom2to1(firstHero, secondHero);
+			else if(ah->canGetArmy(secondHero, firstHero))
+				transferFrom2to1(secondHero, firstHero);
+		}
+
+		completeGoal(sptr(Goals::VisitHero(firstHero->id.getNum()))); //TODO: what if we were visited by other hero in the meantime?
+		completeGoal(sptr(Goals::VisitHero(secondHero->id.getNum())));
+
+		answerQuery(query, 0);
+	});
+}
+
+void VCAI::heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val)
+{
+	LOG_TRACE_PARAMS(logAi, "which '%i', val '%i'", static_cast<int>(which) % val);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level, QueryID queryID)
+{
+	LOG_TRACE_PARAMS(logAi, "level '%i'", level);
+	NET_EVENT_HANDLER;
+
+	status.addQuery(queryID, "RecruitmentDialog");
+	requestActionASAP([=](){
+		recruitCreatures(dwelling, dst);
+		checkHeroArmy(dynamic_cast<const CGHeroInstance*>(dst));
+		answerQuery(queryID, 0);
+	});
+}
+
+void VCAI::heroMovePointsChanged(const CGHeroInstance * hero)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::newObject(const CGObjectInstance * obj)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+	if(obj->isVisitable())
+		addVisitableObj(obj);
+}
+
+//to prevent AI from accessing objects that got deleted while they became invisible (Cover of Darkness, enemy hero moved etc.) below code allows AI to know deletion of objects out of sight
+//see: RemoveObject::applyFirstCl, to keep AI "not cheating" do not use advantage of this and use this function just to prevent crashes
+void VCAI::objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+
+	vstd::erase_if_present(visitableObjs, obj);
+	vstd::erase_if_present(alreadyVisited, obj);
+
+	for(auto h : cb->getHeroesInfo())
+		unreserveObject(h, obj);
+
+	std::function<bool(const Goals::TSubgoal &)> checkRemovalValidity = [&](const Goals::TSubgoal & x) -> bool
+	{
+		if((x->goalType == Goals::VISIT_OBJ) && (x->objid == obj->id.getNum()))
+			return true;
+		else if(x->parent && checkRemovalValidity(x->parent)) //repeat this lambda check recursively on parent goal
+			return true;
+		else
+			return false;
+	};
+
+	//clear VCAI / main loop caches
+	vstd::erase_if(lockedHeroes, [&](const std::pair<HeroPtr, Goals::TSubgoal> & x) -> bool
+	{
+		return checkRemovalValidity(x.second);
+	});
+
+	vstd::erase_if(ultimateGoalsFromBasic, [&](const std::pair<Goals::TSubgoal, Goals::TGoalVec> & x) -> bool
+	{
+		return checkRemovalValidity(x.first);
+	});
+
+	vstd::erase_if(basicGoals, checkRemovalValidity);
+	vstd::erase_if(goalsToAdd, checkRemovalValidity);
+	vstd::erase_if(goalsToRemove, checkRemovalValidity);
+
+	for(auto goal : ultimateGoalsFromBasic)
+		vstd::erase_if(goal.second, checkRemovalValidity);
+
+	//clear resource manager goal cache
+	ah->removeOutdatedObjectives(checkRemovalValidity);
+
+	//TODO: Find better way to handle hero boat removal
+	if(auto hero = dynamic_cast<const CGHeroInstance *>(obj))
+	{
+		if(hero->boat)
+		{
+			vstd::erase_if_present(visitableObjs, hero->boat);
+			vstd::erase_if_present(alreadyVisited, hero->boat);
+
+			for(auto h : cb->getHeroesInfo())
+				unreserveObject(h, hero->boat);
+		}
+	}
+
+	//TODO
+	//there are other places where CGObjectinstance ptrs are stored...
+	//
+
+	if(obj->ID == Obj::HERO && obj->tempOwner == playerID)
+	{
+		lostHero(cb->getHero(obj->id)); //we can promote, since objectRemoved is called just before actual deletion
+	}
+}
+
+void VCAI::showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+
+	requestActionASAP([=]()
+	{
+		makePossibleUpgrades(visitor);
+	});
+}
+
+void VCAI::playerBonusChanged(const Bonus & bonus, bool gain)
+{
+	LOG_TRACE_PARAMS(logAi, "gain '%i'", gain);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::heroCreated(const CGHeroInstance * h)
+{
+	LOG_TRACE(logAi);
+	if(h->visitedTown)
+		townVisitsThisWeek[HeroPtr(h)].insert(h->visitedTown);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::advmapSpellCast(const CGHeroInstance * caster, SpellID spellID)
+{
+	LOG_TRACE_PARAMS(logAi, "spellID '%i", spellID);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector<Component> & components, int soundID)
+{
+	LOG_TRACE_PARAMS(logAi, "soundID '%i'", soundID);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::requestRealized(PackageApplied * pa)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+	if(status.haveTurn())
+	{
+		if(pa->packType == typeList.getTypeID<EndTurn>())
+		{
+			if(pa->result)
+				status.madeTurn();
+		}
+	}
+
+	if(pa->packType == typeList.getTypeID<QueryReply>())
+	{
+		status.receivedAnswerConfirmation(pa->requestID, pa->result);
+	}
+}
+
+void VCAI::receivedResource()
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+
+	status.addQuery(queryID, "UniversityWindow");
+	requestActionASAP([=](){ answerQuery(queryID, 0); });
+}
+
+void VCAI::heroManaPointsChanged(const CGHeroInstance * hero)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val)
+{
+	LOG_TRACE_PARAMS(logAi, "which '%d', val '%d'", which % val);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::battleResultsApplied()
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+	assert(status.getBattle() == ENDING_BATTLE);
+	status.setBattle(NO_BATTLE);
+}
+
+void VCAI::beforeObjectPropertyChanged(const SetObjectProperty * sop)
+{
+
+}
+
+void VCAI::objectPropertyChanged(const SetObjectProperty * sop)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+	if(sop->what == ObjProperty::OWNER)
+	{
+		if(myCb->getPlayerRelations(playerID, (PlayerColor)sop->val) == PlayerRelations::ENEMIES)
+		{
+			//we want to visit objects owned by oppponents
+			auto obj = myCb->getObj(sop->id, false);
+			if(obj)
+			{
+				addVisitableObj(obj); // TODO: Remove once save compatability broken. In past owned objects were removed from this set
+				vstd::erase_if_present(alreadyVisited, obj);
+			}
+		}
+	}
+}
+
+void VCAI::buildChanged(const CGTownInstance * town, BuildingID buildingID, int what)
+{
+	LOG_TRACE_PARAMS(logAi, "what '%i'", what);
+	NET_EVENT_HANDLER;
+
+	if(town->getOwner() == playerID && what == 1) //built
+		completeGoal(sptr(Goals::BuildThis(buildingID, town)));
+}
+
+void VCAI::heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain)
+{
+	LOG_TRACE_PARAMS(logAi, "gain '%i'", gain);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID)
+{
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+
+	status.addQuery(queryID, "MarketWindow");
+	requestActionASAP([=](){ answerQuery(queryID, 0); });
+}
+
+void VCAI::showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain)
+{
+	//TODO: AI support for ViewXXX spell
+	LOG_TRACE(logAi);
+	NET_EVENT_HANDLER;
+}
+
+void VCAI::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB)
+{
+	LOG_TRACE(logAi);
+	env = ENV;
+	myCb = CB;
+	cbc = CB;
+
+	ah->init(CB.get());
+
+	NET_EVENT_HANDLER; //sets ah->rm->cb
+	playerID = *myCb->getPlayerID();
+	myCb->waitTillRealize = true;
+	myCb->unlockGsWhenWaiting = true;
+
+	if(!fh)
+		fh = new FuzzyHelper();
+
+	retrieveVisitableObjs();
+}
+
+void VCAI::yourTurn(QueryID queryID)
+{
+	LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID);
+	NET_EVENT_HANDLER;
+	status.addQuery(queryID, "YourTurn");
+	requestActionASAP([=](){ answerQuery(queryID, 0); });
+	status.startedTurn();
+	makingTurn = std::make_unique<boost::thread>(&VCAI::makeTurn, this);
+}
+
+void VCAI::heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector<SecondarySkill> & skills, QueryID queryID)
+{
+	LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID);
+	NET_EVENT_HANDLER;
+	status.addQuery(queryID, boost::str(boost::format("Hero %s got level %d") % hero->getNameTranslated() % hero->level));
+	requestActionASAP([=](){ answerQuery(queryID, 0); });
+}
+
+void VCAI::commanderGotLevel(const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID)
+{
+	LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID);
+	NET_EVENT_HANDLER;
+	status.addQuery(queryID, boost::str(boost::format("Commander %s of %s got level %d") % commander->name % commander->armyObj->nodeName() % (int)commander->level));
+	requestActionASAP([=](){ answerQuery(queryID, 0); });
+}
+
+void VCAI::showBlockingDialog(const std::string & text, const std::vector<Component> & components, QueryID askID, const int soundID, bool selection, bool cancel)
+{
+	LOG_TRACE_PARAMS(logAi, "text '%s', askID '%i', soundID '%i', selection '%i', cancel '%i'", text % askID % soundID % selection % cancel);
+	NET_EVENT_HANDLER;
+	int sel = 0;
+	status.addQuery(askID, boost::str(boost::format("Blocking dialog query with %d components - %s")
+									  % components.size() % text));
+
+	if(selection) //select from multiple components -> take the last one (they're indexed [1-size])
+		sel = static_cast<int>(components.size());
+
+	if(!selection && cancel) //yes&no -> always answer yes, we are a brave AI :)
+		sel = 1;
+
+	requestActionASAP([=]()
+	{
+		answerQuery(askID, sel);
+	});
+}
+
+void VCAI::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID)
+{
+//	LOG_TRACE_PARAMS(logAi, "askID '%i', exits '%s'", askID % exits);
+	NET_EVENT_HANDLER;
+	status.addQuery(askID, boost::str(boost::format("Teleport dialog query with %d exits")
+																			% exits.size()));
+
+	int choosenExit = -1;
+	if(impassable)
+	{
+		knownTeleportChannels[channel]->passability = TeleportChannel::IMPASSABLE;
+	}
+	else if(destinationTeleport != ObjectInstanceID() && destinationTeleportPos.valid())
+	{
+		auto neededExit = std::make_pair(destinationTeleport, destinationTeleportPos);
+		if(destinationTeleport != ObjectInstanceID() && vstd::contains(exits, neededExit))
+			choosenExit = vstd::find_pos(exits, neededExit);
+	}
+
+	for(auto exit : exits)
+	{
+		if(status.channelProbing() && exit.first == destinationTeleport)
+		{
+			choosenExit = vstd::find_pos(exits, exit);
+			break;
+		}
+		else
+		{
+			// TODO: Implement checking if visiting that teleport will uncovert any FoW
+			// So far this is the best option to handle decision about probing
+			auto obj = cb->getObj(exit.first, false);
+			if(obj == nullptr && !vstd::contains(teleportChannelProbingList, exit.first))
+			{
+				if(exit.first != destinationTeleport)
+					teleportChannelProbingList.push_back(exit.first);
+			}
+		}
+	}
+
+	requestActionASAP([=]()
+	{
+		answerQuery(askID, choosenExit);
+	});
+}
+
+void VCAI::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID)
+{
+	LOG_TRACE_PARAMS(logAi, "removableUnits '%i', queryID '%i'", removableUnits % queryID);
+	NET_EVENT_HANDLER;
+
+	std::string s1 = up ? up->nodeName() : "NONE";
+	std::string s2 = down ? down->nodeName() : "NONE";
+
+	status.addQuery(queryID, boost::str(boost::format("Garrison dialog with %s and %s") % s1 % s2));
+
+	//you can't request action from action-response thread
+	requestActionASAP([=]()
+	{
+		if(removableUnits)
+			pickBestCreatures(down, up);
+
+		answerQuery(queryID, 0);
+	});
+}
+
+void VCAI::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects)
+{
+	NET_EVENT_HANDLER;
+	status.addQuery(askID, "Map object select query");
+	requestActionASAP([=](){ answerQuery(askID, selectedObject.getNum()); });
+}
+
+void VCAI::saveGame(BinarySerializer & h, const int version)
+{
+	LOG_TRACE_PARAMS(logAi, "version '%i'", version);
+	NET_EVENT_HANDLER;
+	validateVisitableObjs();
+
+	#if 0
+	//disabled due to issue 2890
+	registerGoals(h);
+	#endif // 0
+	CAdventureAI::saveGame(h, version);
+	serializeInternal(h, version);
+}
+
+void VCAI::loadGame(BinaryDeserializer & h, const int version)
+{
+	LOG_TRACE_PARAMS(logAi, "version '%i'", version);
+	//NET_EVENT_HANDLER;
+
+	#if 0
+	//disabled due to issue 2890
+	registerGoals(h);
+	#endif // 0
+	CAdventureAI::loadGame(h, version);
+	serializeInternal(h, version);
+}
+
+void makePossibleUpgrades(const CArmedInstance * obj)
+{
+	if(!obj)
+		return;
+
+	for(int i = 0; i < GameConstants::ARMY_SIZE; i++)
+	{
+		if(const CStackInstance * s = obj->getStackPtr(SlotID(i)))
+		{
+			UpgradeInfo ui;
+			cb->fillUpgradeInfo(obj, SlotID(i), ui);
+			if(ui.oldID != CreatureID::NONE && cb->getResourceAmount().canAfford(ui.cost[0] * s->count))
+			{
+				cb->upgradeCreature(obj, SlotID(i), ui.newID[0]);
+			}
+		}
+	}
+}
+
+void VCAI::makeTurn()
+{
+	MAKING_TURN;
+
+	auto day = cb->getDate(Date::DAY);
+	logAi->info("Player %d (%s) starting turn, day %d", playerID, playerID.toString(), day);
+
+	boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
+	setThreadName("VCAI::makeTurn");
+
+	switch(cb->getDate(Date::DAY_OF_WEEK))
+	{
+	case 1:
+	{
+		townVisitsThisWeek.clear();
+		std::vector<const CGObjectInstance *> objs;
+		retrieveVisitableObjs(objs, true);
+		for(const CGObjectInstance * obj : objs)
+		{
+			if(isWeeklyRevisitable(obj))
+			{
+				addVisitableObj(obj);
+				vstd::erase_if_present(alreadyVisited, obj);
+			}
+		}
+		break;
+	}
+	}
+	markHeroAbleToExplore(primaryHero());
+	visitedHeroes.clear();
+
+	try
+	{
+		//it looks messy here, but it's better to have armed heroes before attempting realizing goals
+		for (const CGTownInstance * t : cb->getTownsInfo())
+			moveCreaturesToHero(t);
+
+		mainLoop();
+
+		/*Below function is also responsible for hero movement via internal wander function. By design it is separate logic for heroes that have nothing to do.
+		Heroes that were not picked by striveToGoal(sptr(Goals::Win())); recently (so they do not have new goals and cannot continue/reevaluate previously locked goals) will do logic in wander().*/
+		performTypicalActions();
+
+		//for debug purpose
+		for (auto h : cb->getHeroesInfo())
+		{
+			if (h->movementPointsRemaining())
+				logAi->info("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining());
+		}
+	}
+	catch (boost::thread_interrupted & e)
+	{
+	(void)e;
+		logAi->debug("Making turn thread has been interrupted. We'll end without calling endTurn.");
+		return;
+	}
+	catch (std::exception & e)
+	{
+		logAi->debug("Making turn thread has caught an exception: %s", e.what());
+	}
+
+	endTurn();
+}
+
+std::vector<HeroPtr> VCAI::getMyHeroes() const
+{
+	std::vector<HeroPtr> ret;
+
+	for(auto h : cb->getHeroesInfo())
+	{
+		ret.push_back(h);
+	}
+
+	return ret;
+}
+
+void VCAI::mainLoop()
+{
+	std::vector<Goals::TSubgoal> elementarGoals; //no duplicates allowed (operator ==)
+	basicGoals.clear();
+
+	validateVisitableObjs();
+
+	//get all potential and saved goals
+	//TODO: not lose
+	basicGoals.push_back(sptr(Goals::Win()));
+	for (auto goalPair : lockedHeroes)
+	{
+		fh->setPriority(goalPair.second);  //re-evaluate, as heroes moved in the meantime
+		basicGoals.push_back(goalPair.second);
+	}
+	if (ah->hasTasksLeft())
+		basicGoals.push_back(ah->whatToDo());
+	for (auto quest : myCb->getMyQuests())
+	{
+		basicGoals.push_back(sptr(Goals::CompleteQuest(quest)));
+	}
+	basicGoals.push_back(sptr(Goals::Build()));
+
+	invalidPathHeroes.clear();
+
+	for (int pass = 0; pass< 30 && basicGoals.size(); pass++)
+	{
+		vstd::removeDuplicates(basicGoals); //TODO: container which does this automagically without has would be nice
+		goalsToAdd.clear();
+		goalsToRemove.clear();
+		elementarGoals.clear();
+		ultimateGoalsFromBasic.clear();
+
+		ah->updatePaths(getMyHeroes());
+
+		logAi->debug("Main loop: decomposing %i basic goals", basicGoals.size());
+
+		for (auto basicGoal : basicGoals)
+		{
+			logAi->debug("Main loop: decomposing basic goal %s", basicGoal->name());
+
+			auto goalToDecompose = basicGoal;
+			Goals::TSubgoal elementarGoal = sptr(Goals::Invalid());
+			int maxAbstractGoals = 10;
+			while (!elementarGoal->isElementar && maxAbstractGoals)
+			{
+				try
+				{
+					elementarGoal = decomposeGoal(goalToDecompose);
+				}
+				catch (goalFulfilledException & e)
+				{
+					//it is impossible to continue some goals (like exploration, for example)
+					//complete abstract goal for now, but maybe main goal finds another path
+					logAi->debug("Goal %s decomposition failed: goal was completed as much as possible", e.goal->name());
+					completeGoal(e.goal); //put in goalsToRemove
+					break;
+				}
+				catch(cannotFulfillGoalException & e)
+				{
+					//it is impossible to continue some goals (like exploration, for example)
+					//complete abstract goal for now, but maybe main goal finds another path
+					goalsToRemove.push_back(basicGoal);
+					logAi->debug("Goal %s decomposition failed: %s", goalToDecompose->name(), e.what());
+					break;
+				}
+				catch (std::exception & e) //decomposition failed, which means we can't decompose entire tree
+				{
+					goalsToRemove.push_back(basicGoal);
+					logAi->debug("Goal %s decomposition failed: %s", basicGoal->name(), e.what());
+					break;
+				}
+				if (elementarGoal->isAbstract) //we can decompose it further
+				{
+					goalsToAdd.push_back(elementarGoal);
+					//decompose further now - this is necesssary if we can't add over 10 goals in the pool
+					goalToDecompose = elementarGoal;
+					//there is a risk of infinite abstract goal loop, though it indicates failed logic
+					maxAbstractGoals--;
+				}
+				else if (elementarGoal->isElementar) //should be
+				{
+					logAi->debug("Found elementar goal %s", elementarGoal->name());
+					elementarGoals.push_back(elementarGoal);
+					ultimateGoalsFromBasic[elementarGoal].push_back(goalToDecompose); //TODO: how about indirect basicGoal?
+					break;
+				}
+				else //should never be here
+					throw cannotFulfillGoalException("Goal %s is neither abstract nor elementar!" + basicGoal->name());
+			}
+		}
+
+		logAi->trace("Main loop: selecting best elementar goal");
+
+		//now choose one elementar goal to realize
+		Goals::TGoalVec possibleGoals(elementarGoals.begin(), elementarGoals.end()); //copy to vector
+		Goals::TSubgoal goalToRealize = sptr(Goals::Invalid());
+		while (possibleGoals.size())
+		{
+			//allow assign goals to heroes with 0 movement, but don't realize them
+			//maybe there are beter ones left
+
+			auto bestGoal = fh->chooseSolution(possibleGoals);
+			if (bestGoal->hero) //lock this hero to fulfill goal
+			{
+				setGoal(bestGoal->hero, bestGoal);
+				if (!bestGoal->hero->movementPointsRemaining() || vstd::contains(invalidPathHeroes, bestGoal->hero))
+				{
+					if (!vstd::erase_if_present(possibleGoals, bestGoal))
+					{
+						logAi->error("erase_if_preset failed? Something very wrong!");
+						break;
+					}
+					continue; //chose next from the list
+				}
+			}
+			goalToRealize = bestGoal; //we found our goal to execute
+			break;
+		}
+
+		//realize best goal
+		if (!goalToRealize->invalid())
+		{
+			logAi->debug("Trying to realize %s (value %2.3f)", goalToRealize->name(), goalToRealize->priority);
+
+			try
+			{
+				boost::this_thread::interruption_point();
+				goalToRealize->accept(this); //visitor pattern
+				boost::this_thread::interruption_point();
+			}
+			catch (boost::thread_interrupted & e)
+			{
+				(void)e;
+				logAi->debug("Player %d: Making turn thread received an interruption!", playerID);
+				throw; //rethrow, we want to truly end this thread
+			}
+			catch (goalFulfilledException & e)
+			{
+				//the sub-goal was completed successfully
+				completeGoal(e.goal);
+				//local goal was also completed?
+				completeGoal(goalToRealize);
+
+				// remove abstract visit tile if we completed the elementar one
+				vstd::erase_if_present(goalsToAdd, goalToRealize);
+			}
+			catch (std::exception & e)
+			{
+				logAi->debug("Failed to realize subgoal of type %s, I will stop.", goalToRealize->name());
+				logAi->debug("The error message was: %s", e.what());
+
+				//erase base goal if we failed to execute decomposed goal
+				for (auto basicGoal : ultimateGoalsFromBasic[goalToRealize])
+					goalsToRemove.push_back(basicGoal);
+
+				// sometimes resource manager contains an elementar goal which is not able to execute anymore and just fails each turn.
+				ai->ah->notifyGoalCompleted(goalToRealize);
+
+				//we failed to realize best goal, but maybe others are still possible?
+			}
+
+			//remove goals we couldn't decompose
+			for (auto goal : goalsToRemove)
+				vstd::erase_if_present(basicGoals, goal);
+
+			//add abstract goals
+			boost::sort(goalsToAdd, [](const Goals::TSubgoal & lhs, const Goals::TSubgoal & rhs) -> bool
+			{
+				return lhs->priority > rhs->priority; //highest priority at the beginning
+			});
+
+			//max number of goals = 10
+			int i = 0;
+			while (basicGoals.size() < 10 && goalsToAdd.size() > i)
+			{
+				if (!vstd::contains(basicGoals, goalsToAdd[i])) //don't add duplicates
+					basicGoals.push_back(goalsToAdd[i]);
+				i++;
+			}
+		}
+		else //no elementar goals possible
+		{
+			logAi->debug("Goal decomposition exhausted");
+			break;
+		}
+	}
+}
+
+void VCAI::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h)
+{
+	LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->getNameTranslated() % obj->getObjectName() % obj->pos.toString());
+	switch(obj->ID)
+	{
+	case Obj::TOWN:
+		moveCreaturesToHero(dynamic_cast<const CGTownInstance *>(obj));
+		if(h->visitedTown) //we are inside, not just attacking
+		{
+			townVisitsThisWeek[h].insert(h->visitedTown);
+			if(!h->hasSpellbook() && ah->freeGold() >= GameConstants::SPELLBOOK_GOLD_COST)
+			{
+				if(h->visitedTown->hasBuilt(BuildingID::MAGES_GUILD_1))
+					cb->buyArtifact(h.get(), ArtifactID::SPELLBOOK);
+			}
+		}
+		break;
+	}
+	completeGoal(sptr(Goals::VisitObj(obj->id.getNum()).sethero(h)));
+}
+
+void VCAI::moveCreaturesToHero(const CGTownInstance * t)
+{
+	if(t->visitingHero && t->armedGarrison() && t->visitingHero->tempOwner == t->tempOwner)
+	{
+		pickBestCreatures(t->visitingHero, t);
+	}
+}
+
+void VCAI::pickBestCreatures(const CArmedInstance * destinationArmy, const CArmedInstance * source)
+{
+	const CArmedInstance * armies[] = {destinationArmy, source};
+
+	auto bestArmy = ah->getSortedSlots(destinationArmy, source);
+
+	//foreach best type -> iterate over slots in both armies and if it's the appropriate type, send it to the slot where it belongs
+	for(SlotID i = SlotID(0); i.getNum() < bestArmy.size() && i.validSlot(); i.advance(1)) //i-th strongest creature type will go to i-th slot
+	{
+		const CCreature * targetCreature = bestArmy[i.getNum()].creature;
+
+		for(auto armyPtr : armies)
+		{
+			for(SlotID j = SlotID(0); j.validSlot(); j.advance(1))
+			{
+				if(armyPtr->getCreature(j) == targetCreature && (i != j || armyPtr != destinationArmy)) //it's a searched creature not in dst SLOT
+				{
+					//can't take away last creature without split. generate a new stack with 1 creature which is weak but fast
+					if(armyPtr == source
+						&& source->needsLastStack()
+						&& source->stacksCount() == 1
+						&& (!destinationArmy->hasStackAtSlot(i) || destinationArmy->getCreature(i) == targetCreature))
+					{
+						auto weakest = ah->getWeakestCreature(bestArmy);
+
+						if(weakest->creature == targetCreature)
+						{
+							if(1 == source->getStackCount(j))
+								break;
+
+							// move all except 1 of weakest creature from source to destination
+							cb->splitStack(
+								source,
+								destinationArmy,
+								j,
+								destinationArmy->getSlotFor(targetCreature),
+								destinationArmy->getStackCount(i) + source->getStackCount(j) - 1);
+
+							break;
+						}
+						else
+						{
+							// Source last stack is not weakest. Move 1 of weakest creature from destination to source
+							cb->splitStack(
+								destinationArmy,
+								source,
+								destinationArmy->getSlotFor(weakest->creature),
+								source->getFreeSlot(),
+								1);
+						}
+					}
+
+					cb->mergeOrSwapStacks(armyPtr, destinationArmy, j, i);
+				}
+			}
+		}
+	}
+
+	//TODO - having now strongest possible army, we may want to think about arranging stacks
+
+	auto hero = dynamic_cast<const CGHeroInstance *>(destinationArmy);
+	if(hero)
+		checkHeroArmy(hero);
+}
+
+void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * other)
+{
+	auto equipBest = [](const CGHeroInstance * h, const CGHeroInstance * otherh, bool giveStuffToFirstHero) -> void
+	{
+		bool changeMade = false;
+		do
+		{
+			changeMade = false;
+
+			//we collect gear always in same order
+			std::vector<ArtifactLocation> allArtifacts;
+			if(giveStuffToFirstHero)
+			{
+				for(auto p : h->artifactsWorn)
+				{
+					if(p.second.artifact)
+						allArtifacts.push_back(ArtifactLocation(h, p.first));
+				}
+			}
+			for(auto slot : h->artifactsInBackpack)
+				allArtifacts.push_back(ArtifactLocation(h, h->getArtPos(slot.artifact)));
+
+			if(otherh)
+			{
+				for(auto p : otherh->artifactsWorn)
+				{
+					if(p.second.artifact)
+						allArtifacts.push_back(ArtifactLocation(otherh, p.first));
+				}
+				for(auto slot : otherh->artifactsInBackpack)
+					allArtifacts.push_back(ArtifactLocation(otherh, otherh->getArtPos(slot.artifact)));
+			}
+			//we give stuff to one hero or another, depending on giveStuffToFirstHero
+
+			const CGHeroInstance * target = nullptr;
+			if(giveStuffToFirstHero || !otherh)
+				target = h;
+			else
+				target = otherh;
+
+			for(auto location : allArtifacts)
+			{
+				if(location.slot == ArtifactPosition::MACH4 || location.slot == ArtifactPosition::SPELLBOOK)
+					continue; // don't attempt to move catapult and spellbook
+
+				if(location.relatedObj() == target && location.slot < ArtifactPosition::AFTER_LAST)
+					continue; //don't reequip artifact we already wear
+
+				auto s = location.getSlot();
+				if(!s || s->locked) //we can't move locks
+					continue;
+				auto artifact = s->artifact;
+				if(!artifact)
+					continue;
+				//FIXME: why are the above possible to be null?
+
+				bool emptySlotFound = false;
+				for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType()))
+				{
+					ArtifactLocation destLocation(target, slot);
+					if(target->isPositionFree(slot) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move
+					{
+						cb->swapArtifacts(location, destLocation); //just put into empty slot
+						emptySlotFound = true;
+						changeMade = true;
+						break;
+					}
+				}
+				if(!emptySlotFound) //try to put that atifact in already occupied slot
+				{
+					for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType()))
+					{
+						auto otherSlot = target->getSlot(slot);
+						if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one
+						{
+							ArtifactLocation destLocation(target, slot);
+							//if that artifact is better than what we have, pick it
+							if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move
+							{
+								cb->swapArtifacts(location, ArtifactLocation(target, target->getArtPos(otherSlot->artifact)));
+								changeMade = true;
+								break;
+							}
+						}
+					}
+				}
+				if(changeMade)
+					break; //start evaluating artifacts from scratch
+			}
+		}
+		while(changeMade);
+	};
+
+	equipBest(h, other, true);
+
+	if(other)
+		equipBest(h, other, false);
+}
+
+void VCAI::recruitCreatures(const CGDwelling * d, const CArmedInstance * recruiter)
+{
+	//now used only for visited dwellings / towns, not BuyArmy goal
+	for(int i = 0; i < d->creatures.size(); i++)
+	{
+		if(!d->creatures[i].second.size())
+			continue;
+
+		int count = d->creatures[i].first;
+		CreatureID creID = d->creatures[i].second.back();
+
+		vstd::amin(count, ah->freeResources() / VLC->creatures()->getById(creID)->getFullRecruitCost());
+		if(count > 0)
+			cb->recruitCreatures(d, recruiter, creID, count, i);
+	}
+}
+
+bool VCAI::isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, std::optional<float> movementCostLimit)
+{
+	int3 op = obj->visitablePos();
+	auto paths = ah->getPathsToTile(h, op);
+
+	for(const auto & path : paths)
+	{
+		if(movementCostLimit && movementCostLimit.value() < path.movementCost())
+			return false;
+
+		if(isGoodForVisit(obj, h, path))
+			return true;
+	}
+
+	return false;
+}
+
+bool VCAI::isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, const AIPath & path) const
+{
+	const int3 pos = obj->visitablePos();
+	const int3 targetPos = path.firstTileToGet();
+	if (!targetPos.valid())
+		return false;
+	if (!isTileNotReserved(h.get(), targetPos))
+		return false;
+	if (obj->wasVisited(playerID))
+		return false;
+	if (cb->getPlayerRelations(playerID, obj->tempOwner) != PlayerRelations::ENEMIES && !isWeeklyRevisitable(obj))
+		return false; // Otherwise we flag or get weekly resources / creatures
+	if (!isSafeToVisit(h, pos))
+		return false;
+	if (!shouldVisit(h, obj))
+		return false;
+	if (vstd::contains(alreadyVisited, obj))
+		return false;
+	if (vstd::contains(reservedObjs, obj))
+		return false;
+
+	// TODO: looks extra if we already have AIPath
+	//if (!isAccessibleForHero(targetPos, h))
+	//	return false;
+
+	const CGObjectInstance * topObj = cb->getVisitableObjs(obj->visitablePos()).back(); //it may be hero visiting this obj
+																						//we don't try visiting object on which allied or owned hero stands
+																						// -> it will just trigger exchange windows and AI will be confused that obj behind doesn't get visited
+	return !(topObj->ID == Obj::HERO && cb->getPlayerRelations(h->tempOwner, topObj->tempOwner) != PlayerRelations::ENEMIES); //all of the following is met
+}
+
+bool VCAI::isTileNotReserved(const CGHeroInstance * h, int3 t) const
+{
+	if(t.valid())
+	{
+		auto obj = cb->getTopObj(t);
+		if(obj && vstd::contains(ai->reservedObjs, obj)
+			&& vstd::contains(reservedHeroesMap, h)
+			&& !vstd::contains(reservedHeroesMap.at(h), obj))
+			return false; //do not capture object reserved by another hero
+		else
+			return true;
+	}
+	else
+	{
+		return false;
+	}
+}
+
+bool VCAI::canRecruitAnyHero(const CGTownInstance * t) const
+{
+	//TODO: make gathering gold, building tavern or conquering town (?) possible subgoals
+	if(!t)
+		t = findTownWithTavern();
+	if(!t)
+		return false;
+	if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //TODO: use ResourceManager
+		return false;
+	if(cb->getHeroesInfo().size() >= ALLOWED_ROAMING_HEROES)
+		return false;
+	if(cb->getHeroesInfo().size() >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
+		return false;
+	if(!cb->getAvailableHeroes(t).size())
+		return false;
+
+	return true;
+}
+
+void VCAI::wander(HeroPtr h)
+{
+	auto visitTownIfAny = [this](HeroPtr h) -> bool
+	{
+		if (h->visitedTown)
+		{
+			townVisitsThisWeek[h].insert(h->visitedTown);
+			buildArmyIn(h->visitedTown);
+			return true;
+		}
+		return false;
+	};
+
+	//unclaim objects that are now dangerous for us
+	auto reservedObjsSetCopy = reservedHeroesMap[h];
+	for(auto obj : reservedObjsSetCopy)
+	{
+		if(!isSafeToVisit(h, obj->visitablePos()))
+			unreserveObject(h, obj);
+	}
+
+	TimeCheck tc("looking for wander destination");
+
+	for(int k = 0; k < 10 && h->movementPointsRemaining(); k++)
+	{
+		validateVisitableObjs();
+		ah->updatePaths(getMyHeroes());
+
+		std::vector<ObjectIdRef> dests;
+
+		//also visit our reserved objects - but they are not prioritized to avoid running back and forth
+		vstd::copy_if(reservedHeroesMap[h], std::back_inserter(dests), [&](ObjectIdRef obj) -> bool
+		{
+			return ah->isTileAccessible(h, obj->visitablePos());
+		});
+
+		int pass = 0;
+		std::vector<std::optional<float>> distanceLimits = {1.0, 2.0, std::nullopt};
+
+		while(!dests.size() && pass < distanceLimits.size())
+		{
+			auto & distanceLimit = distanceLimits[pass];
+
+			logAi->debug("Looking for wander destination pass=%i, cost limit=%f", pass, distanceLimit.value_or(-1.0));
+
+			vstd::copy_if(visitableObjs, std::back_inserter(dests), [&](ObjectIdRef obj) -> bool
+			{
+				return isGoodForVisit(obj, h, distanceLimit);
+			});
+
+			pass++;
+		}
+
+		if(!dests.size())
+		{
+			logAi->debug("Looking for town destination");
+
+			if(cb->getVisitableObjs(h->visitablePos()).size() > 1)
+				moveHeroToTile(h->visitablePos(), h); //just in case we're standing on blocked subterranean gate
+
+			auto compareReinforcements = [&](const CGTownInstance * lhs, const CGTownInstance * rhs) -> bool
+			{
+				const CGHeroInstance * hptr = h.get();
+				auto r1 = ah->howManyReinforcementsCanGet(hptr, lhs),
+					r2 = ah->howManyReinforcementsCanGet(hptr, rhs);
+				if (r1 != r2)
+					return r1 < r2;
+				else
+					return ah->howManyReinforcementsCanBuy(hptr, lhs) < ah->howManyReinforcementsCanBuy(hptr, rhs);
+			};
+
+			std::vector<const CGTownInstance *> townsReachable;
+			std::vector<const CGTownInstance *> townsNotReachable;
+			for(const CGTownInstance * t : cb->getTownsInfo())
+			{
+				if(!t->visitingHero && !vstd::contains(townVisitsThisWeek[h], t))
+				{
+					if(isAccessibleForHero(t->visitablePos(), h))
+						townsReachable.push_back(t);
+					else
+						townsNotReachable.push_back(t);
+				}
+			}
+			if(townsReachable.size()) //travel to town with largest garrison, or empty - better than nothing
+			{
+				dests.push_back(*boost::max_element(townsReachable, compareReinforcements));
+			}
+			else if(townsNotReachable.size())
+			{
+				//TODO pick the truly best
+				const CGTownInstance * t = *boost::max_element(townsNotReachable, compareReinforcements);
+				logAi->debug("%s can't reach any town, we'll try to make our way to %s at %s", h->getNameTranslated(), t->getNameTranslated(), t->visitablePos().toString());
+				int3 pos1 = h->pos;
+				striveToGoal(sptr(Goals::ClearWayTo(t->visitablePos()).sethero(h))); //TODO: drop "strive", add to mainLoop
+				//if out hero is stuck, we may need to request another hero to clear the way we see
+
+				if(pos1 == h->pos && h == primaryHero()) //hero can't move
+				{
+					if(canRecruitAnyHero(t))
+						recruitHero(t);
+				}
+				break;
+			}
+			else if(cb->getResourceAmount(EGameResID::GOLD) >= GameConstants::HERO_GOLD_COST)
+			{
+				std::vector<const CGTownInstance *> towns = cb->getTownsInfo();
+				vstd::erase_if(towns, [&](const CGTownInstance * t) -> bool
+				{
+					for(const CGHeroInstance * h : cb->getHeroesInfo())
+					{
+						if(!t->getArmyStrength() || ah->howManyReinforcementsCanGet(h, t))
+							return true;
+					}
+					return false;
+				});
+				if (towns.size())
+				{
+					recruitHero(*boost::max_element(towns, compareArmyStrength));
+				}
+				break;
+			}
+			else
+			{
+				logAi->debug("Nowhere more to go...");
+				break;
+			}
+		}
+		//end of objs empty
+
+		if(dests.size()) //performance improvement
+		{
+			Goals::TGoalVec targetObjectGoals;
+			for(auto destination : dests)
+			{
+				vstd::concatenate(targetObjectGoals, ah->howToVisitObj(h, destination, false));
+			}
+
+			if(targetObjectGoals.size())
+			{
+				auto bestObjectGoal = fh->chooseSolution(targetObjectGoals);
+
+				//wander should not cause heroes to be reserved - they are always considered free
+				if(bestObjectGoal->goalType == Goals::VISIT_OBJ)
+				{
+					auto chosenObject = cb->getObjInstance(ObjectInstanceID(bestObjectGoal->objid));
+					if(chosenObject != nullptr)
+						logAi->debug("Of all %d destinations, object %s at pos=%s seems nice", dests.size(), chosenObject->getObjectName(), chosenObject->pos.toString());
+				}
+				else
+					logAi->debug("Trying to realize goal of type %s as part of wandering.", bestObjectGoal->name());
+
+				try
+				{
+					decomposeGoal(bestObjectGoal)->accept(this);
+				}
+				catch(const goalFulfilledException & e)
+				{
+					if(e.goal->goalType == Goals::EGoals::VISIT_TILE || e.goal->goalType == Goals::EGoals::VISIT_OBJ)
+						continue;
+
+					throw e;
+				}
+			}
+			else
+			{
+				logAi->debug("Nowhere more to go...");
+				break;
+			}
+
+			visitTownIfAny(h);
+		}
+	}
+
+	visitTownIfAny(h); //in case hero is just sitting in town
+}
+
+void VCAI::setGoal(HeroPtr h, Goals::TSubgoal goal)
+{
+	if(goal->invalid())
+	{
+		vstd::erase_if_present(lockedHeroes, h);
+	}
+	else
+	{
+		lockedHeroes[h] = goal;
+		goal->setisElementar(false); //Force always evaluate goals before realizing
+	}
+}
+void VCAI::evaluateGoal(HeroPtr h)
+{
+	if(vstd::contains(lockedHeroes, h))
+		fh->setPriority(lockedHeroes[h]);
+}
+
+void VCAI::completeGoal(Goals::TSubgoal goal)
+{
+	if (goal->goalType == Goals::WIN) //we can never complete this goal - unless we already won
+		return;
+
+	logAi->debug("Completing goal: %s", goal->name());
+
+	//notify Managers
+	ah->notifyGoalCompleted(goal);
+	//notify mainLoop()
+	goalsToRemove.push_back(goal); //will be removed from mainLoop() goals
+	for (auto basicGoal : basicGoals) //we could luckily fulfill any of our goals
+	{
+		if (basicGoal->fulfillsMe(goal))
+			goalsToRemove.push_back(basicGoal);
+	}
+
+	//unreserve heroes
+	if(const CGHeroInstance * h = goal->hero.get(true))
+	{
+		auto it = lockedHeroes.find(h);
+		if(it != lockedHeroes.end())
+		{
+			if(it->second == goal || it->second->fulfillsMe(goal)) //FIXME this is overspecified, fulfillsMe shoudl be complete
+			{
+				logAi->debug(goal->completeMessage());
+				lockedHeroes.erase(it); //goal fulfilled, free hero
+			}
+		}
+	}
+	else //complete goal for all heroes maybe?
+	{
+		vstd::erase_if(lockedHeroes, [goal](std::pair<HeroPtr, Goals::TSubgoal> p)
+		{
+			if(p.second == goal || p.second->fulfillsMe(goal)) //we could have fulfilled goals of other heroes by chance
+			{
+				logAi->debug(p.second->completeMessage());
+				return true;
+			}
+			return false;
+		});
+	}
+
+}
+
+void VCAI::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed)
+{
+	NET_EVENT_HANDLER;
+	assert(!playerID.isValidPlayer() || status.getBattle() == UPCOMING_BATTLE);
+	status.setBattle(ONGOING_BATTLE);
+	const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit
+	battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString());
+	CAdventureAI::battleStart(battleID, army1, army2, tile, hero1, hero2, side, replayAllowed);
+}
+
+void VCAI::battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID)
+{
+	NET_EVENT_HANDLER;
+	assert(status.getBattle() == ONGOING_BATTLE);
+	status.setBattle(ENDING_BATTLE);
+	bool won = br->winner == myCb->getBattle(battleID)->battleGetMySide();
+	logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.toString(), (won ? "won" : "lost"), battlename);
+	battlename.clear();
+
+	if (queryID != QueryID::NONE)
+	{
+		status.addQuery(queryID, "Combat result dialog");
+		const int confirmAction = 0;
+		requestActionASAP([=]()
+		{
+			answerQuery(queryID, confirmAction);
+		});
+	}
+	CAdventureAI::battleEnd(battleID, br, queryID);
+}
+
+void VCAI::waitTillFree()
+{
+	auto unlock = vstd::makeUnlockSharedGuard(CGameState::mutex);
+	status.waitTillFree();
+}
+
+void VCAI::markObjectVisited(const CGObjectInstance * obj)
+{
+	if(!obj)
+		return;
+
+	if(const auto * rewardable = dynamic_cast<const CRewardableObject *>(obj)) //we may want to visit it with another hero
+	{
+		if (rewardable->configuration.getVisitMode() == Rewardable::VISIT_HERO) //we may want to visit it with another hero
+			return;
+
+		if (rewardable->configuration.getVisitMode() == Rewardable::VISIT_BONUS) //or another time
+			return;
+	}
+
+	if(obj->ID == Obj::MONSTER)
+		return;
+
+	alreadyVisited.insert(obj);
+}
+
+void VCAI::reserveObject(HeroPtr h, const CGObjectInstance * obj)
+{
+	reservedObjs.insert(obj);
+	reservedHeroesMap[h].insert(obj);
+	logAi->debug("reserved object id=%d; address=%p; name=%s", obj->id, obj, obj->getObjectName());
+}
+
+void VCAI::unreserveObject(HeroPtr h, const CGObjectInstance * obj)
+{
+	vstd::erase_if_present(reservedObjs, obj); //unreserve objects
+	vstd::erase_if_present(reservedHeroesMap[h], obj);
+}
+
+void VCAI::markHeroUnableToExplore(HeroPtr h)
+{
+	heroesUnableToExplore.insert(h);
+}
+void VCAI::markHeroAbleToExplore(HeroPtr h)
+{
+	vstd::erase_if_present(heroesUnableToExplore, h);
+}
+bool VCAI::isAbleToExplore(HeroPtr h)
+{
+	return !vstd::contains(heroesUnableToExplore, h);
+}
+void VCAI::clearPathsInfo()
+{
+	heroesUnableToExplore.clear();
+}
+
+void VCAI::validateVisitableObjs()
+{
+	std::string errorMsg;
+	auto shouldBeErased = [&](const CGObjectInstance * obj) -> bool
+	{
+		if(obj)
+			return !cb->getObj(obj->id, false); // no verbose output needed as we check object visibility
+		else
+			return true;
+	};
+
+	//errorMsg is captured by ref so lambda will take the new text
+	errorMsg = " shouldn't be on the visitable objects list!";
+	vstd::erase_if(visitableObjs, shouldBeErased);
+
+	//FIXME: how comes our own heroes become inaccessible?
+	vstd::erase_if(reservedHeroesMap, [](std::pair<HeroPtr, std::set<const CGObjectInstance *>> hp) -> bool
+	{
+		return !hp.first.get(true);
+	});
+	for(auto & p : reservedHeroesMap)
+	{
+		errorMsg = " shouldn't be on list for hero " + p.first->getNameTranslated() + "!";
+		vstd::erase_if(p.second, shouldBeErased);
+	}
+
+	errorMsg = " shouldn't be on the reserved objs list!";
+	vstd::erase_if(reservedObjs, shouldBeErased);
+
+	//TODO overkill, hidden object should not be removed. However, we can't know if hidden object is erased from game.
+	errorMsg = " shouldn't be on the already visited objs list!";
+	vstd::erase_if(alreadyVisited, shouldBeErased);
+}
+
+void VCAI::retrieveVisitableObjs(std::vector<const CGObjectInstance *> & out, bool includeOwned) const
+{
+	foreach_tile_pos([&](const int3 & pos)
+	{
+		for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false))
+		{
+			if(includeOwned || obj->tempOwner != playerID)
+				out.push_back(obj);
+		}
+	});
+}
+
+void VCAI::retrieveVisitableObjs()
+{
+	foreach_tile_pos([&](const int3 & pos)
+	{
+		for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false))
+		{
+			if(obj->tempOwner != playerID)
+				addVisitableObj(obj);
+		}
+	});
+}
+
+std::vector<const CGObjectInstance *> VCAI::getFlaggedObjects() const
+{
+	std::vector<const CGObjectInstance *> ret;
+	for(const CGObjectInstance * obj : visitableObjs)
+	{
+		if(obj->tempOwner == playerID)
+			ret.push_back(obj);
+	}
+	return ret;
+}
+
+void VCAI::addVisitableObj(const CGObjectInstance * obj)
+{
+	if(obj->ID == Obj::EVENT)
+		return;
+
+	visitableObjs.insert(obj);
+
+	// All teleport objects seen automatically assigned to appropriate channels
+	auto teleportObj = dynamic_cast<const CGTeleport *>(obj);
+	if(teleportObj)
+		CGTeleport::addToChannel(knownTeleportChannels, teleportObj);
+}
+
+const CGObjectInstance * VCAI::lookForArt(int aid) const
+{
+	for(const CGObjectInstance * obj : ai->visitableObjs)
+	{
+		if(obj->ID == Obj::ARTIFACT && obj->subID == aid)
+			return obj;
+	}
+
+	return nullptr;
+
+	//TODO what if more than one artifact is available? return them all or some slection criteria
+}
+
+bool VCAI::isAccessible(const int3 & pos) const
+{
+	//TODO precalculate for speed
+
+	for(const CGHeroInstance * h : cb->getHeroesInfo())
+	{
+		if(isAccessibleForHero(pos, h))
+			return true;
+	}
+
+	return false;
+}
+
+HeroPtr VCAI::getHeroWithGrail() const
+{
+	for(const CGHeroInstance * h : cb->getHeroesInfo())
+	{
+		if(h->hasArt(ArtifactID::GRAIL))
+			return h;
+	}
+	return nullptr;
+}
+
+const CGObjectInstance * VCAI::getUnvisitedObj(const std::function<bool(const CGObjectInstance *)> & predicate)
+{
+	//TODO smarter definition of unvisited
+	for(const CGObjectInstance * obj : visitableObjs)
+	{
+		if(predicate(obj) && !vstd::contains(alreadyVisited, obj))
+			return obj;
+	}
+	return nullptr;
+}
+
+bool VCAI::isAccessibleForHero(const int3 & pos, HeroPtr h, bool includeAllies) const
+{
+	// Don't visit tile occupied by allied hero
+	if(!includeAllies)
+	{
+		for(auto obj : cb->getVisitableObjs(pos))
+		{
+			if(obj->ID == Obj::HERO && cb->getPlayerRelations(ai->playerID, obj->tempOwner) != PlayerRelations::ENEMIES)
+			{
+				if(obj != h.get())
+					return false;
+			}
+		}
+	}
+	return cb->getPathsInfo(h.get())->getPathInfo(pos)->reachable();
+}
+
+bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
+{
+	//TODO: consider if blockVisit objects change something in our checks: AIUtility::isBlockVisitObj()
+
+	auto afterMovementCheck = [&]() -> void
+	{
+		waitTillFree(); //movement may cause battle or blocking dialog
+		if(!h)
+		{
+			lostHero(h);
+			teleportChannelProbingList.clear();
+			if(status.channelProbing()) // if hero lost during channel probing we need to switch this mode off
+				status.setChannelProbing(false);
+			throw cannotFulfillGoalException("Hero was lost!");
+		}
+	};
+
+	logAi->debug("Moving hero %s to tile %s", h->getNameTranslated(), dst.toString());
+	int3 startHpos = h->visitablePos();
+	bool ret = false;
+	if(startHpos == dst)
+	{
+		//FIXME: this assertion fails also if AI moves onto defeated guarded object
+		assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object
+		cb->moveHero(*h, h->convertFromVisitablePos(dst));
+		afterMovementCheck(); // TODO: is it feasible to hero get killed there if game work properly?
+		// If revisiting, teleport probing is never done, and so the entries into the list would remain unused and uncleared
+		teleportChannelProbingList.clear();
+		// not sure if AI can currently reconsider to attack bank while staying on it. Check issue 2084 on mantis for more information.
+		ret = true;
+	}
+	else
+	{
+		CGPath path;
+		cb->getPathsInfo(h.get())->getPath(path, dst);
+		if(path.nodes.empty())
+		{
+			logAi->error("Hero %s cannot reach %s.", h->getNameTranslated(), dst.toString());
+			throw goalFulfilledException(sptr(Goals::VisitTile(dst).sethero(h)));
+		}
+		int i = (int)path.nodes.size() - 1;
+
+		auto getObj = [&](int3 coord, bool ignoreHero)
+		{
+			auto tile = cb->getTile(coord, false);
+			assert(tile);
+			return tile->topVisitableObj(ignoreHero);
+			//return cb->getTile(coord,false)->topVisitableObj(ignoreHero);
+		};
+
+		auto isTeleportAction = [&](EPathNodeAction action) -> bool
+		{
+			if(action != EPathNodeAction::TELEPORT_NORMAL && action != EPathNodeAction::TELEPORT_BLOCKING_VISIT)
+			{
+				if(action != EPathNodeAction::TELEPORT_BATTLE)
+				{
+					return false;
+				}
+			}
+
+			return true;
+		};
+
+		auto getDestTeleportObj = [&](const CGObjectInstance * currentObject, const CGObjectInstance * nextObjectTop, const CGObjectInstance * nextObject) -> const CGObjectInstance *
+		{
+			if(CGTeleport::isConnected(currentObject, nextObjectTop))
+				return nextObjectTop;
+			if(nextObjectTop && nextObjectTop->ID == Obj::HERO)
+			{
+				if(CGTeleport::isConnected(currentObject, nextObject))
+					return nextObject;
+			}
+
+			return nullptr;
+		};
+
+		auto doMovement = [&](int3 dst, bool transit)
+		{
+			cb->moveHero(*h, h->convertFromVisitablePos(dst), transit);
+		};
+
+		auto doTeleportMovement = [&](ObjectInstanceID exitId, int3 exitPos)
+		{
+			destinationTeleport = exitId;
+			if(exitPos.valid())
+				destinationTeleportPos = h->convertFromVisitablePos(exitPos);
+			cb->moveHero(*h, h->pos);
+			destinationTeleport = ObjectInstanceID();
+			destinationTeleportPos = int3(-1);
+			afterMovementCheck();
+		};
+
+		auto doChannelProbing = [&]() -> void
+		{
+			auto currentPos = h->visitablePos();
+			auto currentExit = getObj(currentPos, true)->id;
+
+			status.setChannelProbing(true);
+			for(auto exit : teleportChannelProbingList)
+				doTeleportMovement(exit, int3(-1));
+			teleportChannelProbingList.clear();
+			status.setChannelProbing(false);
+
+			doTeleportMovement(currentExit, currentPos);
+		};
+
+		for(; i > 0; i--)
+		{
+			int3 currentCoord = path.nodes[i].coord;
+			int3 nextCoord = path.nodes[i - 1].coord;
+
+			auto currentObject = getObj(currentCoord, currentCoord == h->visitablePos());
+			auto nextObjectTop = getObj(nextCoord, false);
+			auto nextObject = getObj(nextCoord, true);
+			auto destTeleportObj = getDestTeleportObj(currentObject, nextObjectTop, nextObject);
+			if(isTeleportAction(path.nodes[i - 1].action) && destTeleportObj != nullptr)
+			{
+				//we use special login if hero standing on teleporter it's mean we need
+				doTeleportMovement(destTeleportObj->id, nextCoord);
+				if(teleportChannelProbingList.size())
+					doChannelProbing();
+				markObjectVisited(destTeleportObj); //FIXME: Monoliths are not correctly visited
+
+				continue;
+			}
+
+			//stop sending move requests if the next node can't be reached at the current turn (hero exhausted his move points)
+			if(path.nodes[i - 1].turns)
+			{
+				//blockedHeroes.insert(h); //to avoid attempts of moving heroes with very little MPs
+				break;
+			}
+
+			int3 endpos = path.nodes[i - 1].coord;
+			if(endpos == h->visitablePos())
+				continue;
+
+			bool isConnected = false;
+			bool isNextObjectTeleport = false;
+			// Check there is node after next one; otherwise transit is pointless
+			if(i - 2 >= 0)
+			{
+				isConnected = CGTeleport::isConnected(nextObjectTop, getObj(path.nodes[i - 2].coord, false));
+				isNextObjectTeleport = CGTeleport::isTeleport(nextObjectTop);
+			}
+			if(isConnected || isNextObjectTeleport)
+			{
+				// Hero should be able to go through object if it's allow transit
+				doMovement(endpos, true);
+			}
+			else if(path.nodes[i - 1].layer == EPathfindingLayer::AIR)
+			{
+				doMovement(endpos, true);
+			}
+			else
+			{
+				doMovement(endpos, false);
+			}
+
+			afterMovementCheck();
+
+			if(teleportChannelProbingList.size())
+				doChannelProbing();
+		}
+
+		if(path.nodes[0].action == EPathNodeAction::BLOCKING_VISIT)
+		{
+			ret = h && i == 0; // when we take resource we do not reach its position. We even might not move
+		}
+	}
+	if(h)
+	{
+		if(auto visitedObject = vstd::frontOrNull(cb->getVisitableObjs(h->visitablePos()))) //we stand on something interesting
+		{
+			if(visitedObject != *h)
+				performObjectInteraction(visitedObject, h);
+		}
+	}
+	if(h) //we could have lost hero after last move
+	{
+		completeGoal(sptr(Goals::VisitTile(dst).sethero(h))); //we stepped on some tile, anyway
+		completeGoal(sptr(Goals::ClearWayTo(dst).sethero(h)));
+
+		ret = ret || (dst == h->visitablePos());
+
+		if(!ret) //reserve object we are heading towards
+		{
+			auto obj = vstd::frontOrNull(cb->getVisitableObjs(dst));
+			if(obj && obj != *h)
+				reserveObject(h, obj);
+		}
+
+		if(startHpos == h->visitablePos() && !ret) //we didn't move and didn't reach the target
+		{
+			vstd::erase_if_present(lockedHeroes, h); //hero seemingly is confused or has only 95mp which is not enough to move
+			invalidPathHeroes.insert(h);
+			throw cannotFulfillGoalException("Invalid path found!");
+		}
+		evaluateGoal(h); //new hero position means new game situation
+		logAi->debug("Hero %s moved from %s to %s. Returning %d.", h->getNameTranslated(), startHpos.toString(), h->visitablePos().toString(), ret);
+	}
+	return ret;
+}
+
+void VCAI::buildStructure(const CGTownInstance * t, BuildingID building)
+{
+	auto name = t->town->buildings.at(building)->getNameTranslated();
+	logAi->debug("Player %d will build %s in town of %s at %s", ai->playerID, name, t->getNameTranslated(), t->pos.toString());
+	cb->buildBuilding(t, building); //just do this;
+}
+
+void VCAI::tryRealize(Goals::Explore & g)
+{
+	throw cannotFulfillGoalException("EXPLORE is not an elementar goal!");
+}
+
+void VCAI::tryRealize(Goals::RecruitHero & g)
+{
+	if(const CGTownInstance * t = findTownWithTavern())
+	{
+		recruitHero(t, true);
+		//TODO try to free way to blocked town
+		//TODO: adventure map tavern or prison?
+	}
+	else
+	{
+		throw cannotFulfillGoalException("No town to recruit hero!");
+	}
+}
+
+void VCAI::tryRealize(Goals::VisitTile & g)
+{
+	if(!g.hero->movementPointsRemaining())
+		throw cannotFulfillGoalException("Cannot visit tile: hero is out of MPs!");
+	if(g.tile == g.hero->visitablePos() && cb->getVisitableObjs(g.hero->visitablePos()).size() < 2)
+	{
+		logAi->warn("Why do I want to move hero %s to tile %s? Already standing on that tile! ", g.hero->getNameTranslated(), g.tile.toString());
+		throw goalFulfilledException(sptr(g));
+	}
+	if(ai->moveHeroToTile(g.tile, g.hero.get()))
+	{
+		throw goalFulfilledException(sptr(g));
+	}
+}
+
+void VCAI::tryRealize(Goals::VisitObj & g)
+{
+	auto position = g.tile;
+	if(!g.hero->movementPointsRemaining())
+		throw cannotFulfillGoalException("Cannot visit object: hero is out of MPs!");
+	if(position == g.hero->visitablePos() && cb->getVisitableObjs(g.hero->visitablePos()).size() < 2)
+	{
+		logAi->warn("Why do I want to move hero %s to tile %s? Already standing on that tile! ", g.hero->getNameTranslated(), g.tile.toString());
+		throw goalFulfilledException(sptr(g));
+	}
+	if(ai->moveHeroToTile(position, g.hero.get()))
+	{
+		throw goalFulfilledException(sptr(g));
+	}
+}
+
+void VCAI::tryRealize(Goals::VisitHero & g)
+{
+	if(!g.hero->movementPointsRemaining())
+		throw cannotFulfillGoalException("Cannot visit target hero: hero is out of MPs!");
+
+	const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid));
+	if(obj)
+	{
+		if(ai->moveHeroToTile(obj->visitablePos(), g.hero.get()))
+		{
+			throw goalFulfilledException(sptr(g));
+		}
+	}
+	else
+	{
+		throw cannotFulfillGoalException("Cannot visit hero: object not found!");
+	}
+}
+
+void VCAI::tryRealize(Goals::BuildThis & g)
+{
+	auto b = BuildingID(g.bid);
+	auto t = g.town;
+
+	if (t)
+	{
+		if (cb->canBuildStructure(t, b) == EBuildingState::ALLOWED)
+		{
+			logAi->debug("Player %d will build %s in town of %s at %s",
+				playerID, t->town->buildings.at(b)->getNameTranslated(), t->getNameTranslated(), t->pos.toString());
+			cb->buildBuilding(t, b);
+			throw goalFulfilledException(sptr(g));
+		}
+	}
+	throw cannotFulfillGoalException("Cannot build a given structure!");
+}
+
+void VCAI::tryRealize(Goals::DigAtTile & g)
+{
+	assert(g.hero->visitablePos() == g.tile); //surely we want to crash here?
+	if(g.hero->diggingStatus() == EDiggingStatus::CAN_DIG)
+	{
+		cb->dig(g.hero.get());
+		completeGoal(sptr(g)); // finished digging
+	}
+	else
+	{
+		ai->lockedHeroes[g.hero] = sptr(g); //hero who tries to dig shouldn't do anything else
+		throw cannotFulfillGoalException("A hero can't dig!\n");
+	}
+}
+
+void VCAI::tryRealize(Goals::Trade & g) //trade
+{
+	if(ah->freeResources()[g.resID] >= g.value) //goal is already fulfilled. Why we need this check, anyway?
+		throw goalFulfilledException(sptr(g));
+
+	int accquiredResources = 0;
+	if(const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid), false))
+	{
+		if(const IMarket * m = IMarket::castFrom(obj, false))
+		{
+			auto freeRes = ah->freeResources(); //trade only resources which are not reserved
+			for(auto it = ResourceSet::nziterator(freeRes); it.valid(); it++)
+			{
+				auto res = it->resType;
+				if(res.getNum() == g.resID) //sell any other resource
+					continue;
+
+				int toGive, toGet;
+				m->getOffer(res, g.resID, toGive, toGet, EMarketMode::RESOURCE_RESOURCE);
+				toGive = static_cast<int>(toGive * (it->resVal / toGive)); //round down
+				//TODO trade only as much as needed
+				if (toGive) //don't try to sell 0 resources
+				{
+					cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, g.resID, toGive);
+					accquiredResources = static_cast<int>(toGet * (it->resVal / toGive));
+					logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, accquiredResources, g.resID, obj->getObjectName());
+				}
+				if (ah->freeResources()[g.resID] >= g.value)
+					throw goalFulfilledException(sptr(g)); //we traded all we needed
+			}
+
+			throw cannotFulfillGoalException("I cannot get needed resources by trade!");
+		}
+		else
+		{
+			throw cannotFulfillGoalException("I don't know how to use this object to raise resources!");
+		}
+	}
+	else
+	{
+		throw cannotFulfillGoalException("No object that could be used to raise resources!");
+	}
+}
+
+void VCAI::tryRealize(Goals::BuyArmy & g)
+{
+	auto t = g.town;
+
+	ui64 valueBought = 0;
+	//buy the stacks with largest AI value
+
+	makePossibleUpgrades(t);
+
+	while (valueBought < g.value)
+	{
+		auto res = ah->allResources();
+		std::vector<creInfo> creaturesInDwellings;
+
+		for (int i = 0; i < t->creatures.size(); i++)
+		{
+			auto ci = infoFromDC(t->creatures[i]);
+
+			if(!ci.count
+				|| ci.creID == CreatureID::NONE
+				|| (g.objid != -1 && ci.creID.getNum() != g.objid)
+				|| t->getUpperArmy()->getSlotFor(ci.creID) == SlotID())
+				continue;
+
+			vstd::amin(ci.count, res / ci.cre->getFullRecruitCost()); //max count we can afford
+
+			if(!ci.count)
+				continue;
+
+			ci.level = i; //this is important for Dungeon Summoning Portal
+			creaturesInDwellings.push_back(ci);
+		}
+
+		if (creaturesInDwellings.empty())
+			throw cannotFulfillGoalException("Can't buy any more creatures!");
+
+		creInfo ci =
+			*boost::max_element(creaturesInDwellings, [](const creInfo & lhs, const creInfo & rhs)
+		{
+			//max value of creatures we can buy with our res
+			int value1 = lhs.cre->getAIValue() * lhs.count,
+				value2 = rhs.cre->getAIValue() * rhs.count;
+
+			return value1 < value2;
+		});
+
+
+		cb->recruitCreatures(t, t->getUpperArmy(), ci.creID, ci.count, ci.level);
+		valueBought += ci.count * ci.cre->getAIValue();
+	}
+
+	throw goalFulfilledException(sptr(g)); //we bought as many creatures as we wanted
+}
+
+void VCAI::tryRealize(Goals::Invalid & g)
+{
+	throw cannotFulfillGoalException("I don't know how to fulfill this!");
+}
+
+void VCAI::tryRealize(Goals::AbstractGoal & g)
+{
+	logAi->debug("Attempting realizing goal with code %s", g.name());
+	throw cannotFulfillGoalException("Unknown type of goal !");
+}
+
+const CGTownInstance * VCAI::findTownWithTavern() const
+{
+	for(const CGTownInstance * t : cb->getTownsInfo())
+		if(t->hasBuilt(BuildingID::TAVERN) && !t->visitingHero)
+			return t;
+
+	return nullptr;
+}
+
+Goals::TSubgoal VCAI::getGoal(HeroPtr h) const
+{
+	auto it = lockedHeroes.find(h);
+	if(it != lockedHeroes.end())
+		return it->second;
+	else
+		return sptr(Goals::Invalid());
+}
+
+
+std::vector<HeroPtr> VCAI::getUnblockedHeroes() const
+{
+	std::vector<HeroPtr> ret;
+	for(auto h : cb->getHeroesInfo())
+	{
+		//&& !vstd::contains(lockedHeroes, h)
+		//at this point we assume heroes exhausted their locked goals
+		if(canAct(h))
+			ret.push_back(h);
+	}
+	return ret;
+}
+
+bool VCAI::canAct(HeroPtr h) const
+{
+	auto mission = lockedHeroes.find(h);
+	if(mission != lockedHeroes.end())
+	{
+		//FIXME: I'm afraid there can be other conditions when heroes can act but not move :?
+		if(mission->second->goalType == Goals::DIG_AT_TILE && !mission->second->isElementar)
+			return false;
+	}
+
+	return h->movementPointsRemaining();
+}
+
+HeroPtr VCAI::primaryHero() const
+{
+	auto hs = cb->getHeroesInfo();
+	if (hs.empty())
+		return nullptr;
+	else
+		return *boost::max_element(hs, compareHeroStrength);
+}
+
+void VCAI::endTurn()
+{
+	logAi->info("Player %d (%s) ends turn", playerID, playerID.toString());
+	if(!status.haveTurn())
+	{
+		logAi->error("Not having turn at the end of turn???");
+	}
+	logAi->debug("Resources at the end of turn: %s", cb->getResourceAmount().toString());
+	do
+	{
+		cb->endTurn();
+	}
+	while(status.haveTurn()); //for some reasons, our request may fail -> stop requesting end of turn only after we've received a confirmation that it's over
+
+	logGlobal->info("Player %d (%s) ended turn", playerID, playerID.toString());
+}
+
+void VCAI::striveToGoal(Goals::TSubgoal basicGoal)
+{
+	//TODO: this function is deprecated and should be dropped altogether
+
+	auto goalToDecompose = basicGoal;
+	Goals::TSubgoal elementarGoal = sptr(Goals::Invalid());
+	int maxAbstractGoals = 10;
+	while (!elementarGoal->isElementar && maxAbstractGoals)
+	{
+		try
+		{
+			elementarGoal = decomposeGoal(goalToDecompose);
+		}
+		catch (goalFulfilledException & e)
+		{
+			//it is impossible to continue some goals (like exploration, for example)
+			completeGoal(e.goal); //put in goalsToRemove
+			logAi->debug("Goal %s decomposition failed: goal was completed as much as possible", e.goal->name());
+			return;
+		}
+		catch (std::exception & e)
+		{
+			goalsToRemove.push_back(basicGoal);
+			logAi->debug("Goal %s decomposition failed: %s", basicGoal->name(), e.what());
+			return;
+		}
+		if (elementarGoal->isAbstract) //we can decompose it further
+		{
+			goalsToAdd.push_back(elementarGoal);
+			//decompose further now - this is necesssary if we can't add over 10 goals in the pool
+			goalToDecompose = elementarGoal;
+			//there is a risk of infinite abstract goal loop, though it indicates failed logic
+			maxAbstractGoals--;
+		}
+		else if (elementarGoal->isElementar) //should be
+		{
+			logAi->debug("Found elementar goal %s", elementarGoal->name());
+			ultimateGoalsFromBasic[elementarGoal].push_back(goalToDecompose); //TODO: how about indirect basicGoal?
+			break;
+		}
+		else //should never be here
+			throw cannotFulfillGoalException("Goal %s is neither abstract nor elementar!" + basicGoal->name());
+	}
+
+	//realize best goal
+	if (!elementarGoal->invalid())
+	{
+		logAi->debug("Trying to realize %s (value %2.3f)", elementarGoal->name(), elementarGoal->priority);
+
+		try
+		{
+			boost::this_thread::interruption_point();
+			elementarGoal->accept(this); //visitor pattern
+			boost::this_thread::interruption_point();
+		}
+		catch (boost::thread_interrupted & e)
+		{
+			(void)e;
+			logAi->debug("Player %d: Making turn thread received an interruption!", playerID);
+			throw; //rethrow, we want to truly end this thread
+		}
+		catch (goalFulfilledException & e)
+		{
+			//the sub-goal was completed successfully
+			completeGoal(e.goal);
+			//local goal was also completed
+			completeGoal(elementarGoal);
+		}
+		catch (std::exception & e)
+		{
+			logAi->debug("Failed to realize subgoal of type %s, I will stop.", elementarGoal->name());
+			logAi->debug("The error message was: %s", e.what());
+
+			//erase base goal if we failed to execute decomposed goal
+			for (auto basicGoalToRemove : ultimateGoalsFromBasic[elementarGoal])
+				goalsToRemove.push_back(basicGoalToRemove);
+		}
+	}
+}
+
+Goals::TSubgoal VCAI::decomposeGoal(Goals::TSubgoal ultimateGoal)
+{
+	if(ultimateGoal->isElementar)
+	{
+		logAi->warn("Trying to decompose elementar goal %s", ultimateGoal->name());
+
+		return ultimateGoal;
+	}
+
+	const int searchDepth = 30;
+
+	Goals::TSubgoal goal = ultimateGoal;
+	logAi->debug("Decomposing goal %s", ultimateGoal->name());
+	int maxGoals = searchDepth; //preventing deadlock for mutually dependent goals
+	while (maxGoals)
+	{
+		boost::this_thread::interruption_point();
+
+		goal = goal->whatToDoToAchieve(); //may throw if decomposition fails
+		--maxGoals;
+		if (goal == ultimateGoal) //compare objects by value
+			if (goal->isElementar == ultimateGoal->isElementar)
+				throw cannotFulfillGoalException((boost::format("Goal dependency loop detected for %s!")
+												% ultimateGoal->name()).str());
+		if (goal->isAbstract || goal->isElementar)
+			return goal;
+		else
+			logAi->debug("Considering: %s", goal->name());
+	}
+
+	throw cannotFulfillGoalException("Too many subgoals, don't know what to do");
+}
+
+void VCAI::performTypicalActions()
+{
+	for(auto h : getUnblockedHeroes())
+	{
+		if(!h) //hero might be lost. getUnblockedHeroes() called once on start of turn
+			continue;
+
+		logAi->debug("Hero %s started wandering, MP=%d", h->getNameTranslated(), h->movementPointsRemaining());
+		makePossibleUpgrades(*h);
+		pickBestArtifacts(*h);
+		try
+		{
+			wander(h);
+		}
+		catch(std::exception & e)
+		{
+			logAi->debug("Cannot use this hero anymore, received exception: %s", e.what());
+			continue;
+		}
+	}
+}
+
+void VCAI::buildArmyIn(const CGTownInstance * t)
+{
+	makePossibleUpgrades(t->visitingHero);
+	makePossibleUpgrades(t);
+	recruitCreatures(t, t->getUpperArmy());
+	moveCreaturesToHero(t);
+}
+
+void VCAI::checkHeroArmy(HeroPtr h)
+{
+	auto it = lockedHeroes.find(h);
+	if(it != lockedHeroes.end())
+	{
+		if(it->second->goalType == Goals::GATHER_ARMY && it->second->value <= h->getArmyStrength())
+			completeGoal(sptr(Goals::GatherArmy(it->second->value).sethero(h)));
+	}
+}
+
+void VCAI::recruitHero(const CGTownInstance * t, bool throwing)
+{
+	logAi->debug("Trying to recruit a hero in %s at %s", t->getNameTranslated(), t->visitablePos().toString());
+
+	auto heroes = cb->getAvailableHeroes(t);
+	if(heroes.size())
+	{
+		auto hero = heroes[0];
+		if(heroes.size() >= 2) //makes sense to recruit two heroes with starting amries in first week
+		{
+			if(heroes[1]->getTotalStrength() > hero->getTotalStrength())
+				hero = heroes[1];
+		}
+		cb->recruitHero(t, hero);
+		throw goalFulfilledException(sptr(Goals::RecruitHero().settown(t)));
+	}
+	else if(throwing)
+	{
+		throw cannotFulfillGoalException("No available heroes in tavern in " + t->nodeName());
+	}
+}
+
+void VCAI::finish()
+{
+	//we want to lock to avoid multiple threads from calling makingTurn->join() at same time
+	boost::lock_guard<boost::mutex> multipleCleanupGuard(turnInterruptionMutex);
+	if(makingTurn)
+	{
+		makingTurn->interrupt();
+		makingTurn->join();
+		makingTurn.reset();
+	}
+}
+
+void VCAI::requestActionASAP(std::function<void()> whatToDo)
+{
+	boost::thread newThread([this, whatToDo]()
+	{
+		setThreadName("VCAI::requestActionASAP::whatToDo");
+		SET_GLOBAL_STATE(this);
+		boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
+		whatToDo();
+	});
+
+	newThread.detach();
+}
+
+void VCAI::lostHero(HeroPtr h)
+{
+	logAi->debug("I lost my hero %s. It's best to forget and move on.", h.name);
+
+	vstd::erase_if_present(lockedHeroes, h);
+	for(auto obj : reservedHeroesMap[h])
+	{
+		vstd::erase_if_present(reservedObjs, obj); //unreserve all objects for that hero
+	}
+	vstd::erase_if_present(reservedHeroesMap, h);
+	vstd::erase_if_present(visitedHeroes, h);
+	for (auto heroVec : visitedHeroes)
+	{
+		vstd::erase_if_present(heroVec.second, h);
+	}
+
+	//remove goals with removed hero assigned from main loop
+	vstd::erase_if(ultimateGoalsFromBasic, [&](const std::pair<Goals::TSubgoal, Goals::TGoalVec> & x) -> bool
+	{
+		if(x.first->hero == h)
+			return true;
+		else
+			return false;
+	});
+
+	auto removedHeroGoalPredicate = [&](const Goals::TSubgoal & x) ->bool
+	{
+		if(x->hero == h)
+			return true;
+		else
+			return false;
+	};
+
+	vstd::erase_if(basicGoals, removedHeroGoalPredicate);
+	vstd::erase_if(goalsToAdd, removedHeroGoalPredicate);
+	vstd::erase_if(goalsToRemove, removedHeroGoalPredicate);
+
+	for(auto goal : ultimateGoalsFromBasic)
+		vstd::erase_if(goal.second, removedHeroGoalPredicate);
+}
+
+void VCAI::answerQuery(QueryID queryID, int selection)
+{
+	logAi->debug("I'll answer the query %d giving the choice %d", queryID, selection);
+	if(queryID != QueryID(-1))
+	{
+		cb->selectionMade(selection, queryID);
+	}
+	else
+	{
+		logAi->debug("Since the query ID is %d, the answer won't be sent. This is not a real query!", queryID);
+		//do nothing
+	}
+}
+
+void VCAI::requestSent(const CPackForServer * pack, int requestID)
+{
+	//BNLOG("I have sent request of type %s", typeid(*pack).name());
+	if(auto reply = dynamic_cast<const QueryReply *>(pack))
+	{
+		status.attemptedAnsweringQuery(reply->qid, requestID);
+	}
+}
+
+std::string VCAI::getBattleAIName() const
+{
+	if(settings["server"]["enemyAI"].getType() == JsonNode::JsonType::DATA_STRING)
+		return settings["server"]["enemyAI"].String();
+	else
+		return "BattleAI";
+}
+
+void VCAI::validateObject(const CGObjectInstance * obj)
+{
+	validateObject(obj->id);
+}
+
+void VCAI::validateObject(ObjectIdRef obj)
+{
+	auto matchesId = [&](const CGObjectInstance * hlpObj) -> bool
+	{
+		return hlpObj->id == obj.id;
+	};
+	if(!obj)
+	{
+		vstd::erase_if(visitableObjs, matchesId);
+
+		for(auto & p : reservedHeroesMap)
+			vstd::erase_if(p.second, matchesId);
+
+		vstd::erase_if(reservedObjs, matchesId);
+	}
+}
+
+AIStatus::AIStatus()
+{
+	battle = NO_BATTLE;
+	havingTurn = false;
+	ongoingHeroMovement = false;
+	ongoingChannelProbing = false;
+}
+
+AIStatus::~AIStatus()
+{
+
+}
+
+void AIStatus::setBattle(BattleState BS)
+{
+	boost::unique_lock<boost::mutex> lock(mx);
+	LOG_TRACE_PARAMS(logAi, "battle state=%d", (int)BS);
+	battle = BS;
+	cv.notify_all();
+}
+
+BattleState AIStatus::getBattle()
+{
+	boost::unique_lock<boost::mutex> lock(mx);
+	return battle;
+}
+
+void AIStatus::addQuery(QueryID ID, std::string description)
+{
+	if(ID == QueryID(-1))
+	{
+		logAi->debug("The \"query\" has an id %d, it'll be ignored as non-query. Description: %s", ID, description);
+		return;
+	}
+
+	assert(ID.getNum() >= 0);
+	boost::unique_lock<boost::mutex> lock(mx);
+
+	assert(!vstd::contains(remainingQueries, ID));
+
+	remainingQueries[ID] = description;
+
+	cv.notify_all();
+	logAi->debug("Adding query %d - %s. Total queries count: %d", ID, description, remainingQueries.size());
+}
+
+void AIStatus::removeQuery(QueryID ID)
+{
+	boost::unique_lock<boost::mutex> lock(mx);
+	assert(vstd::contains(remainingQueries, ID));
+
+	std::string description = remainingQueries[ID];
+	remainingQueries.erase(ID);
+
+	cv.notify_all();
+	logAi->debug("Removing query %d - %s. Total queries count: %d", ID, description, remainingQueries.size());
+}
+
+int AIStatus::getQueriesCount()
+{
+	boost::unique_lock<boost::mutex> lock(mx);
+	return static_cast<int>(remainingQueries.size());
+}
+
+void AIStatus::startedTurn()
+{
+	boost::unique_lock<boost::mutex> lock(mx);
+	havingTurn = true;
+	cv.notify_all();
+}
+
+void AIStatus::madeTurn()
+{
+	boost::unique_lock<boost::mutex> lock(mx);
+	havingTurn = false;
+	cv.notify_all();
+}
+
+void AIStatus::waitTillFree()
+{
+	boost::unique_lock<boost::mutex> lock(mx);
+	while(battle != NO_BATTLE || !remainingQueries.empty() || !objectsBeingVisited.empty() || ongoingHeroMovement)
+		cv.wait_for(lock, boost::chrono::milliseconds(100));
+}
+
+bool AIStatus::haveTurn()
+{
+	boost::unique_lock<boost::mutex> lock(mx);
+	return havingTurn;
+}
+
+void AIStatus::attemptedAnsweringQuery(QueryID queryID, int answerRequestID)
+{
+	boost::unique_lock<boost::mutex> lock(mx);
+	assert(vstd::contains(remainingQueries, queryID));
+	std::string description = remainingQueries[queryID];
+	logAi->debug("Attempted answering query %d - %s. Request id=%d. Waiting for results...", queryID, description, answerRequestID);
+	requestToQueryID[answerRequestID] = queryID;
+}
+
+void AIStatus::receivedAnswerConfirmation(int answerRequestID, int result)
+{
+	QueryID query;
+
+	{
+		boost::unique_lock<boost::mutex> lock(mx);
+
+		assert(vstd::contains(requestToQueryID, answerRequestID));
+		query = requestToQueryID[answerRequestID];
+		assert(vstd::contains(remainingQueries, query));
+		requestToQueryID.erase(answerRequestID);
+	}
+
+	if(result)
+	{
+		removeQuery(query);
+	}
+	else
+	{
+		logAi->error("Something went really wrong, failed to answer query %d : %s", query.getNum(), remainingQueries[query]);
+		//TODO safely retry
+	}
+}
+
+void AIStatus::heroVisit(const CGObjectInstance * obj, bool started)
+{
+	boost::unique_lock<boost::mutex> lock(mx);
+	if(started)
+	{
+		objectsBeingVisited.push_back(obj);
+	}
+	else
+	{
+		// There can be more than one object visited at the time (eg. hero visits Subterranean Gate
+		// causing visit to hero on the other side.
+		// However, we are guaranteed that start/end visit notification maintain stack order.
+		assert(!objectsBeingVisited.empty());
+		objectsBeingVisited.pop_back();
+	}
+	cv.notify_all();
+}
+
+void AIStatus::setMove(bool ongoing)
+{
+	boost::unique_lock<boost::mutex> lock(mx);
+	ongoingHeroMovement = ongoing;
+	cv.notify_all();
+}
+
+void AIStatus::setChannelProbing(bool ongoing)
+{
+	boost::unique_lock<boost::mutex> lock(mx);
+	ongoingChannelProbing = ongoing;
+	cv.notify_all();
+}
+
+bool AIStatus::channelProbing()
+{
+	return ongoingChannelProbing;
+}
+
+
+
+bool isWeeklyRevisitable(const CGObjectInstance * obj)
+{
+	//TODO: allow polling of remaining creatures in dwelling
+	if(const auto * rewardable = dynamic_cast<const CRewardableObject *>(obj))
+		return rewardable->configuration.getResetDuration() == 7;
+
+	if(dynamic_cast<const CGDwelling *>(obj))
+		return true;
+	if(dynamic_cast<const CBank *>(obj)) //banks tend to respawn often in mods
+		return true;
+
+	switch(obj->ID)
+	{
+	case Obj::STABLES:
+	case Obj::MAGIC_WELL:
+	case Obj::HILL_FORT:
+		return true;
+	case Obj::BORDER_GATE:
+	case Obj::BORDERGUARD:
+		return (dynamic_cast<const CGKeys *>(obj))->wasMyColorVisited(ai->playerID); //FIXME: they could be revisited sooner than in a week
+	}
+	return false;
+}
+
+bool shouldVisit(HeroPtr h, const CGObjectInstance * obj)
+{
+	switch(obj->ID)
+	{
+	case Obj::TOWN:
+	case Obj::HERO: //never visit our heroes at random
+		return obj->tempOwner != h->tempOwner; //do not visit our towns at random
+	case Obj::BORDER_GATE:
+	{
+		for(auto q : ai->myCb->getMyQuests())
+		{
+			if(q.obj == obj)
+			{
+				return false; // do not visit guards or gates when wandering
+			}
+		}
+		return true; //we don't have this quest yet
+	}
+	case Obj::BORDERGUARD: //open borderguard if possible
+		return (dynamic_cast<const CGKeys *>(obj))->wasMyColorVisited(ai->playerID);
+	case Obj::SEER_HUT:
+	case Obj::QUEST_GUARD:
+	{
+		for(auto q : ai->myCb->getMyQuests())
+		{
+			if(q.obj == obj)
+			{
+				if(q.quest->checkQuest(h.h))
+					return true; //we completed the quest
+				else
+					return false; //we can't complete this quest
+			}
+		}
+		return true; //we don't have this quest yet
+	}
+	case Obj::CREATURE_GENERATOR1:
+	{
+		if(obj->tempOwner != h->tempOwner)
+			return true; //flag just in case
+		bool canRecruitCreatures = false;
+		const CGDwelling * d = dynamic_cast<const CGDwelling *>(obj);
+		for(auto level : d->creatures)
+		{
+			for(auto c : level.second)
+			{
+				if(h->getSlotFor(CreatureID(c)) != SlotID())
+					canRecruitCreatures = true;
+			}
+		}
+		return canRecruitCreatures;
+	}
+	case Obj::HILL_FORT:
+	{
+		for(auto slot : h->Slots())
+		{
+			if(slot.second->type->hasUpgrades())
+				return true; //TODO: check price?
+		}
+		return false;
+	}
+	case Obj::MONOLITH_ONE_WAY_ENTRANCE:
+	case Obj::MONOLITH_ONE_WAY_EXIT:
+	case Obj::MONOLITH_TWO_WAY:
+	case Obj::WHIRLPOOL:
+		return false;
+	case Obj::SCHOOL_OF_MAGIC:
+	case Obj::SCHOOL_OF_WAR:
+	{
+		if (ai->ah->freeGold() < 1000)
+			return false;
+		break;
+	}
+	case Obj::LIBRARY_OF_ENLIGHTENMENT:
+		if(h->level < 12)
+			return false;
+		break;
+	case Obj::TREE_OF_KNOWLEDGE:
+	{
+		TResources myRes = ai->ah->freeResources();
+		if(myRes[EGameResID::GOLD] < 2000 || myRes[EGameResID::GEMS] < 10)
+			return false;
+		break;
+	}
+	case Obj::MAGIC_WELL:
+		return h->mana < h->manaLimit();
+	case Obj::PRISON:
+		return ai->myCb->getHeroesInfo().size() < VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP);
+	case Obj::TAVERN:
+	{
+		//TODO: make AI actually recruit heroes
+		//TODO: only on request
+		if(ai->myCb->getHeroesInfo().size() >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
+			return false;
+		else if(ai->ah->freeGold() < GameConstants::HERO_GOLD_COST)
+			return false;
+		break;
+	}
+	case Obj::BOAT:
+		return false;
+	//Boats are handled by pathfinder
+	case Obj::EYE_OF_MAGI:
+		return false; //this object is useless to visit, but could be visited indefinitely
+	}
+
+	if(obj->wasVisited(*h)) //it must pointer to hero instance, heroPtr calls function wasVisited(ui8 player);
+		return false;
+
+	return true;
+}
+
+std::optional<BattleAction> VCAI::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState)
+{
+	return std::nullopt;
+}

+ 407 - 407
AI/VCAI/VCAI.h

@@ -1,407 +1,407 @@
-/*
- * VCAI.h, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-#pragma once
-
-#include "AIUtility.h"
-#include "Goals/AbstractGoal.h"
-#include "../../lib/AI_Base.h"
-#include "../../CCallback.h"
-
-#include "../../lib/CThreadHelper.h"
-
-#include "../../lib/GameConstants.h"
-#include "../../lib/VCMI_Lib.h"
-#include "../../lib/CBuildingHandler.h"
-#include "../../lib/CCreatureHandler.h"
-#include "../../lib/CTownHandler.h"
-#include "../../lib/mapObjects/MiscObjects.h"
-#include "../../lib/spells/CSpellHandler.h"
-#include "../../lib/CondSh.h"
-#include "Pathfinding/AIPathfinder.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-struct QuestInfo;
-
-VCMI_LIB_NAMESPACE_END
-
-class AIhelper;
-
-class AIStatus
-{
-	boost::mutex mx;
-	boost::condition_variable cv;
-
-	BattleState battle;
-	std::map<QueryID, std::string> remainingQueries;
-	std::map<int, QueryID> requestToQueryID; //IDs of answer-requests sent to server => query ids (so we can match answer confirmation from server to the query)
-	std::vector<const CGObjectInstance *> objectsBeingVisited;
-	bool ongoingHeroMovement;
-	bool ongoingChannelProbing; // true if AI currently explore bidirectional teleport channel exits
-
-	bool havingTurn;
-
-public:
-	AIStatus();
-	~AIStatus();
-	void setBattle(BattleState BS);
-	void setMove(bool ongoing);
-	void setChannelProbing(bool ongoing);
-	bool channelProbing();
-	BattleState getBattle();
-	void addQuery(QueryID ID, std::string description);
-	void removeQuery(QueryID ID);
-	int getQueriesCount();
-	void startedTurn();
-	void madeTurn();
-	void waitTillFree();
-	bool haveTurn();
-	void attemptedAnsweringQuery(QueryID queryID, int answerRequestID);
-	void receivedAnswerConfirmation(int answerRequestID, int result);
-	void heroVisit(const CGObjectInstance * obj, bool started);
-
-
-	template<typename Handler> void serialize(Handler & h, const int version)
-	{
-		h & battle;
-		h & remainingQueries;
-		h & requestToQueryID;
-		h & havingTurn;
-	}
-};
-
-class DLL_EXPORT VCAI : public CAdventureAI
-{
-public:
-
-	friend class FuzzyHelper;
-	friend class ResourceManager;
-	friend class BuildingManager;
-
-	std::map<TeleportChannelID, std::shared_ptr<TeleportChannel>> knownTeleportChannels;
-	std::map<const CGObjectInstance *, const CGObjectInstance *> knownSubterraneanGates;
-	ObjectInstanceID destinationTeleport;
-	int3 destinationTeleportPos;
-	std::vector<ObjectInstanceID> teleportChannelProbingList; //list of teleport channel exits that not visible and need to be (re-)explored
-	//std::vector<const CGObjectInstance *> visitedThisWeek; //only OPWs
-	std::map<HeroPtr, std::set<const CGTownInstance *>> townVisitsThisWeek;
-
-	//part of mainLoop, but accessible from outisde
-	std::vector<Goals::TSubgoal> basicGoals;
-	Goals::TGoalVec goalsToRemove;
-	Goals::TGoalVec goalsToAdd;
-	std::map<Goals::TSubgoal, Goals::TGoalVec> ultimateGoalsFromBasic; //theoreticlaly same goal can fulfill multiple basic goals
-
-	std::set<HeroPtr> invalidPathHeroes; //FIXME, just a workaround
-	std::map<HeroPtr, Goals::TSubgoal> lockedHeroes; //TODO: allow non-elementar objectives
-	std::map<HeroPtr, std::set<const CGObjectInstance *>> reservedHeroesMap; //objects reserved by specific heroes
-	std::set<HeroPtr> heroesUnableToExplore; //these heroes will not be polled for exploration in current state of game
-
-	//sets are faster to search, also do not contain duplicates
-	std::set<const CGObjectInstance *> visitableObjs;
-	std::set<const CGObjectInstance *> alreadyVisited;
-	std::set<const CGObjectInstance *> reservedObjs; //to be visited by specific hero
-	std::map<HeroPtr, std::set<HeroPtr>> visitedHeroes; //visited this turn //FIXME: this is just bug workaround
-
-	AIStatus status;
-	std::string battlename;
-
-	std::shared_ptr<CCallback> myCb;
-
-	std::unique_ptr<boost::thread> makingTurn;
-private:
-	boost::mutex turnInterruptionMutex;
-public:
-	ObjectInstanceID selectedObject;
-
-	AIhelper * ah;
-
-	VCAI();
-	virtual ~VCAI();
-
-	//TODO: use only smart pointers?
-	void tryRealize(Goals::Explore & g);
-	void tryRealize(Goals::RecruitHero & g);
-	void tryRealize(Goals::VisitTile & g);
-	void tryRealize(Goals::VisitObj & g);
-	void tryRealize(Goals::VisitHero & g);
-	void tryRealize(Goals::BuildThis & g);
-	void tryRealize(Goals::DigAtTile & g);
-	void tryRealize(Goals::Trade & g);
-	void tryRealize(Goals::BuyArmy & g);
-	void tryRealize(Goals::Invalid & g);
-	void tryRealize(Goals::AbstractGoal & g);
-
-	bool isTileNotReserved(const CGHeroInstance * h, int3 t) const; //the tile is not occupied by allied hero and the object is not reserved
-
-	std::string getBattleAIName() const override;
-
-	void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
-	void yourTurn(QueryID queryID) override;
-
-	void heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector<SecondarySkill> & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id
-	void commanderGotLevel(const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID) override; //TODO
-	void showBlockingDialog(const std::string & text, const std::vector<Component> & components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID.
-	void showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) override; //all stacks operations between these objects become allowed, interface has to call onEnd when done
-	void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override;
-	void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects) override;
-	void saveGame(BinarySerializer & h, const int version) override; //saving
-	void loadGame(BinaryDeserializer & h, const int version) override; //loading
-	void finish() override;
-
-	void availableCreaturesChanged(const CGDwelling * town) override;
-	void heroMoved(const TryMoveHero & details, bool verbose = true) override;
-	void heroInGarrisonChange(const CGTownInstance * town) override;
-	void centerView(int3 pos, int focusTime) override;
-	void tileHidden(const std::unordered_set<int3> & pos) override;
-	void artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) override;
-	void artifactAssembled(const ArtifactLocation & al) override;
-	void showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) override;
-	void showThievesGuildWindow(const CGObjectInstance * obj) override;
-	void playerBlocked(int reason, bool start) override;
-	void showPuzzleMap() override;
-	void showShipyardDialog(const IShipyard * obj) override;
-	void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) override;
-	void artifactPut(const ArtifactLocation & al) override;
-	void artifactRemoved(const ArtifactLocation & al) override;
-	void artifactDisassembled(const ArtifactLocation & al) override;
-	void heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) override;
-	void availableArtifactsChanged(const CGBlackMarket * bm = nullptr) override;
-	void heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) override;
-	void tileRevealed(const std::unordered_set<int3> & pos) override;
-	void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override;
-	void heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) override;
-	void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level, QueryID queryID) override;
-	void heroMovePointsChanged(const CGHeroInstance * hero) override;
-	void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override;
-	void newObject(const CGObjectInstance * obj) override;
-	void showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor) override;
-	void playerBonusChanged(const Bonus & bonus, bool gain) override;
-	void heroCreated(const CGHeroInstance *) override;
-	void advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) override;
-	void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector<Component> & components, int soundID) override;
-	void requestRealized(PackageApplied * pa) override;
-	void receivedResource() override;
-	void objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) override;
-	void showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override;
-	void heroManaPointsChanged(const CGHeroInstance * hero) override;
-	void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override;
-	void battleResultsApplied() override;
-	void beforeObjectPropertyChanged(const SetObjectProperty * sop) override;
-	void objectPropertyChanged(const SetObjectProperty * sop) override;
-	void buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) override;
-	void heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) override;
-	void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override;
-	void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
-
-	void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override;
-	void battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) override;
-	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override;
-
-	void makeTurn();
-	void mainLoop();
-	void performTypicalActions();
-
-	void buildArmyIn(const CGTownInstance * t);
-	void striveToGoal(Goals::TSubgoal ultimateGoal);
-	Goals::TSubgoal decomposeGoal(Goals::TSubgoal ultimateGoal);
-	void endTurn();
-	void wander(HeroPtr h);
-	void setGoal(HeroPtr h, Goals::TSubgoal goal);
-	void evaluateGoal(HeroPtr h); //evaluates goal assigned to hero, if any
-	void completeGoal(Goals::TSubgoal goal); //safely removes goal from reserved hero
-
-	void recruitHero(const CGTownInstance * t, bool throwing = false);
-	bool isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, std::optional<float> movementCostLimit = std::nullopt);
-	bool isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, const AIPath & path) const;
-	//void recruitCreatures(const CGTownInstance * t);
-	void recruitCreatures(const CGDwelling * d, const CArmedInstance * recruiter);
-	void pickBestCreatures(const CArmedInstance * army, const CArmedInstance * source); //called when we can't find a slot for new stack
-	void pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * other = nullptr);
-	void moveCreaturesToHero(const CGTownInstance * t);
-	void performObjectInteraction(const CGObjectInstance * obj, HeroPtr h);
-
-	bool moveHeroToTile(int3 dst, HeroPtr h);
-	void buildStructure(const CGTownInstance * t, BuildingID building); //TODO: move to BuildingManager
-
-	void lostHero(HeroPtr h); //should remove all references to hero (assigned tasks and so on)
-	void waitTillFree();
-
-	void addVisitableObj(const CGObjectInstance * obj);
-	void markObjectVisited(const CGObjectInstance * obj);
-	void reserveObject(HeroPtr h, const CGObjectInstance * obj); //TODO: reserve all objects that heroes attempt to visit
-	void unreserveObject(HeroPtr h, const CGObjectInstance * obj);
-
-	void markHeroUnableToExplore(HeroPtr h);
-	void markHeroAbleToExplore(HeroPtr h);
-	bool isAbleToExplore(HeroPtr h);
-	void clearPathsInfo();
-
-	void validateObject(const CGObjectInstance * obj); //checks if object is still visible and if not, removes references to it
-	void validateObject(ObjectIdRef obj); //checks if object is still visible and if not, removes references to it
-	void validateVisitableObjs();
-	void retrieveVisitableObjs(std::vector<const CGObjectInstance *> & out, bool includeOwned = false) const;
-	void retrieveVisitableObjs();
-	virtual std::vector<const CGObjectInstance *> getFlaggedObjects() const;
-
-	const CGObjectInstance * lookForArt(int aid) const;
-	bool isAccessible(const int3 & pos) const;
-	HeroPtr getHeroWithGrail() const;
-
-	const CGObjectInstance * getUnvisitedObj(const std::function<bool(const CGObjectInstance *)> & predicate);
-	bool isAccessibleForHero(const int3 & pos, HeroPtr h, bool includeAllies = false) const;
-	//optimization - use one SM for every hero call
-
-	const CGTownInstance * findTownWithTavern() const;
-	bool canRecruitAnyHero(const CGTownInstance * t = NULL) const;
-
-	Goals::TSubgoal getGoal(HeroPtr h) const;
-	bool canAct(HeroPtr h) const;
-	std::vector<HeroPtr> getUnblockedHeroes() const;
-	std::vector<HeroPtr> getMyHeroes() const;
-	HeroPtr primaryHero() const;
-	void checkHeroArmy(HeroPtr h);
-
-	void requestSent(const CPackForServer * pack, int requestID) override;
-	void answerQuery(QueryID queryID, int selection);
-	//special function that can be called ONLY from game events handling thread and will send request ASAP
-	void requestActionASAP(std::function<void()> whatToDo);
-
-	#if 0
-	//disabled due to issue 2890
-	template<typename Handler> void registerGoals(Handler & h)
-	{
-		//h.template registerType<Goals::AbstractGoal, Goals::BoostHero>();
-		h.template registerType<Goals::AbstractGoal, Goals::Build>();
-		h.template registerType<Goals::AbstractGoal, Goals::BuildThis>();
-		//h.template registerType<Goals::AbstractGoal, Goals::CIssueCommand>();
-		h.template registerType<Goals::AbstractGoal, Goals::ClearWayTo>();
-		h.template registerType<Goals::AbstractGoal, Goals::CollectRes>();
-		h.template registerType<Goals::AbstractGoal, Goals::Conquer>();
-		h.template registerType<Goals::AbstractGoal, Goals::DigAtTile>();
-		h.template registerType<Goals::AbstractGoal, Goals::Explore>();
-		h.template registerType<Goals::AbstractGoal, Goals::FindObj>();
-		h.template registerType<Goals::AbstractGoal, Goals::GatherArmy>();
-		h.template registerType<Goals::AbstractGoal, Goals::GatherTroops>();
-		h.template registerType<Goals::AbstractGoal, Goals::GetArtOfType>();
-		h.template registerType<Goals::AbstractGoal, Goals::VisitObj>();
-		h.template registerType<Goals::AbstractGoal, Goals::Invalid>();
-		//h.template registerType<Goals::AbstractGoal, Goals::NotLose>();
-		h.template registerType<Goals::AbstractGoal, Goals::RecruitHero>();
-		h.template registerType<Goals::AbstractGoal, Goals::VisitHero>();
-		h.template registerType<Goals::AbstractGoal, Goals::VisitTile>();
-		h.template registerType<Goals::AbstractGoal, Goals::Win>();
-	}
-	#endif
-
-	template<typename Handler> void serializeInternal(Handler & h, const int version)
-	{
-		h & knownTeleportChannels;
-		h & knownSubterraneanGates;
-		h & destinationTeleport;
-		h & townVisitsThisWeek;
-
-		#if 0
-		//disabled due to issue 2890
-		h & lockedHeroes;
-		#else
-		{
-			ui32 length = 0;
-			h & length;
-			if(!h.saving)
-			{
-				std::set<ui32> loadedPointers;
-				lockedHeroes.clear();
-				for(ui32 index = 0; index < length; index++)
-				{
-					HeroPtr ignored1;
-					h & ignored1;
-
-					ui8 flag = 0;
-					h & flag;
-
-					if(flag)
-					{
-						ui32 pid = 0xffffffff;
-						h & pid;
-
-						if(!vstd::contains(loadedPointers, pid))
-						{
-							loadedPointers.insert(pid);
-
-							ui16 typeId = 0;
-							//this is the problem requires such hack
-							//we have to explicitly ignore invalid goal class type id
-							h & typeId;
-							Goals::AbstractGoal ignored2;
-							ignored2.serialize(h, version);
-						}
-					}
-				}
-			}
-		}
-		#endif
-
-		h & reservedHeroesMap; //FIXME: cannot instantiate abstract class
-		h & visitableObjs;
-		h & alreadyVisited;
-		h & reservedObjs;
-		h & status;
-		h & battlename;
-		h & heroesUnableToExplore;
-
-		//myCB is restored after load by init call
-	}
-};
-
-class cannotFulfillGoalException : public std::exception
-{
-	std::string msg;
-
-public:
-	explicit cannotFulfillGoalException(crstring _Message)
-		: msg(_Message)
-	{
-	}
-
-	virtual ~cannotFulfillGoalException() throw ()
-	{
-	};
-
-	const char * what() const throw () override
-	{
-		return msg.c_str();
-	}
-};
-
-class goalFulfilledException : public std::exception
-{
-	std::string msg;
-
-public:
-	Goals::TSubgoal goal;
-
-	explicit goalFulfilledException(Goals::TSubgoal Goal)
-		: goal(Goal)
-	{
-		msg = goal->name();
-	}
-
-	virtual ~goalFulfilledException() throw ()
-	{
-	};
-
-	const char * what() const throw () override
-	{
-		return msg.c_str();
-	}
-};
-
-void makePossibleUpgrades(const CArmedInstance * obj);
+/*
+ * VCAI.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "AIUtility.h"
+#include "Goals/AbstractGoal.h"
+#include "../../lib/AI_Base.h"
+#include "../../CCallback.h"
+
+#include "../../lib/CThreadHelper.h"
+
+#include "../../lib/GameConstants.h"
+#include "../../lib/VCMI_Lib.h"
+#include "../../lib/CBuildingHandler.h"
+#include "../../lib/CCreatureHandler.h"
+#include "../../lib/CTownHandler.h"
+#include "../../lib/mapObjects/MiscObjects.h"
+#include "../../lib/spells/CSpellHandler.h"
+#include "../../lib/CondSh.h"
+#include "Pathfinding/AIPathfinder.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+struct QuestInfo;
+
+VCMI_LIB_NAMESPACE_END
+
+class AIhelper;
+
+class AIStatus
+{
+	boost::mutex mx;
+	boost::condition_variable cv;
+
+	BattleState battle;
+	std::map<QueryID, std::string> remainingQueries;
+	std::map<int, QueryID> requestToQueryID; //IDs of answer-requests sent to server => query ids (so we can match answer confirmation from server to the query)
+	std::vector<const CGObjectInstance *> objectsBeingVisited;
+	bool ongoingHeroMovement;
+	bool ongoingChannelProbing; // true if AI currently explore bidirectional teleport channel exits
+
+	bool havingTurn;
+
+public:
+	AIStatus();
+	~AIStatus();
+	void setBattle(BattleState BS);
+	void setMove(bool ongoing);
+	void setChannelProbing(bool ongoing);
+	bool channelProbing();
+	BattleState getBattle();
+	void addQuery(QueryID ID, std::string description);
+	void removeQuery(QueryID ID);
+	int getQueriesCount();
+	void startedTurn();
+	void madeTurn();
+	void waitTillFree();
+	bool haveTurn();
+	void attemptedAnsweringQuery(QueryID queryID, int answerRequestID);
+	void receivedAnswerConfirmation(int answerRequestID, int result);
+	void heroVisit(const CGObjectInstance * obj, bool started);
+
+
+	template<typename Handler> void serialize(Handler & h, const int version)
+	{
+		h & battle;
+		h & remainingQueries;
+		h & requestToQueryID;
+		h & havingTurn;
+	}
+};
+
+class DLL_EXPORT VCAI : public CAdventureAI
+{
+public:
+
+	friend class FuzzyHelper;
+	friend class ResourceManager;
+	friend class BuildingManager;
+
+	std::map<TeleportChannelID, std::shared_ptr<TeleportChannel>> knownTeleportChannels;
+	std::map<const CGObjectInstance *, const CGObjectInstance *> knownSubterraneanGates;
+	ObjectInstanceID destinationTeleport;
+	int3 destinationTeleportPos;
+	std::vector<ObjectInstanceID> teleportChannelProbingList; //list of teleport channel exits that not visible and need to be (re-)explored
+	//std::vector<const CGObjectInstance *> visitedThisWeek; //only OPWs
+	std::map<HeroPtr, std::set<const CGTownInstance *>> townVisitsThisWeek;
+
+	//part of mainLoop, but accessible from outisde
+	std::vector<Goals::TSubgoal> basicGoals;
+	Goals::TGoalVec goalsToRemove;
+	Goals::TGoalVec goalsToAdd;
+	std::map<Goals::TSubgoal, Goals::TGoalVec> ultimateGoalsFromBasic; //theoreticlaly same goal can fulfill multiple basic goals
+
+	std::set<HeroPtr> invalidPathHeroes; //FIXME, just a workaround
+	std::map<HeroPtr, Goals::TSubgoal> lockedHeroes; //TODO: allow non-elementar objectives
+	std::map<HeroPtr, std::set<const CGObjectInstance *>> reservedHeroesMap; //objects reserved by specific heroes
+	std::set<HeroPtr> heroesUnableToExplore; //these heroes will not be polled for exploration in current state of game
+
+	//sets are faster to search, also do not contain duplicates
+	std::set<const CGObjectInstance *> visitableObjs;
+	std::set<const CGObjectInstance *> alreadyVisited;
+	std::set<const CGObjectInstance *> reservedObjs; //to be visited by specific hero
+	std::map<HeroPtr, std::set<HeroPtr>> visitedHeroes; //visited this turn //FIXME: this is just bug workaround
+
+	AIStatus status;
+	std::string battlename;
+
+	std::shared_ptr<CCallback> myCb;
+
+	std::unique_ptr<boost::thread> makingTurn;
+private:
+	boost::mutex turnInterruptionMutex;
+public:
+	ObjectInstanceID selectedObject;
+
+	AIhelper * ah;
+
+	VCAI();
+	virtual ~VCAI();
+
+	//TODO: use only smart pointers?
+	void tryRealize(Goals::Explore & g);
+	void tryRealize(Goals::RecruitHero & g);
+	void tryRealize(Goals::VisitTile & g);
+	void tryRealize(Goals::VisitObj & g);
+	void tryRealize(Goals::VisitHero & g);
+	void tryRealize(Goals::BuildThis & g);
+	void tryRealize(Goals::DigAtTile & g);
+	void tryRealize(Goals::Trade & g);
+	void tryRealize(Goals::BuyArmy & g);
+	void tryRealize(Goals::Invalid & g);
+	void tryRealize(Goals::AbstractGoal & g);
+
+	bool isTileNotReserved(const CGHeroInstance * h, int3 t) const; //the tile is not occupied by allied hero and the object is not reserved
+
+	std::string getBattleAIName() const override;
+
+	void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
+	void yourTurn(QueryID queryID) override;
+
+	void heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector<SecondarySkill> & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id
+	void commanderGotLevel(const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID) override; //TODO
+	void showBlockingDialog(const std::string & text, const std::vector<Component> & components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID.
+	void showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) override; //all stacks operations between these objects become allowed, interface has to call onEnd when done
+	void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override;
+	void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects) override;
+	void saveGame(BinarySerializer & h, const int version) override; //saving
+	void loadGame(BinaryDeserializer & h, const int version) override; //loading
+	void finish() override;
+
+	void availableCreaturesChanged(const CGDwelling * town) override;
+	void heroMoved(const TryMoveHero & details, bool verbose = true) override;
+	void heroInGarrisonChange(const CGTownInstance * town) override;
+	void centerView(int3 pos, int focusTime) override;
+	void tileHidden(const std::unordered_set<int3> & pos) override;
+	void artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) override;
+	void artifactAssembled(const ArtifactLocation & al) override;
+	void showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) override;
+	void showThievesGuildWindow(const CGObjectInstance * obj) override;
+	void playerBlocked(int reason, bool start) override;
+	void showPuzzleMap() override;
+	void showShipyardDialog(const IShipyard * obj) override;
+	void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) override;
+	void artifactPut(const ArtifactLocation & al) override;
+	void artifactRemoved(const ArtifactLocation & al) override;
+	void artifactDisassembled(const ArtifactLocation & al) override;
+	void heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) override;
+	void availableArtifactsChanged(const CGBlackMarket * bm = nullptr) override;
+	void heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) override;
+	void tileRevealed(const std::unordered_set<int3> & pos) override;
+	void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override;
+	void heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) override;
+	void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level, QueryID queryID) override;
+	void heroMovePointsChanged(const CGHeroInstance * hero) override;
+	void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override;
+	void newObject(const CGObjectInstance * obj) override;
+	void showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor) override;
+	void playerBonusChanged(const Bonus & bonus, bool gain) override;
+	void heroCreated(const CGHeroInstance *) override;
+	void advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) override;
+	void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector<Component> & components, int soundID) override;
+	void requestRealized(PackageApplied * pa) override;
+	void receivedResource() override;
+	void objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) override;
+	void showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override;
+	void heroManaPointsChanged(const CGHeroInstance * hero) override;
+	void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override;
+	void battleResultsApplied() override;
+	void beforeObjectPropertyChanged(const SetObjectProperty * sop) override;
+	void objectPropertyChanged(const SetObjectProperty * sop) override;
+	void buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) override;
+	void heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) override;
+	void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override;
+	void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
+
+	void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override;
+	void battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) override;
+	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override;
+
+	void makeTurn();
+	void mainLoop();
+	void performTypicalActions();
+
+	void buildArmyIn(const CGTownInstance * t);
+	void striveToGoal(Goals::TSubgoal ultimateGoal);
+	Goals::TSubgoal decomposeGoal(Goals::TSubgoal ultimateGoal);
+	void endTurn();
+	void wander(HeroPtr h);
+	void setGoal(HeroPtr h, Goals::TSubgoal goal);
+	void evaluateGoal(HeroPtr h); //evaluates goal assigned to hero, if any
+	void completeGoal(Goals::TSubgoal goal); //safely removes goal from reserved hero
+
+	void recruitHero(const CGTownInstance * t, bool throwing = false);
+	bool isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, std::optional<float> movementCostLimit = std::nullopt);
+	bool isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, const AIPath & path) const;
+	//void recruitCreatures(const CGTownInstance * t);
+	void recruitCreatures(const CGDwelling * d, const CArmedInstance * recruiter);
+	void pickBestCreatures(const CArmedInstance * army, const CArmedInstance * source); //called when we can't find a slot for new stack
+	void pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * other = nullptr);
+	void moveCreaturesToHero(const CGTownInstance * t);
+	void performObjectInteraction(const CGObjectInstance * obj, HeroPtr h);
+
+	bool moveHeroToTile(int3 dst, HeroPtr h);
+	void buildStructure(const CGTownInstance * t, BuildingID building); //TODO: move to BuildingManager
+
+	void lostHero(HeroPtr h); //should remove all references to hero (assigned tasks and so on)
+	void waitTillFree();
+
+	void addVisitableObj(const CGObjectInstance * obj);
+	void markObjectVisited(const CGObjectInstance * obj);
+	void reserveObject(HeroPtr h, const CGObjectInstance * obj); //TODO: reserve all objects that heroes attempt to visit
+	void unreserveObject(HeroPtr h, const CGObjectInstance * obj);
+
+	void markHeroUnableToExplore(HeroPtr h);
+	void markHeroAbleToExplore(HeroPtr h);
+	bool isAbleToExplore(HeroPtr h);
+	void clearPathsInfo();
+
+	void validateObject(const CGObjectInstance * obj); //checks if object is still visible and if not, removes references to it
+	void validateObject(ObjectIdRef obj); //checks if object is still visible and if not, removes references to it
+	void validateVisitableObjs();
+	void retrieveVisitableObjs(std::vector<const CGObjectInstance *> & out, bool includeOwned = false) const;
+	void retrieveVisitableObjs();
+	virtual std::vector<const CGObjectInstance *> getFlaggedObjects() const;
+
+	const CGObjectInstance * lookForArt(int aid) const;
+	bool isAccessible(const int3 & pos) const;
+	HeroPtr getHeroWithGrail() const;
+
+	const CGObjectInstance * getUnvisitedObj(const std::function<bool(const CGObjectInstance *)> & predicate);
+	bool isAccessibleForHero(const int3 & pos, HeroPtr h, bool includeAllies = false) const;
+	//optimization - use one SM for every hero call
+
+	const CGTownInstance * findTownWithTavern() const;
+	bool canRecruitAnyHero(const CGTownInstance * t = NULL) const;
+
+	Goals::TSubgoal getGoal(HeroPtr h) const;
+	bool canAct(HeroPtr h) const;
+	std::vector<HeroPtr> getUnblockedHeroes() const;
+	std::vector<HeroPtr> getMyHeroes() const;
+	HeroPtr primaryHero() const;
+	void checkHeroArmy(HeroPtr h);
+
+	void requestSent(const CPackForServer * pack, int requestID) override;
+	void answerQuery(QueryID queryID, int selection);
+	//special function that can be called ONLY from game events handling thread and will send request ASAP
+	void requestActionASAP(std::function<void()> whatToDo);
+
+	#if 0
+	//disabled due to issue 2890
+	template<typename Handler> void registerGoals(Handler & h)
+	{
+		//h.template registerType<Goals::AbstractGoal, Goals::BoostHero>();
+		h.template registerType<Goals::AbstractGoal, Goals::Build>();
+		h.template registerType<Goals::AbstractGoal, Goals::BuildThis>();
+		//h.template registerType<Goals::AbstractGoal, Goals::CIssueCommand>();
+		h.template registerType<Goals::AbstractGoal, Goals::ClearWayTo>();
+		h.template registerType<Goals::AbstractGoal, Goals::CollectRes>();
+		h.template registerType<Goals::AbstractGoal, Goals::Conquer>();
+		h.template registerType<Goals::AbstractGoal, Goals::DigAtTile>();
+		h.template registerType<Goals::AbstractGoal, Goals::Explore>();
+		h.template registerType<Goals::AbstractGoal, Goals::FindObj>();
+		h.template registerType<Goals::AbstractGoal, Goals::GatherArmy>();
+		h.template registerType<Goals::AbstractGoal, Goals::GatherTroops>();
+		h.template registerType<Goals::AbstractGoal, Goals::GetArtOfType>();
+		h.template registerType<Goals::AbstractGoal, Goals::VisitObj>();
+		h.template registerType<Goals::AbstractGoal, Goals::Invalid>();
+		//h.template registerType<Goals::AbstractGoal, Goals::NotLose>();
+		h.template registerType<Goals::AbstractGoal, Goals::RecruitHero>();
+		h.template registerType<Goals::AbstractGoal, Goals::VisitHero>();
+		h.template registerType<Goals::AbstractGoal, Goals::VisitTile>();
+		h.template registerType<Goals::AbstractGoal, Goals::Win>();
+	}
+	#endif
+
+	template<typename Handler> void serializeInternal(Handler & h, const int version)
+	{
+		h & knownTeleportChannels;
+		h & knownSubterraneanGates;
+		h & destinationTeleport;
+		h & townVisitsThisWeek;
+
+		#if 0
+		//disabled due to issue 2890
+		h & lockedHeroes;
+		#else
+		{
+			ui32 length = 0;
+			h & length;
+			if(!h.saving)
+			{
+				std::set<ui32> loadedPointers;
+				lockedHeroes.clear();
+				for(ui32 index = 0; index < length; index++)
+				{
+					HeroPtr ignored1;
+					h & ignored1;
+
+					ui8 flag = 0;
+					h & flag;
+
+					if(flag)
+					{
+						ui32 pid = 0xffffffff;
+						h & pid;
+
+						if(!vstd::contains(loadedPointers, pid))
+						{
+							loadedPointers.insert(pid);
+
+							ui16 typeId = 0;
+							//this is the problem requires such hack
+							//we have to explicitly ignore invalid goal class type id
+							h & typeId;
+							Goals::AbstractGoal ignored2;
+							ignored2.serialize(h, version);
+						}
+					}
+				}
+			}
+		}
+		#endif
+
+		h & reservedHeroesMap; //FIXME: cannot instantiate abstract class
+		h & visitableObjs;
+		h & alreadyVisited;
+		h & reservedObjs;
+		h & status;
+		h & battlename;
+		h & heroesUnableToExplore;
+
+		//myCB is restored after load by init call
+	}
+};
+
+class cannotFulfillGoalException : public std::exception
+{
+	std::string msg;
+
+public:
+	explicit cannotFulfillGoalException(crstring _Message)
+		: msg(_Message)
+	{
+	}
+
+	virtual ~cannotFulfillGoalException() throw ()
+	{
+	};
+
+	const char * what() const throw () override
+	{
+		return msg.c_str();
+	}
+};
+
+class goalFulfilledException : public std::exception
+{
+	std::string msg;
+
+public:
+	Goals::TSubgoal goal;
+
+	explicit goalFulfilledException(Goals::TSubgoal Goal)
+		: goal(Goal)
+	{
+		msg = goal->name();
+	}
+
+	virtual ~goalFulfilledException() throw ()
+	{
+	};
+
+	const char * what() const throw () override
+	{
+		return msg.c_str();
+	}
+};
+
+void makePossibleUpgrades(const CArmedInstance * obj);

+ 53 - 53
AUTHORS.h

@@ -1,53 +1,53 @@
-/*
- * AUTHORS.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 PROJECT CODE CONTRIBUTORS:
-std::vector<std::vector<std::string>> contributors = {
-//   Task          Name                    Aka                      E-Mail
-   { "Idea",       "Michał Urbańczyk",     "Tow",                   "[email protected]"             },
-   { "Idea",       "Mateusz B.",           "Tow dragon",            "[email protected]"            },
-
-   { "Developing", "Andrea Palmate",       "afxgroup",              "[email protected]"         },
-   { "Developing", "Alexander Shishkin",   "alexvins",              ""                             },
-   { "Developing", "Rafal R.",             "ambtrip",               "[email protected]"                },
-   { "Developing", "Andrii Danylchenko",   "",                      ""                             },
-   { "Developing", "Benjamin Gentner",     "beegee",                ""                             },
-   { "Developing", "Piotr Wójcik",         "Chocimier",             "[email protected]"            },
-   { "Developing", "Dmitry Orlov",         "",                      "[email protected]" },
-   { "Developing", "",                     "Dydzio",                "[email protected]"           },
-   { "Developing", "Andrzej Żak",          "godric3",               ""                             },
-   { "Developing", "Henning Koehler",      "henningkoehlernz",      "[email protected]" },
-   { "Developing", "Ivan Savenko",         "",                      "[email protected]"         },
-   { "Developing", "",                     "kambala-decapitator",   "[email protected]"          },
-   { "Developing", "",                     "krs0",                  ""                             },
-   { "Developing", "",                     "Laserlicht",            ""                             },
-   { "Developing", "Alexey",               "Macron1Robot",          ""                             },
-   { "Developing", "Michał Kalinowski",    "",                      "[email protected]"            },
-   { "Developing", "Vadim Glazunov",       "neweagle",              "[email protected]"           },
-   { "Developing", "Andrey Cherkas",       "nordsoft",              "[email protected]"           },
-   { "Developing", "Rickard Westerlund",   "Onion Knight",          "[email protected]"         },
-   { "Developing", "Yifeng Sun",           "phoebus118",            "[email protected]"       },
-   { "Developing", "",                     "rilian-la-te",          ""                             },
-   { "Developing", "",                     "SoundSSGood",           ""                             },
-   { "Developing", "Stefan Pavlov",        "Ste",                   "[email protected]"            },
-   { "Developing", "Arseniy Shestakov",    "SXX",                   "[email protected]"      },
-   { "Developing", "Lukasz Wychrystenko",  "tezeriusz",             "[email protected]"         },
-   { "Developing", "Trevor Standley",      "tstandley",             ""                             },
-   { "Developing", "Vadim Markovtsev",     "",                      "[email protected]"           },
-   { "Developing", "Frank Zago",           "ubuntux",               ""                             },
-   { "Developing", "",                     "vmarkovtsev",           ""                             },
-   { "Developing", "Tom Zielinski",        "Warmonger",             "[email protected]"              },
-   { "Developing", "Xiaomin Ding",         "",                      "[email protected]"        },
-
-   { "Testing",    "Ben Yan",              "by003",                 "[email protected],"        },
-   { "Testing",    "",                     "Misiokles",             ""                             },
-   { "Testing",    "",                     "Povelitel",             ""                             },
-};
+/*
+ * AUTHORS.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 PROJECT CODE CONTRIBUTORS:
+std::vector<std::vector<std::string>> contributors = {
+//   Task          Name                    Aka                      E-Mail
+   { "Idea",       "Michał Urbańczyk",     "Tow",                   "[email protected]"             },
+   { "Idea",       "Mateusz B.",           "Tow dragon",            "[email protected]"            },
+
+   { "Developing", "Andrea Palmate",       "afxgroup",              "[email protected]"         },
+   { "Developing", "Alexander Shishkin",   "alexvins",              ""                             },
+   { "Developing", "Rafal R.",             "ambtrip",               "[email protected]"                },
+   { "Developing", "Andrii Danylchenko",   "",                      ""                             },
+   { "Developing", "Benjamin Gentner",     "beegee",                ""                             },
+   { "Developing", "Piotr Wójcik",         "Chocimier",             "[email protected]"            },
+   { "Developing", "Dmitry Orlov",         "",                      "[email protected]" },
+   { "Developing", "",                     "Dydzio",                "[email protected]"           },
+   { "Developing", "Andrzej Żak",          "godric3",               ""                             },
+   { "Developing", "Henning Koehler",      "henningkoehlernz",      "[email protected]" },
+   { "Developing", "Ivan Savenko",         "",                      "[email protected]"         },
+   { "Developing", "",                     "kambala-decapitator",   "[email protected]"          },
+   { "Developing", "",                     "krs0",                  ""                             },
+   { "Developing", "",                     "Laserlicht",            ""                             },
+   { "Developing", "Alexey",               "Macron1Robot",          ""                             },
+   { "Developing", "Michał Kalinowski",    "",                      "[email protected]"            },
+   { "Developing", "Vadim Glazunov",       "neweagle",              "[email protected]"           },
+   { "Developing", "Andrey Cherkas",       "nordsoft",              "[email protected]"           },
+   { "Developing", "Rickard Westerlund",   "Onion Knight",          "[email protected]"         },
+   { "Developing", "Yifeng Sun",           "phoebus118",            "[email protected]"       },
+   { "Developing", "",                     "rilian-la-te",          ""                             },
+   { "Developing", "",                     "SoundSSGood",           ""                             },
+   { "Developing", "Stefan Pavlov",        "Ste",                   "[email protected]"            },
+   { "Developing", "Arseniy Shestakov",    "SXX",                   "[email protected]"      },
+   { "Developing", "Lukasz Wychrystenko",  "tezeriusz",             "[email protected]"         },
+   { "Developing", "Trevor Standley",      "tstandley",             ""                             },
+   { "Developing", "Vadim Markovtsev",     "",                      "[email protected]"           },
+   { "Developing", "Frank Zago",           "ubuntux",               ""                             },
+   { "Developing", "",                     "vmarkovtsev",           ""                             },
+   { "Developing", "Tom Zielinski",        "Warmonger",             "[email protected]"              },
+   { "Developing", "Xiaomin Ding",         "",                      "[email protected]"        },
+
+   { "Testing",    "Ben Yan",              "by003",                 "[email protected],"        },
+   { "Testing",    "",                     "Misiokles",             ""                             },
+   { "Testing",    "",                     "Povelitel",             ""                             },
+};

+ 423 - 423
CCallback.cpp

@@ -1,423 +1,423 @@
-/*
- * CCallback.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 "CCallback.h"
-
-#include "lib/CCreatureHandler.h"
-#include "lib/gameState/CGameState.h"
-#include "client/CPlayerInterface.h"
-#include "client/Client.h"
-#include "lib/mapping/CMap.h"
-#include "lib/CBuildingHandler.h"
-#include "lib/CGeneralTextHandler.h"
-#include "lib/CHeroHandler.h"
-#include "lib/NetPacks.h"
-#include "lib/CArtHandler.h"
-#include "lib/GameConstants.h"
-#include "lib/CPlayerState.h"
-#include "lib/UnlockGuard.h"
-#include "lib/battle/BattleInfo.h"
-
-bool CCallback::teleportHero(const CGHeroInstance *who, const CGTownInstance *where)
-{
-	CastleTeleportHero pack(who->id, where->id, 1);
-	sendRequest(&pack);
-	return true;
-}
-
-bool CCallback::moveHero(const CGHeroInstance *h, int3 dst, bool transit)
-{
-	MoveHero pack(dst,h->id,transit);
-	sendRequest(&pack);
-	return true;
-}
-
-int CCallback::selectionMade(int selection, QueryID queryID)
-{
-	return sendQueryReply(selection, queryID);
-}
-
-int CCallback::sendQueryReply(std::optional<int32_t> reply, QueryID queryID)
-{
-	ASSERT_IF_CALLED_WITH_PLAYER
-	if(queryID == QueryID(-1))
-	{
-		logGlobal->error("Cannot answer the query -1!");
-		return -1;
-	}
-
-	QueryReply pack(queryID, reply);
-	pack.player = *player;
-	return sendRequest(&pack);
-}
-
-void CCallback::recruitCreatures(const CGDwelling * obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level)
-{
-	// TODO exception for neutral dwellings shouldn't be hardcoded
-	if(player != obj->tempOwner && obj->ID != Obj::WAR_MACHINE_FACTORY && obj->ID != Obj::REFUGEE_CAMP)
-		return;
-
-	RecruitCreatures pack(obj->id, dst->id, ID, amount, level);
-	sendRequest(&pack);
-}
-
-bool CCallback::dismissCreature(const CArmedInstance *obj, SlotID stackPos)
-{
-	if((player && obj->tempOwner != player) || (obj->stacksCount()<2  && obj->needsLastStack()))
-		return false;
-
-	DisbandCreature pack(stackPos,obj->id);
-	sendRequest(&pack);
-	return true;
-}
-
-bool CCallback::upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID)
-{
-	UpgradeCreature pack(stackPos,obj->id,newID);
-	sendRequest(&pack);
-	return false;
-}
-
-void CCallback::endTurn()
-{
-	logGlobal->trace("Player %d ended his turn.", player->getNum());
-	EndTurn pack;
-	sendRequest(&pack);
-}
-int CCallback::swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)
-{
-	ArrangeStacks pack(1,p1,p2,s1->id,s2->id,0);
-	sendRequest(&pack);
-	return 0;
-}
-
-int CCallback::mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)
-{
-	ArrangeStacks pack(2,p1,p2,s1->id,s2->id,0);
-	sendRequest(&pack);
-	return 0;
-}
-
-int CCallback::splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val)
-{
-	ArrangeStacks pack(3,p1,p2,s1->id,s2->id,val);
-	sendRequest(&pack);
-	return 0;
-}
-
-int CCallback::bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot)
-{
-	BulkMoveArmy pack(srcArmy, destArmy, srcSlot);
-	sendRequest(&pack);
-	return 0;
-}
-
-int CCallback::bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany)
-{
-	BulkSplitStack pack(armyId, srcSlot, howMany);
-	sendRequest(&pack);
-	return 0;
-}
-
-int CCallback::bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot)
-{
-	BulkSmartSplitStack pack(armyId, srcSlot);
-	sendRequest(&pack);
-	return 0;
-}
-
-int CCallback::bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot)
-{
-	BulkMergeStacks pack(armyId, srcSlot);
-	sendRequest(&pack);
-	return 0;
-}
-
-bool CCallback::dismissHero(const CGHeroInstance *hero)
-{
-	if(player!=hero->tempOwner) return false;
-
-	DismissHero pack(hero->id);
-	sendRequest(&pack);
-	return true;
-}
-
-bool CCallback::swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2)
-{
-	ExchangeArtifacts ea;
-	ea.src = l1;
-	ea.dst = l2;
-	sendRequest(&ea);
-	return true;
-}
-
-/**
- * Assembles or disassembles a combination artifact.
- * @param hero Hero holding the artifact(s).
- * @param artifactSlot The worn slot ID of the combination- or constituent artifact.
- * @param assemble True for assembly operation, false for disassembly.
- * @param assembleTo If assemble is true, this represents the artifact ID of the combination
- * artifact to assemble to. Otherwise it's not used.
- */
-void CCallback::assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo)
-{
-	AssembleArtifacts aa(hero->id, artifactSlot, assemble, assembleTo);
-	sendRequest(&aa);
-}
-
-void CCallback::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack)
-{
-	BulkExchangeArtifacts bma(srcHero, dstHero, swap, equipped, backpack);
-	sendRequest(&bma);
-}
-
-void CCallback::eraseArtifactByClient(const ArtifactLocation & al)
-{
-	EraseArtifactByClient ea(al);
-	sendRequest(&ea);
-}
-
-bool CCallback::buildBuilding(const CGTownInstance *town, BuildingID buildingID)
-{
-	if(town->tempOwner!=player)
-		return false;
-
-	if(canBuildStructure(town, buildingID) != EBuildingState::ALLOWED)
-		return false;
-
-	BuildStructure pack(town->id,buildingID);
-	sendRequest(&pack);
-	return true;
-}
-
-void CBattleCallback::battleMakeSpellAction(const BattleID & battleID, const BattleAction & action)
-{
-	assert(action.actionType == EActionType::HERO_SPELL);
-	MakeAction mca(action);
-	mca.battleID = battleID;
-	sendRequest(&mca);
-}
-
-int CBattleCallback::sendRequest(const CPackForServer * request)
-{
-	int requestID = cl->sendRequest(request, *getPlayerID());
-	if(waitTillRealize)
-	{
-		logGlobal->trace("We'll wait till request %d is answered.\n", requestID);
-		auto gsUnlocker = vstd::makeUnlockSharedGuardIf(CGameState::mutex, unlockGsWhenWaiting);
-		CClient::waitingRequest.waitWhileContains(requestID);
-	}
-
-	boost::this_thread::interruption_point();
-	return requestID;
-}
-
-void CCallback::swapGarrisonHero( const CGTownInstance *town )
-{
-	if(town->tempOwner == *player || (town->garrisonHero && town->garrisonHero->tempOwner == *player ))
-	{
-		GarrisonHeroSwap pack(town->id);
-		sendRequest(&pack);
-	}
-}
-
-void CCallback::buyArtifact(const CGHeroInstance *hero, ArtifactID aid)
-{
-	if(hero->tempOwner != *player) return;
-
-	BuyArtifact pack(hero->id,aid);
-	sendRequest(&pack);
-}
-
-void CCallback::trade(const IMarket * market, EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero)
-{
-	trade(market, mode, std::vector<ui32>(1, id1), std::vector<ui32>(1, id2), std::vector<ui32>(1, val1), hero);
-}
-
-void CCallback::trade(const IMarket * market, EMarketMode mode, const std::vector<ui32> & id1, const std::vector<ui32> & id2, const std::vector<ui32> & val1, const CGHeroInstance * hero)
-{
-	TradeOnMarketplace pack;
-	pack.marketId = dynamic_cast<const CGObjectInstance *>(market)->id;
-	pack.heroId = hero ? hero->id : ObjectInstanceID();
-	pack.mode = mode;
-	pack.r1 = id1;
-	pack.r2 = id2;
-	pack.val = val1;
-	sendRequest(&pack);
-}
-
-void CCallback::setFormation(const CGHeroInstance * hero, bool tight)
-{
-	SetFormation pack(hero->id,tight);
-	sendRequest(&pack);
-}
-
-void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero)
-{
-	assert(townOrTavern);
-	assert(hero);
-
-	HireHero pack(HeroTypeID(hero->subID), townOrTavern->id);
-	pack.player = *player;
-	sendRequest(&pack);
-}
-
-void CCallback::save( const std::string &fname )
-{
-	cl->save(fname);
-}
-
-void CCallback::gamePause(bool pause)
-{
-	if(pause)
-	{
-		GamePause pack;
-		pack.player = *player;
-		sendRequest(&pack);
-	}
-	else
-	{
-		sendQueryReply(0, QueryID::CLIENT);
-	}
-}
-
-void CCallback::sendMessage(const std::string &mess, const CGObjectInstance * currentObject)
-{
-	ASSERT_IF_CALLED_WITH_PLAYER
-	PlayerMessage pm(mess, currentObject? currentObject->id : ObjectInstanceID(-1));
-	if(player)
-		pm.player = *player;
-	sendRequest(&pm);
-}
-
-void CCallback::buildBoat( const IShipyard *obj )
-{
-	BuildBoat bb;
-	bb.objid = dynamic_cast<const CGObjectInstance*>(obj)->id;
-	sendRequest(&bb);
-}
-
-CCallback::CCallback(CGameState * GS, std::optional<PlayerColor> Player, CClient * C)
-	: CBattleCallback(Player, C)
-{
-	gs = GS;
-
-	waitTillRealize = false;
-	unlockGsWhenWaiting = false;
-}
-
-CCallback::~CCallback() = default;
-
-bool CCallback::canMoveBetween(const int3 &a, const int3 &b)
-{
-	//bidirectional
-	return gs->map->canMoveBetween(a, b);
-}
-
-std::shared_ptr<const CPathsInfo> CCallback::getPathsInfo(const CGHeroInstance * h)
-{
-	return cl->getPathsInfo(h);
-}
-
-std::optional<PlayerColor> CCallback::getPlayerID() const
-{
-	return CBattleCallback::getPlayerID();
-}
-
-int3 CCallback::getGuardingCreaturePosition(int3 tile)
-{
-	if (!gs->map->isInTheMap(tile))
-		return int3(-1,-1,-1);
-
-	return gs->map->guardingCreaturePositions[tile.z][tile.x][tile.y];
-}
-
-void CCallback::dig( const CGObjectInstance *hero )
-{
-	DigWithHero dwh;
-	dwh.id = hero->id;
-	sendRequest(&dwh);
-}
-
-void CCallback::castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos)
-{
-	CastAdvSpell cas;
-	cas.hid = hero->id;
-	cas.sid = spellID;
-	cas.pos = pos;
-	sendRequest(&cas);
-}
-
-int CCallback::mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)
-{
-	if(s1->getCreature(p1) == s2->getCreature(p2))
-		return mergeStacks(s1, s2, p1, p2);
-	else
-		return swapCreatures(s1, s2, p1, p2);
-}
-
-void CCallback::registerBattleInterface(std::shared_ptr<IBattleEventsReceiver> battleEvents)
-{
-	cl->additionalBattleInts[*player].push_back(battleEvents);
-}
-
-void CCallback::unregisterBattleInterface(std::shared_ptr<IBattleEventsReceiver> battleEvents)
-{
-	cl->additionalBattleInts[*player] -= battleEvents;
-}
-
-CBattleCallback::CBattleCallback(std::optional<PlayerColor> player, CClient * C):
-	cl(C),
-	player(player)
-{
-}
-
-void CBattleCallback::battleMakeUnitAction(const BattleID & battleID, const BattleAction & action)
-{
-	assert(!cl->gs->getBattle(battleID)->tacticDistance);
-	MakeAction ma;
-	ma.ba = action;
-	ma.battleID = battleID;
-	sendRequest(&ma);
-}
-
-void CBattleCallback::battleMakeTacticAction(const BattleID & battleID, const BattleAction & action )
-{
-	assert(cl->gs->getBattle(battleID)->tacticDistance);
-	MakeAction ma;
-	ma.ba = action;
-	ma.battleID = battleID;
-	sendRequest(&ma);
-}
-
-std::optional<BattleAction> CBattleCallback::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState)
-{
-	return cl->playerint[getPlayerID().value()]->makeSurrenderRetreatDecision(battleID, battleState);
-}
-
-std::shared_ptr<CPlayerBattleCallback> CBattleCallback::getBattle(const BattleID & battleID)
-{
-	return activeBattles.at(battleID);
-}
-
-std::optional<PlayerColor> CBattleCallback::getPlayerID() const
-{
-	return player;
-}
-
-void CBattleCallback::onBattleStarted(const IBattleInfo * info)
-{
-	activeBattles[info->getBattleID()] = std::make_shared<CPlayerBattleCallback>(info, *getPlayerID());
-}
-
-void CBattleCallback::onBattleEnded(const BattleID & battleID)
-{
-	activeBattles.erase(battleID);
-}
+/*
+ * CCallback.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 "CCallback.h"
+
+#include "lib/CCreatureHandler.h"
+#include "lib/gameState/CGameState.h"
+#include "client/CPlayerInterface.h"
+#include "client/Client.h"
+#include "lib/mapping/CMap.h"
+#include "lib/CBuildingHandler.h"
+#include "lib/CGeneralTextHandler.h"
+#include "lib/CHeroHandler.h"
+#include "lib/NetPacks.h"
+#include "lib/CArtHandler.h"
+#include "lib/GameConstants.h"
+#include "lib/CPlayerState.h"
+#include "lib/UnlockGuard.h"
+#include "lib/battle/BattleInfo.h"
+
+bool CCallback::teleportHero(const CGHeroInstance *who, const CGTownInstance *where)
+{
+	CastleTeleportHero pack(who->id, where->id, 1);
+	sendRequest(&pack);
+	return true;
+}
+
+bool CCallback::moveHero(const CGHeroInstance *h, int3 dst, bool transit)
+{
+	MoveHero pack(dst,h->id,transit);
+	sendRequest(&pack);
+	return true;
+}
+
+int CCallback::selectionMade(int selection, QueryID queryID)
+{
+	return sendQueryReply(selection, queryID);
+}
+
+int CCallback::sendQueryReply(std::optional<int32_t> reply, QueryID queryID)
+{
+	ASSERT_IF_CALLED_WITH_PLAYER
+	if(queryID == QueryID(-1))
+	{
+		logGlobal->error("Cannot answer the query -1!");
+		return -1;
+	}
+
+	QueryReply pack(queryID, reply);
+	pack.player = *player;
+	return sendRequest(&pack);
+}
+
+void CCallback::recruitCreatures(const CGDwelling * obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level)
+{
+	// TODO exception for neutral dwellings shouldn't be hardcoded
+	if(player != obj->tempOwner && obj->ID != Obj::WAR_MACHINE_FACTORY && obj->ID != Obj::REFUGEE_CAMP)
+		return;
+
+	RecruitCreatures pack(obj->id, dst->id, ID, amount, level);
+	sendRequest(&pack);
+}
+
+bool CCallback::dismissCreature(const CArmedInstance *obj, SlotID stackPos)
+{
+	if((player && obj->tempOwner != player) || (obj->stacksCount()<2  && obj->needsLastStack()))
+		return false;
+
+	DisbandCreature pack(stackPos,obj->id);
+	sendRequest(&pack);
+	return true;
+}
+
+bool CCallback::upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID)
+{
+	UpgradeCreature pack(stackPos,obj->id,newID);
+	sendRequest(&pack);
+	return false;
+}
+
+void CCallback::endTurn()
+{
+	logGlobal->trace("Player %d ended his turn.", player->getNum());
+	EndTurn pack;
+	sendRequest(&pack);
+}
+int CCallback::swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)
+{
+	ArrangeStacks pack(1,p1,p2,s1->id,s2->id,0);
+	sendRequest(&pack);
+	return 0;
+}
+
+int CCallback::mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)
+{
+	ArrangeStacks pack(2,p1,p2,s1->id,s2->id,0);
+	sendRequest(&pack);
+	return 0;
+}
+
+int CCallback::splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val)
+{
+	ArrangeStacks pack(3,p1,p2,s1->id,s2->id,val);
+	sendRequest(&pack);
+	return 0;
+}
+
+int CCallback::bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot)
+{
+	BulkMoveArmy pack(srcArmy, destArmy, srcSlot);
+	sendRequest(&pack);
+	return 0;
+}
+
+int CCallback::bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany)
+{
+	BulkSplitStack pack(armyId, srcSlot, howMany);
+	sendRequest(&pack);
+	return 0;
+}
+
+int CCallback::bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot)
+{
+	BulkSmartSplitStack pack(armyId, srcSlot);
+	sendRequest(&pack);
+	return 0;
+}
+
+int CCallback::bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot)
+{
+	BulkMergeStacks pack(armyId, srcSlot);
+	sendRequest(&pack);
+	return 0;
+}
+
+bool CCallback::dismissHero(const CGHeroInstance *hero)
+{
+	if(player!=hero->tempOwner) return false;
+
+	DismissHero pack(hero->id);
+	sendRequest(&pack);
+	return true;
+}
+
+bool CCallback::swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2)
+{
+	ExchangeArtifacts ea;
+	ea.src = l1;
+	ea.dst = l2;
+	sendRequest(&ea);
+	return true;
+}
+
+/**
+ * Assembles or disassembles a combination artifact.
+ * @param hero Hero holding the artifact(s).
+ * @param artifactSlot The worn slot ID of the combination- or constituent artifact.
+ * @param assemble True for assembly operation, false for disassembly.
+ * @param assembleTo If assemble is true, this represents the artifact ID of the combination
+ * artifact to assemble to. Otherwise it's not used.
+ */
+void CCallback::assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo)
+{
+	AssembleArtifacts aa(hero->id, artifactSlot, assemble, assembleTo);
+	sendRequest(&aa);
+}
+
+void CCallback::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack)
+{
+	BulkExchangeArtifacts bma(srcHero, dstHero, swap, equipped, backpack);
+	sendRequest(&bma);
+}
+
+void CCallback::eraseArtifactByClient(const ArtifactLocation & al)
+{
+	EraseArtifactByClient ea(al);
+	sendRequest(&ea);
+}
+
+bool CCallback::buildBuilding(const CGTownInstance *town, BuildingID buildingID)
+{
+	if(town->tempOwner!=player)
+		return false;
+
+	if(canBuildStructure(town, buildingID) != EBuildingState::ALLOWED)
+		return false;
+
+	BuildStructure pack(town->id,buildingID);
+	sendRequest(&pack);
+	return true;
+}
+
+void CBattleCallback::battleMakeSpellAction(const BattleID & battleID, const BattleAction & action)
+{
+	assert(action.actionType == EActionType::HERO_SPELL);
+	MakeAction mca(action);
+	mca.battleID = battleID;
+	sendRequest(&mca);
+}
+
+int CBattleCallback::sendRequest(const CPackForServer * request)
+{
+	int requestID = cl->sendRequest(request, *getPlayerID());
+	if(waitTillRealize)
+	{
+		logGlobal->trace("We'll wait till request %d is answered.\n", requestID);
+		auto gsUnlocker = vstd::makeUnlockSharedGuardIf(CGameState::mutex, unlockGsWhenWaiting);
+		CClient::waitingRequest.waitWhileContains(requestID);
+	}
+
+	boost::this_thread::interruption_point();
+	return requestID;
+}
+
+void CCallback::swapGarrisonHero( const CGTownInstance *town )
+{
+	if(town->tempOwner == *player || (town->garrisonHero && town->garrisonHero->tempOwner == *player ))
+	{
+		GarrisonHeroSwap pack(town->id);
+		sendRequest(&pack);
+	}
+}
+
+void CCallback::buyArtifact(const CGHeroInstance *hero, ArtifactID aid)
+{
+	if(hero->tempOwner != *player) return;
+
+	BuyArtifact pack(hero->id,aid);
+	sendRequest(&pack);
+}
+
+void CCallback::trade(const IMarket * market, EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero)
+{
+	trade(market, mode, std::vector<ui32>(1, id1), std::vector<ui32>(1, id2), std::vector<ui32>(1, val1), hero);
+}
+
+void CCallback::trade(const IMarket * market, EMarketMode mode, const std::vector<ui32> & id1, const std::vector<ui32> & id2, const std::vector<ui32> & val1, const CGHeroInstance * hero)
+{
+	TradeOnMarketplace pack;
+	pack.marketId = dynamic_cast<const CGObjectInstance *>(market)->id;
+	pack.heroId = hero ? hero->id : ObjectInstanceID();
+	pack.mode = mode;
+	pack.r1 = id1;
+	pack.r2 = id2;
+	pack.val = val1;
+	sendRequest(&pack);
+}
+
+void CCallback::setFormation(const CGHeroInstance * hero, bool tight)
+{
+	SetFormation pack(hero->id,tight);
+	sendRequest(&pack);
+}
+
+void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero)
+{
+	assert(townOrTavern);
+	assert(hero);
+
+	HireHero pack(HeroTypeID(hero->subID), townOrTavern->id);
+	pack.player = *player;
+	sendRequest(&pack);
+}
+
+void CCallback::save( const std::string &fname )
+{
+	cl->save(fname);
+}
+
+void CCallback::gamePause(bool pause)
+{
+	if(pause)
+	{
+		GamePause pack;
+		pack.player = *player;
+		sendRequest(&pack);
+	}
+	else
+	{
+		sendQueryReply(0, QueryID::CLIENT);
+	}
+}
+
+void CCallback::sendMessage(const std::string &mess, const CGObjectInstance * currentObject)
+{
+	ASSERT_IF_CALLED_WITH_PLAYER
+	PlayerMessage pm(mess, currentObject? currentObject->id : ObjectInstanceID(-1));
+	if(player)
+		pm.player = *player;
+	sendRequest(&pm);
+}
+
+void CCallback::buildBoat( const IShipyard *obj )
+{
+	BuildBoat bb;
+	bb.objid = dynamic_cast<const CGObjectInstance*>(obj)->id;
+	sendRequest(&bb);
+}
+
+CCallback::CCallback(CGameState * GS, std::optional<PlayerColor> Player, CClient * C)
+	: CBattleCallback(Player, C)
+{
+	gs = GS;
+
+	waitTillRealize = false;
+	unlockGsWhenWaiting = false;
+}
+
+CCallback::~CCallback() = default;
+
+bool CCallback::canMoveBetween(const int3 &a, const int3 &b)
+{
+	//bidirectional
+	return gs->map->canMoveBetween(a, b);
+}
+
+std::shared_ptr<const CPathsInfo> CCallback::getPathsInfo(const CGHeroInstance * h)
+{
+	return cl->getPathsInfo(h);
+}
+
+std::optional<PlayerColor> CCallback::getPlayerID() const
+{
+	return CBattleCallback::getPlayerID();
+}
+
+int3 CCallback::getGuardingCreaturePosition(int3 tile)
+{
+	if (!gs->map->isInTheMap(tile))
+		return int3(-1,-1,-1);
+
+	return gs->map->guardingCreaturePositions[tile.z][tile.x][tile.y];
+}
+
+void CCallback::dig( const CGObjectInstance *hero )
+{
+	DigWithHero dwh;
+	dwh.id = hero->id;
+	sendRequest(&dwh);
+}
+
+void CCallback::castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos)
+{
+	CastAdvSpell cas;
+	cas.hid = hero->id;
+	cas.sid = spellID;
+	cas.pos = pos;
+	sendRequest(&cas);
+}
+
+int CCallback::mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)
+{
+	if(s1->getCreature(p1) == s2->getCreature(p2))
+		return mergeStacks(s1, s2, p1, p2);
+	else
+		return swapCreatures(s1, s2, p1, p2);
+}
+
+void CCallback::registerBattleInterface(std::shared_ptr<IBattleEventsReceiver> battleEvents)
+{
+	cl->additionalBattleInts[*player].push_back(battleEvents);
+}
+
+void CCallback::unregisterBattleInterface(std::shared_ptr<IBattleEventsReceiver> battleEvents)
+{
+	cl->additionalBattleInts[*player] -= battleEvents;
+}
+
+CBattleCallback::CBattleCallback(std::optional<PlayerColor> player, CClient * C):
+	cl(C),
+	player(player)
+{
+}
+
+void CBattleCallback::battleMakeUnitAction(const BattleID & battleID, const BattleAction & action)
+{
+	assert(!cl->gs->getBattle(battleID)->tacticDistance);
+	MakeAction ma;
+	ma.ba = action;
+	ma.battleID = battleID;
+	sendRequest(&ma);
+}
+
+void CBattleCallback::battleMakeTacticAction(const BattleID & battleID, const BattleAction & action )
+{
+	assert(cl->gs->getBattle(battleID)->tacticDistance);
+	MakeAction ma;
+	ma.ba = action;
+	ma.battleID = battleID;
+	sendRequest(&ma);
+}
+
+std::optional<BattleAction> CBattleCallback::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState)
+{
+	return cl->playerint[getPlayerID().value()]->makeSurrenderRetreatDecision(battleID, battleState);
+}
+
+std::shared_ptr<CPlayerBattleCallback> CBattleCallback::getBattle(const BattleID & battleID)
+{
+	return activeBattles.at(battleID);
+}
+
+std::optional<PlayerColor> CBattleCallback::getPlayerID() const
+{
+	return player;
+}
+
+void CBattleCallback::onBattleStarted(const IBattleInfo * info)
+{
+	activeBattles[info->getBattleID()] = std::make_shared<CPlayerBattleCallback>(info, *getPlayerID());
+}
+
+void CBattleCallback::onBattleEnded(const BattleID & battleID)
+{
+	activeBattles.erase(battleID);
+}

+ 197 - 197
CCallback.h

@@ -1,197 +1,197 @@
-/*
- * CCallback.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/CGameInfoCallback.h"
-#include "lib/battle/CPlayerBattleCallback.h"
-#include "lib/int3.h" // for int3
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-class CGHeroInstance;
-class CGameState;
-struct CPath;
-class CGObjectInstance;
-class CArmedInstance;
-class BattleAction;
-class CGTownInstance;
-class IShipyard;
-struct CGPathNode;
-struct CGPath;
-struct CPathsInfo;
-class PathfinderConfig;
-struct CPack;
-struct CPackForServer;
-class IBattleEventsReceiver;
-class IGameEventsReceiver;
-struct ArtifactLocation;
-class BattleStateInfoForRetreat;
-class IMarket;
-
-VCMI_LIB_NAMESPACE_END
-
-// in static AI build this file gets included into libvcmi
-#ifdef STATIC_AI
-VCMI_LIB_USING_NAMESPACE
-#endif
-
-class CClient;
-struct lua_State;
-
-class IBattleCallback
-{
-public:
-	virtual ~IBattleCallback() = default;
-
-	bool waitTillRealize = false; //if true, request functions will return after they are realized by server
-	bool unlockGsWhenWaiting = false;//if true after sending each request, gs mutex will be unlocked so the changes can be applied; NOTICE caller must have gs mx locked prior to any call to actiob callback!
-	//battle
-	virtual void battleMakeSpellAction(const BattleID & battleID, const BattleAction & action) = 0;
-	virtual void battleMakeUnitAction(const BattleID & battleID, const BattleAction & action) = 0;
-	virtual void battleMakeTacticAction(const BattleID & battleID, const BattleAction & action) = 0;
-	virtual std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) = 0;
-
-	virtual std::shared_ptr<CPlayerBattleCallback> getBattle(const BattleID & battleID) = 0;
-	virtual std::optional<PlayerColor> getPlayerID() const = 0;
-};
-
-class IGameActionCallback
-{
-public:
-	//hero
-	virtual bool moveHero(const CGHeroInstance *h, int3 dst, bool transit) =0; //dst must be free, neighbouring tile (this function can move hero only by one tile)
-	virtual bool dismissHero(const CGHeroInstance * hero)=0; //dismisses given hero; true - successfuly, false - not successfuly
-	virtual void dig(const CGObjectInstance *hero)=0;
-	virtual void castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos = int3(-1, -1, -1))=0; //cast adventure map spell
-
-	//town
-	virtual void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero)=0;
-	virtual bool buildBuilding(const CGTownInstance *town, BuildingID buildingID)=0;
-	virtual void recruitCreatures(const CGDwelling *obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1)=0;
-	virtual bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE)=0; //if newID==-1 then best possible upgrade will be made
-	virtual void swapGarrisonHero(const CGTownInstance *town)=0;
-
-	virtual void trade(const IMarket * market, EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr)=0; //mode==0: sell val1 units of id1 resource for id2 resiurce
-	virtual void trade(const IMarket * market, EMarketMode mode, const std::vector<ui32> & id1, const std::vector<ui32> & id2, const std::vector<ui32> & val1, const CGHeroInstance * hero = nullptr)=0;
-
-	virtual int selectionMade(int selection, QueryID queryID) =0;
-	virtual int sendQueryReply(std::optional<int32_t> reply, QueryID queryID) =0;
-	virtual int swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)=0;//swaps creatures between two possibly different garrisons // TODO: AI-unsafe code - fix it!
-	virtual int mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)=0;//joins first stack to the second (creatures must be same type)
-	virtual int mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) =0; //first goes to the second
-	virtual int splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val)=0;//split creatures from the first stack
-	//virtual bool swapArtifacts(const CGHeroInstance * hero1, ui16 pos1, const CGHeroInstance * hero2, ui16 pos2)=0; //swaps artifacts between two given heroes
-	virtual bool swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2)=0;
-	virtual void assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo)=0;
-	virtual void eraseArtifactByClient(const ArtifactLocation & al)=0;
-	virtual bool dismissCreature(const CArmedInstance *obj, SlotID stackPos)=0;
-	virtual void endTurn()=0;
-	virtual void buyArtifact(const CGHeroInstance *hero, ArtifactID aid)=0; //used to buy artifacts in towns (including spell book in the guild and war machines in blacksmith)
-	virtual void setFormation(const CGHeroInstance * hero, bool tight)=0;
-
-	virtual void save(const std::string &fname) = 0;
-	virtual void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) = 0;
-	virtual void gamePause(bool pause) = 0;
-	virtual void buildBoat(const IShipyard *obj) = 0;
-
-	// To implement high-level army management bulk actions
-	virtual int bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) = 0;
-	virtual int bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany = 1) = 0;
-	virtual int bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot) = 0;
-	virtual int bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) = 0;
-	
-	
-	// Moves all artifacts from one hero to another
-	virtual void bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack) = 0;
-};
-
-class CBattleCallback : public IBattleCallback
-{
-	std::map<BattleID, std::shared_ptr<CPlayerBattleCallback>> activeBattles;
-
-	std::optional<PlayerColor> player;
-
-protected:
-	int sendRequest(const CPackForServer * request); //returns requestID (that'll be matched to requestID in PackageApplied)
-	CClient *cl;
-
-public:
-	CBattleCallback(std::optional<PlayerColor> player, CClient * C);
-	void battleMakeSpellAction(const BattleID & battleID, const BattleAction & action) override;//for casting spells by hero - DO NOT use it for moving active stack
-	void battleMakeUnitAction(const BattleID & battleID, const BattleAction & action) override;
-	void battleMakeTacticAction(const BattleID & battleID, const BattleAction & action) override; // performs tactic phase actions
-	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override;
-
-	std::shared_ptr<CPlayerBattleCallback> getBattle(const BattleID & battleID) override;
-	std::optional<PlayerColor> getPlayerID() const override;
-
-	void onBattleStarted(const IBattleInfo * info);
-	void onBattleEnded(const BattleID & battleID);
-
-	friend class CCallback;
-	friend class CClient;
-};
-
-class CCallback : public CPlayerSpecificInfoCallback, public CBattleCallback, public IGameActionCallback
-{
-public:
-	CCallback(CGameState * GS, std::optional<PlayerColor> Player, CClient * C);
-	virtual ~CCallback();
-
-	//client-specific functionalities (pathfinding)
-	virtual bool canMoveBetween(const int3 &a, const int3 &b);
-	virtual int3 getGuardingCreaturePosition(int3 tile);
-	virtual std::shared_ptr<const CPathsInfo> getPathsInfo(const CGHeroInstance * h);
-
-	std::optional<PlayerColor> getPlayerID() const override;
-
-	//Set of metrhods that allows adding more interfaces for this player that'll receive game event call-ins.
-	void registerBattleInterface(std::shared_ptr<IBattleEventsReceiver> battleEvents);
-	void unregisterBattleInterface(std::shared_ptr<IBattleEventsReceiver> battleEvents);
-
-//commands
-	bool moveHero(const CGHeroInstance *h, int3 dst, bool transit = false) override; //dst must be free, neighbouring tile (this function can move hero only by one tile)
-	bool teleportHero(const CGHeroInstance *who, const CGTownInstance *where);
-	int selectionMade(int selection, QueryID queryID) override;
-	int sendQueryReply(std::optional<int32_t> reply, QueryID queryID) override;
-	int swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override;
-	int mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; //first goes to the second
-	int mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; //first goes to the second
-	int splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val) override;
-	int bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) override;
-	int bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany = 1) override;
-	int bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot) override;
-	int bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) override;
-	bool dismissHero(const CGHeroInstance * hero) override;
-	bool swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2) override;
-	void assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) override;
-	void bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped = true, bool backpack = true) override;
-	void eraseArtifactByClient(const ArtifactLocation & al) override;
-	bool buildBuilding(const CGTownInstance *town, BuildingID buildingID) override;
-	void recruitCreatures(const CGDwelling * obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1) override;
-	bool dismissCreature(const CArmedInstance *obj, SlotID stackPos) override;
-	bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE) override;
-	void endTurn() override;
-	void swapGarrisonHero(const CGTownInstance *town) override;
-	void buyArtifact(const CGHeroInstance *hero, ArtifactID aid) override;
-	void trade(const IMarket * market, EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr) override;
-	void trade(const IMarket * market, EMarketMode mode, const std::vector<ui32> & id1, const std::vector<ui32> & id2, const std::vector<ui32> & val1, const CGHeroInstance * hero = nullptr) override;
-	void setFormation(const CGHeroInstance * hero, bool tight) override;
-	void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero) override;
-	void save(const std::string &fname) override;
-	void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) override;
-	void gamePause(bool pause) override;
-	void buildBoat(const IShipyard *obj) override;
-	void dig(const CGObjectInstance *hero) override;
-	void castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos = int3(-1, -1, -1)) override;
-
-//friends
-	friend class CClient;
-};
+/*
+ * CCallback.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/CGameInfoCallback.h"
+#include "lib/battle/CPlayerBattleCallback.h"
+#include "lib/int3.h" // for int3
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class CGHeroInstance;
+class CGameState;
+struct CPath;
+class CGObjectInstance;
+class CArmedInstance;
+class BattleAction;
+class CGTownInstance;
+class IShipyard;
+struct CGPathNode;
+struct CGPath;
+struct CPathsInfo;
+class PathfinderConfig;
+struct CPack;
+struct CPackForServer;
+class IBattleEventsReceiver;
+class IGameEventsReceiver;
+struct ArtifactLocation;
+class BattleStateInfoForRetreat;
+class IMarket;
+
+VCMI_LIB_NAMESPACE_END
+
+// in static AI build this file gets included into libvcmi
+#ifdef STATIC_AI
+VCMI_LIB_USING_NAMESPACE
+#endif
+
+class CClient;
+struct lua_State;
+
+class IBattleCallback
+{
+public:
+	virtual ~IBattleCallback() = default;
+
+	bool waitTillRealize = false; //if true, request functions will return after they are realized by server
+	bool unlockGsWhenWaiting = false;//if true after sending each request, gs mutex will be unlocked so the changes can be applied; NOTICE caller must have gs mx locked prior to any call to actiob callback!
+	//battle
+	virtual void battleMakeSpellAction(const BattleID & battleID, const BattleAction & action) = 0;
+	virtual void battleMakeUnitAction(const BattleID & battleID, const BattleAction & action) = 0;
+	virtual void battleMakeTacticAction(const BattleID & battleID, const BattleAction & action) = 0;
+	virtual std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) = 0;
+
+	virtual std::shared_ptr<CPlayerBattleCallback> getBattle(const BattleID & battleID) = 0;
+	virtual std::optional<PlayerColor> getPlayerID() const = 0;
+};
+
+class IGameActionCallback
+{
+public:
+	//hero
+	virtual bool moveHero(const CGHeroInstance *h, int3 dst, bool transit) =0; //dst must be free, neighbouring tile (this function can move hero only by one tile)
+	virtual bool dismissHero(const CGHeroInstance * hero)=0; //dismisses given hero; true - successfuly, false - not successfuly
+	virtual void dig(const CGObjectInstance *hero)=0;
+	virtual void castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos = int3(-1, -1, -1))=0; //cast adventure map spell
+
+	//town
+	virtual void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero)=0;
+	virtual bool buildBuilding(const CGTownInstance *town, BuildingID buildingID)=0;
+	virtual void recruitCreatures(const CGDwelling *obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1)=0;
+	virtual bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE)=0; //if newID==-1 then best possible upgrade will be made
+	virtual void swapGarrisonHero(const CGTownInstance *town)=0;
+
+	virtual void trade(const IMarket * market, EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr)=0; //mode==0: sell val1 units of id1 resource for id2 resiurce
+	virtual void trade(const IMarket * market, EMarketMode mode, const std::vector<ui32> & id1, const std::vector<ui32> & id2, const std::vector<ui32> & val1, const CGHeroInstance * hero = nullptr)=0;
+
+	virtual int selectionMade(int selection, QueryID queryID) =0;
+	virtual int sendQueryReply(std::optional<int32_t> reply, QueryID queryID) =0;
+	virtual int swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)=0;//swaps creatures between two possibly different garrisons // TODO: AI-unsafe code - fix it!
+	virtual int mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)=0;//joins first stack to the second (creatures must be same type)
+	virtual int mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) =0; //first goes to the second
+	virtual int splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val)=0;//split creatures from the first stack
+	//virtual bool swapArtifacts(const CGHeroInstance * hero1, ui16 pos1, const CGHeroInstance * hero2, ui16 pos2)=0; //swaps artifacts between two given heroes
+	virtual bool swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2)=0;
+	virtual void assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo)=0;
+	virtual void eraseArtifactByClient(const ArtifactLocation & al)=0;
+	virtual bool dismissCreature(const CArmedInstance *obj, SlotID stackPos)=0;
+	virtual void endTurn()=0;
+	virtual void buyArtifact(const CGHeroInstance *hero, ArtifactID aid)=0; //used to buy artifacts in towns (including spell book in the guild and war machines in blacksmith)
+	virtual void setFormation(const CGHeroInstance * hero, bool tight)=0;
+
+	virtual void save(const std::string &fname) = 0;
+	virtual void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) = 0;
+	virtual void gamePause(bool pause) = 0;
+	virtual void buildBoat(const IShipyard *obj) = 0;
+
+	// To implement high-level army management bulk actions
+	virtual int bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) = 0;
+	virtual int bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany = 1) = 0;
+	virtual int bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot) = 0;
+	virtual int bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) = 0;
+	
+	
+	// Moves all artifacts from one hero to another
+	virtual void bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack) = 0;
+};
+
+class CBattleCallback : public IBattleCallback
+{
+	std::map<BattleID, std::shared_ptr<CPlayerBattleCallback>> activeBattles;
+
+	std::optional<PlayerColor> player;
+
+protected:
+	int sendRequest(const CPackForServer * request); //returns requestID (that'll be matched to requestID in PackageApplied)
+	CClient *cl;
+
+public:
+	CBattleCallback(std::optional<PlayerColor> player, CClient * C);
+	void battleMakeSpellAction(const BattleID & battleID, const BattleAction & action) override;//for casting spells by hero - DO NOT use it for moving active stack
+	void battleMakeUnitAction(const BattleID & battleID, const BattleAction & action) override;
+	void battleMakeTacticAction(const BattleID & battleID, const BattleAction & action) override; // performs tactic phase actions
+	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override;
+
+	std::shared_ptr<CPlayerBattleCallback> getBattle(const BattleID & battleID) override;
+	std::optional<PlayerColor> getPlayerID() const override;
+
+	void onBattleStarted(const IBattleInfo * info);
+	void onBattleEnded(const BattleID & battleID);
+
+	friend class CCallback;
+	friend class CClient;
+};
+
+class CCallback : public CPlayerSpecificInfoCallback, public CBattleCallback, public IGameActionCallback
+{
+public:
+	CCallback(CGameState * GS, std::optional<PlayerColor> Player, CClient * C);
+	virtual ~CCallback();
+
+	//client-specific functionalities (pathfinding)
+	virtual bool canMoveBetween(const int3 &a, const int3 &b);
+	virtual int3 getGuardingCreaturePosition(int3 tile);
+	virtual std::shared_ptr<const CPathsInfo> getPathsInfo(const CGHeroInstance * h);
+
+	std::optional<PlayerColor> getPlayerID() const override;
+
+	//Set of metrhods that allows adding more interfaces for this player that'll receive game event call-ins.
+	void registerBattleInterface(std::shared_ptr<IBattleEventsReceiver> battleEvents);
+	void unregisterBattleInterface(std::shared_ptr<IBattleEventsReceiver> battleEvents);
+
+//commands
+	bool moveHero(const CGHeroInstance *h, int3 dst, bool transit = false) override; //dst must be free, neighbouring tile (this function can move hero only by one tile)
+	bool teleportHero(const CGHeroInstance *who, const CGTownInstance *where);
+	int selectionMade(int selection, QueryID queryID) override;
+	int sendQueryReply(std::optional<int32_t> reply, QueryID queryID) override;
+	int swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override;
+	int mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; //first goes to the second
+	int mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; //first goes to the second
+	int splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val) override;
+	int bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) override;
+	int bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany = 1) override;
+	int bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot) override;
+	int bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) override;
+	bool dismissHero(const CGHeroInstance * hero) override;
+	bool swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2) override;
+	void assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) override;
+	void bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped = true, bool backpack = true) override;
+	void eraseArtifactByClient(const ArtifactLocation & al) override;
+	bool buildBuilding(const CGTownInstance *town, BuildingID buildingID) override;
+	void recruitCreatures(const CGDwelling * obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1) override;
+	bool dismissCreature(const CArmedInstance *obj, SlotID stackPos) override;
+	bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE) override;
+	void endTurn() override;
+	void swapGarrisonHero(const CGTownInstance *town) override;
+	void buyArtifact(const CGHeroInstance *hero, ArtifactID aid) override;
+	void trade(const IMarket * market, EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr) override;
+	void trade(const IMarket * market, EMarketMode mode, const std::vector<ui32> & id1, const std::vector<ui32> & id2, const std::vector<ui32> & val1, const CGHeroInstance * hero = nullptr) override;
+	void setFormation(const CGHeroInstance * hero, bool tight) override;
+	void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero) override;
+	void save(const std::string &fname) override;
+	void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) override;
+	void gamePause(bool pause) override;
+	void buildBoat(const IShipyard *obj) override;
+	void dig(const CGObjectInstance *hero) override;
+	void castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos = int3(-1, -1, -1)) override;
+
+//friends
+	friend class CClient;
+};

+ 2160 - 2160
ChangeLog.md

@@ -1,2160 +1,2160 @@
-# 1.3.1 -> 1.3.2
-
-### GENERAL
-* VCMI now uses new application icon
-* Added initial version of Czech translation
-* Game will now use tile hero is moving from for movement cost calculations, in line with H3
-* Added option to open hero backpack window in hero screen
-* Added detection of misclicks for touch inputs to make hitting small UI elements easier
-* Hero commander will now gain option to learn perks on reaching master level in corresponding abilities
-* It is no longer possible to stop movement while moving over water with Water Walk
-* Game will now automatically update hero path if it was blocked by another hero
-* Added "vcmiartifacts angelWings" form to "give artifacts" cheat
-
-### STABILITY
-* Fixed freeze in Launcher on repository checkout and on mod install
-* Fixed crash on loading VCMI map with placed Abandoned Mine
-* Fixed crash on loading VCMI map with neutral towns
-* Fixed crash on attempting to visit unknown object, such as Market of Time
-* Fixed crash on attempting to teleport unit that is immune to a spell
-* Fixed crash on switching fullscreen mode during AI turn
-
-### CAMPAIGNS
-* Fixed reorderging of hero primary skills after moving to next scenario in campaigns
-
-### BATTLES
-* Conquering a town will now correctly award additional 500 experience points
-* Quick combat is now enabled by default
-* Fixed invisible creatures from SUMMON_GUARDIANS and TRANSMUTATION bonuses
-* Added option to toggle spell usage by AI in quick combat
-* Fixed updating of spell point of enemy hero in game interface after spell cast
-* Fixed wrong creature spellcasting shortcut (now set to "F")
-* It is now possible to perform melee attack by creatures with spells, especially area spells
-* Right-click will now properly end spellcast mode
-* Fixed cursor preview when casting spell using touchscreen
-* Long tap during spell casting will now properly abort the spell
-
-### INTERFACE
-* Added "Fill all empty slots with 1 creature" option to radial wheel in garrison windows
-* Context popup for adventure map monsters will now show creature icon
-* Game will now show correct victory message for gather troops victory condition
-* Fixed incorrect display of number of owned Sawmills in Kingdom Overview window
-* Fixed incorrect color of resource bar in hotseat mode
-* Fixed broken toggle map level button in world view mode
-* Fixed corrupted interface after opening puzzle window from world view mode
-* Fixed blocked interface after attempt to start invalid map
-* Add yellow border to selected commander grandmaster ability
-* Always use bonus description for commander abilities instead of not provided wog-specific translation  
-* Fix scrolling when commander has large number of grandmaster abilities
-* Fixed corrupted message on another player defeat
-* Fixed unavailable Quest Log button on maps with quests
-* Fixed incorrect values on a difficulty selector in save load screen
-* Removed invalid error message on attempting to move non-existing unit in exchange window
-
-### RANDOM MAP GENERATOR
-* Fixed bug leading to unreachable resources around mines
-
-### MAP EDITOR
-* Fixed crash on maps containing abandoned mines
-* Fixed crash on maps containing neutral objects
-* Fixed problem with random map initialized in map editor
-* Fixed problem with initialization of random dwellings
-
-# 1.3.0 -> 1.3.1
-
-### GENERAL:
-* Fixed framerate drops on hero movement with active hota mod
-* Fade-out animations will now be skipped when instant hero movement speed is used
-* Restarting loaded campaing scenario will now correctly reapply starting bonus
-* Reverted FPS limit on mobile systems back to 60 fps
-* Fixed loading of translations for maps and campaigns
-* Fixed loading of preconfigured starting army for heroes with preconfigured spells
-* Background battlefield obstacles will now appear below creatures
-* it is now possible to load save game located inside mod
-* Added option to configure reserved screen area in Launcher on iOS
-* Fixed border scrolling when game window is maximized
-
-### AI PLAYER:
-* BattleAI: Improved performance of AI spell selection
-* NKAI: Fixed freeze on attempt to exchange army between garrisoned and visiting hero
-* NKAI: Fixed town threat calculation
-* NKAI: Fixed recruitment of new heroes
-* VCAI: Added workaround to avoid freeze on attempting to reach unreachable location
-* VCAI: Fixed spellcasting by Archangels
-
-### RANDOM MAP GENERATOR:
-* Fixed placement of roads inside rock in underground
-* Fixed placement of shifted creature animations from HotA
-* Fixed placement of treasures at the boundary of wide connections
-* Added more potential locations for quest artifacts in zone
-
-### STABILITY:
-* When starting client without H3 data game will now show message instead of silently crashing
-* When starting invalid map in campaign, game will now show message instead of silently crashing
-* Blocked loading of saves made with different set of mods to prevent crashes
-* Fixed crash on starting game with outdated mods
-* Fixed crash on attempt to sacrifice all your artifacts in Altar of Sacrifice
-* Fixed crash on leveling up after winning battle as defender
-* Fixed possible crash on end of battle opening sound
-* Fixed crash on accepting battle result after winning battle as defender
-* Fixed possible crash on casting spell in battle by AI
-* Fixed multiple possible crashes on managing mods on Android
-* Fixed multiple possible crashes on importing data on Android
-* Fixed crash on refusing rewards from town building
-* Fixed possible crash on threat evaluation by NKAI
-* Fixed crash on using haptic feedback on some Android systems
-* Fixed crash on right-clicking flags area in RMG setup mode
-* Fixed crash on opening Blacksmith window and Build Structure dialogs in some localizations
-* Fixed possible crash on displaying animated main menu
-* Fixed crash on recruiting hero in town located on the border of map
-
-# 1.2.1 -> 1.3.0
-
-### GENERAL:
-* Implemented automatic interface scaling to any resolution supported by monitor
-* Implemented UI scaling option to scale game interface
-* Game resolution and UI scaling can now be changed without game restart
-* Fixed multiple issues with borderless fullscreen mode
-* On mobile systems game will now always run at native resolution with configurable UI scaling
-* Implemented support for Horn of the Abyss map format
-* Implemented option to replay results of quick combat
-* Added translations to French and Chinese
-* All in-game cheats are now case-insensitive
-* Added high-definition icon for Windows
-* Fix crash on connecting to server on FreeBSD and Flatpak builds
-* Save games now consist of a single file
-* Added H3:SOD cheat codes as alternative to vcmi cheats
-* Fixed several possible crashes caused by autocombat activation
-* Fixed artifact lock icon in localized versions of the game
-* Fixed possible crash on changing hardware cursor
-
-### TOUCHSCREEN SUPPORT:
-* VCMI will now properly recognizes touch screen input
-* Implemented long tap gesture that shows popup window. Tap once more to close popup
-* Long tap gesture duration can now be configured in settings
-* Implemented radial menu for army management, activated via swiping creature icon
-* Implemented swipe gesture for scrolling through lists
-* All windows that have sliders in UI can now be scrolled using swipe gesture
-* Implemented swipe gesture for attack direction selection: swipe from enemy position to position you want to attack from
-* Implemented pinch gesture for zooming adventure map
-* Implemented haptic feedback (vibration) for long press gesture
-
-### LAUNCHER:
-* Launcher will now attempt to automatically detect language of OS on first launch
-* Added "About" tab with information about project and environment
-* Added separate options for Allied AI and Enemy AI for adventure map
-* Patially fixed displaying of download progress for mods
-* Fixed potential crash on opening mod information for mods with a changelog
-* Added option to configure number of autosaves
-
-### MAP EDITOR:
-* Fixed crash on cutting random town
-* Added option to export entire map as an image
-* Added validation for placing multiple heroes into starting town
-* It is now possible to have single player on a map
-* It is now possible to configure teams in editor
-
-### AI PLAYER:
-* Fixed potential crash on accessing market (VCAI)
-* Fixed potentially infinite turns (VCAI)
-* Reworked object prioritizing
-* Improved town defense against enemy heroes
-* Improved town building (mage guild and horde)
-* Various behavior fixes
-
-### GAME MECHANICS
-* Hero retreating after end of 7th turn will now correctly appear in tavern
-* Implemented hero backpack limit (disabled by default)
-* Fixed Admiral's Hat movement points calculation
-* It is now possible to access Shipwrecks from coast
-* Hero path will now be correctly updated on equipping/unequipping Levitation Boots or Angel Wings
-* It is no longer possible to abort movement while hero is flying over water
-* Fixed digging for Grail
-* Implemented "Survive beyond a time limit" victory condition
-* Implemented "Defeat all monsters" victory condition
-* 100% damage resistance or damage reduction will make unit immune to a spell
-* Game will now randomly select obligatory skill for hero on levelup instead of always picking Fire Magic
-* Fixed duration of bonuses from visitable object such as Idol of Fortune
-* Rescued hero from prison will now correctly reveal map around him
-* Lighthouses will no longer give movement bonus on land
-
-### CAMPAIGNS:
-* Fixed transfer of artifacts into next scenario
-* Fixed crash on advancing to next scenario with heroes from mods
-* Fixed handling of "Start with building" campaign bonus
-* Fixed incorrect starting level of heroes in campaigns
-* Game will now play correct music track on scenario selection window
-* Dracon woll now correctly start without spellbook in Dragon Slayer campaign
-* Fixed frequent crash on moving to next scenario during campaign
-* Fixed inability to dismiss heroes on maps with "capture town" victory condition
-
-### RANDOM MAP GENERATOR:
-* Improved zone placement, shape and connections
-* Improved zone passability for better gameplay
-* Improved treasure distribution and treasure values to match SoD closely
-* Navigation and water-specific spells are now banned on maps without water
-* RMG will now respect road settings set in menu
-* Tweaked many original templates so they allow new terrains and factions
-* Added "bannedTowns", "bannedTerrains", "bannedMonsters" zone properties
-* Added "road" property to connections
-* Added monster strength "none"
-* Support for "wide" connections
-* Support for new "fictive" and "repulsive" connections
-* RMG will now run faster, utilizing many CPU cores
-* Removed random seed number from random map description
-
-### INTERFACE:
-* Adventure map is now scalable and can be used with any resolution without mods
-* Adventure map interface is now correctly blocked during enemy turn
-* Visiting creature banks will now show amount of guards in bank
-* It is now possible to arrange army using status window
-* It is now possible to zoom in or out using mouse wheel or pinch gesture
-* It is now possible to reset zoom via Backspace hotkey
-* Receiving a message in chat will now play sound
-* Map grid will now correctly display on map start
-* Fixed multiple issues with incorrect updates of save/load game screen
-* Fixed missing fortifications level icon in town tooltip
-* Fixed positioning of resource label in Blacksmith window
-* Status bar on inactive windows will no longer show any tooltip from active window
-* Fixed highlighting of possible artifact placements when exchanging with allied hero
-* Implemented sound of flying movement (for Fly spell or Angel Wings)
-* Last symbol of entered cheat/chat message will no longer trigger hotkey
-* Right-clicking map name in scenario selection will now show file name
-* Right-clicking save game in save/load screen will now show file name and creation date
-* Right-clicking in town fort window will now show creature information popup
-* Implemented pasting from clipboard (Ctrl+V) for text input
-
-### BATTLES:
-* Implemented Tower moat (Land Mines)
-* Implemented defence reduction for units in moat
-* Added option to always show hero status window
-* Battle opening sound can now be skipped with mouse click
-* Fixed movement through moat of double-hexed units
-* Fixed removal of Land Mines and Fire Walls
-* Obstacles will now corectly show up either below or above unit
-* It is now possible to teleport a unit through destroyed walls
-* Added distinct overlay image for showing movement range of highlighted unit
-* Added overlay for displaying shooting range penalties of units
-
-### MODDING:
-* Implemented initial version of VCMI campaign format
-* Implemented spell cast as possible reward for configurable object
-* Implemented support for configurable buildings in towns
-* Implemented support for placing prison, tavern and heroes on water
-* Implemented support for new boat types
-* It is now possible for boats to use other movement layers, such as "air"
-* It is now possible to use growing artifacts on artifacts that can be used by hero
-* It is now possible to configure town moat
-* Palette-cycling animation of terrains and rivers can now be configured in json
-* Game will now correctly resolve identifier in unexpected form (e.g. 'bless' vs 'spell.bless' vs 'core:bless')
-* Creature specialties that use short form ( "creature" : "pikeman" ) will now correctly affect all creature upgrades
-* It is now possible to configure spells for Shrines
-* It is now possible to configure upgrade costs per level for Hill Forts
-* It is now possible to configure boat type for Shipyards on adventure map and in town
-* Implemented support for HotA-style adventure map images for monsters, with offset
-* Replaced (SCHOOL)_SPELL_DMG_PREMY with SPELL_DAMAGE bonus (uses school as subtype).
-* Removed bonuses (SCHOOL)_SPELLS - replaced with SPELLS_OF_SCHOOL
-* Removed DIRECT_DAMAGE_IMMUNITY bonus - replaced by 100% spell damage resistance
-* MAGIC_SCHOOL_SKILL subtype has been changed for consistency with other spell school bonuses
-* Configurable objects can now be translated
-* Fixed loading of custom battlefield identifiers for map objects
-
-# 1.2.0 -> 1.2.1
-
-### GENERAL:
-* Implemented spell range overlay for Dimension Door and Scuttle Boat
-* Fixed movement cost penalty from terrain
-* Fixed empty Black Market on game start
-* Fixed bad morale happening after waiting
-* Fixed good morale happening after defeating last enemy unit
-* Fixed death animation of Efreeti killed by petrification attack
-* Fixed crash on leaving to main menu from battle in hotseat mode
-* Fixed music playback on switching between towns
-* Special months (double growth and plague) will now appear correctly
-* Adventure map spells are no longer visible on units in battle
-* Attempt to cast spell with no valid targets in hotseat will show appropriate error message
-* RMG settings will now show all existing in game templates and not just those suitable for current settings
-* RMG settings (map size and two-level maps) that are not compatible with current template will be blocked
-* Fixed centering of scenario information window
-* Fixed crash on empty save game list after filtering
-* Fixed blocked progress in Launcher on language detection failure
-* Launcher will now correctly handle selection of Ddata directory in H3 install
-* Map editor will now correctly save message property for events and pandoras
-* Fixed incorrect saving of heroes portraits in editor
-
-# 1.1.1 -> 1.2.0
-
-### GENERAL:
-* Adventure map rendering was entirely rewritten with better, more functional code
-* Client battle code was heavily reworked, leading to better visual look & feel and fixing multiple minor battle bugs / glitches
-* Client mechanics are now framerate-independent, rather than speeding up with higher framerate
-* Implemented hardware cursor support
-* Heroes III language can now be detected automatically
-* Increased targeted framerate from 48 to 60
-* Increased performance of UI updates
-* Fixed bonus values of heroes who specialize in secondary skills
-* Fixed bonus values of heroes who specialize in creatures
-* Fixed damage increase from Adela's Bless specialty
-* Fixed missing obstacles in battles on subterranean terrain 
-* Video files now play at correct speed
-* Fixed crash on switching to second mission in campaigns
-* New cheat code: vcmiazure - give 5000 azure dragons in every empty slot
-* New cheat code: vcmifaerie - give 5000 faerie dragons in every empty slot
-* New cheat code: vcmiarmy or vcminissi - give specified creatures in every empty slot. EG: vcmiarmy imp
-* New cheat code: vcmiexp or vcmiolorin - give specified amount of experience to current hero. EG: vcmiexp 10000
-* Fixed oversided message window from Scholar skill that had confirmation button outside game window
-* Fixed loading of prebuilt creature hordes from h3m maps
-* Fixed volume of ambient sounds when changing game sounds volume
-* Fixed might&magic affinities of Dungeon heroes
-* Fixed Roland's specialty to affect Swordsmen/Crusaders instead of Griffins
-* Buying boat in town of an ally now correctly uses own resources instead of stealing them from ally
-* Default game difficulty is now set to "normal" instead of "easy"
-* Fixed crash on missing music files
-
-### MAP EDITOR:
-* Added translations to German, Polish, Russian, Spanish, Ukrainian
-* Implemented cut/copy/paste operations
-* Implemented lasso brush for terrain editing
-* Toolbar actions now have names
-* Added basic victory and lose conditions
-
-### LAUNCHER:
-* Added initial Welcome/Setup screen for new players
-* Added option to install translation mod if such mod exists and player's H3 version has different language
-* Icons now have higher resolution, to prevent upscaling artifacts
-* Added translations to German, Polish, Russian, Spanish, Ukrainian
-* Mods tab layout has been adjusted based on feedback from players
-* Settings tab layout has been redesigned to support longer texts
-* Added button to start map editor directly from Launcher
-* Simplified game starting flow from online lobby
-* Mod description will now show list of languages supported by mod
-* Launcher now uses separate mod repository from vcmi-1.1 version to prevent mod updates to unsupported versions
-* Size of mod list and mod details sub-windows can now be adjusted by player
-
-### AI PLAYER:
-* Nullkiller AI is now used by default
-* AI should now be more active in destroying heroes causing treat on AI towns
-* AI now has higher priority for resource-producing mines
-* Increased AI priority of town dwelling upgrades
-* AI will now de-prioritize town hall upgrades when low on resources
-* Messages from cheats used by AI are now hidden
-* Improved army gathering from towns
-* AI will now attempt to exchange armies between main heroes to get the strongest hero with the strongest army.
-* Improved Pandora handling
-* AI takes into account fort level now when evaluating enemy town capturing priority.
-* AI can not use allied shipyard now to avoid freeze
-* AI will avoid attacking creatures standing on draw-bridge tile during siege if the bridge is closed.
-* AI will consider retreat during siege if it can not do anything (catapult is destroyed, no destroyed walls exist)
-
-### RANDOM MAP GENERATOR
-* Random map generator can now be used without vcmi-extras mod
-* RMG will no longer place shipyards or boats at very small lakes
-* Fixed placement of shipyards in invalid locations
-* Fixed potential game hang on generation of random map
-* RMG will now generate addditional monolith pairs to create required number of zone connections
-* RMG will try to place Subterranean Gates as far away from other objects (including each other) as possible
-* RMG will now try to place objects as far as possible in both zones sharing a guard, not only the first one.
-* Use only one template for an object in zone
-* Objects with limited per-map count will be distributed evenly among zones with suitable terrain
-* Objects above zone treasure value will not be considered for placement
-* RMG will prefer terrain-specific templates for objects placement
-* RMG will place Towns and Monoliths first in order to generate long roads across the zone.
-* Adjust the position of center town in the zone for better look & feel on S maps.
-* Description of random map will correctly show number of levels
-* Fixed amount of creatures found in Pandora Boxes to match H3
-* Visitable objects will no longer be placed on top of the map, obscured by map border
-
-### ADVENTURE MAP:
-* Added option to replace popup messages on object visiting with messages in status window
-* Implemented different hero movement sounds for offroad movement
-* Cartographers now reveal terrain in the same way as in H3
-* Status bar will now show movement points information on pressing ALT or after enabling option in settings
-* It is now not possible to receive rewards from School of War without required gold amount
-* Owned objects, like Mines and Dwellings will always show their owner in status bar
-* It is now possible to interact with on-map Shipyard when no hero is selected
-* Added option to show amount of creatures as numeric range rather than adjective
-* Added option to show map grid
-* Map swipe is no longer exclusive for phones and can be enabled on desktop platforms
-* Added more graduated settigns for hero movement speed
-* Map scrolling is now more graduated and scrolls with pixel-level precision
-* Hero movement speed now matches H3
-* Improved performance of adventure map rendering
-* Fixed embarking and disembarking sounds
-* Fixed selection of "new week" animation for status window
-* Object render order now mostly matches H3
-* Fixed movement cost calculation when using "Fly" spell or "Angel Wings"
-* Fixed game freeze on using Town Portal to teleport into town with unvisited Battle Scholar Academy
-* Fixed invalid ambient sound of Whirlpool
-* Hero path will now be correctly removed on defeating monsters that are at the end of hero path
-* Seer Hut tooltips will now show messages for correct quest type
-
-### INTERFACE
-* Implemented new settings window
-* Added framerate display option
-* Fixed white status bar on server connection screen
-* Buttons in battle window now correctly show tooltip in status bar
-* Fixed cursor image during enemy turn in combat
-* Game will no longer promt to assemble artifacts if they fall into backpack
-* It is now possible to use in-game console for vcmi commands
-* Stacks sized 1000-9999 units will not be displayed as "1k"
-* It is now possible to select destination town for Town Portal via double-click
-* Implemented extended options for random map tab: generate G+U size, select RMG template, manage teams and roads
-
-### HERO SCREEN
-* Fixed cases of incorrect artifact slot highlighting
-* Improved performance of artifact exchange operation
-* Picking up composite artifact will immediately unlock slots
-* It is now possible to swap two composite artifacts
-
-### TOWN SCREEN
-* Fixed gradual fade-in of a newly built building
-* Fixed duration of building fade-in to match H3
-* Fixed rendering of Shipyard in Castle
-* Blacksmith purchase button is now properly locked if artifact slot is occupied by another warmachine
-* Added option to show number of available creatures in place of growth
-* Fixed possible interaction with hero / town list from adventure map while in town screen
-* Fixed missing left-click message popup for some town buildings
-* Moving hero from garrison by pressing space will now correctly show message "Cannot have more than 8 adventuring heroes"
-
-### BATTLES:
-* Added settings for even faster animation speed than in H3
-* Added display of potential kills numbers into attack tooltip in status bar
-* Added option to skip battle opening music entirely
-* All effects will now wait for battle opening sound before playing
-* Hex highlighting will now be disabled during enemy turn
-* Fixed incorrect log message when casting spell that kills zero units
-* Implemented animated cursor for spellcasting
-* Fixed multiple issues related to ordering of creature animations
-* Fixed missing flags from hero animations when opening menus
-* Fixed rendering order of moat and grid shadow
-* Jousting bonus from Champions will now be correctly accounted for in damage estimation
-* Building Castle building will now provide walls with additional health point
-* Speed of all battle animations should now match H3
-* Fixed missing obstacles on subterranean terrain
-* Ballistics mechanics now matches H3 logic
-* Arrow Tower base damage should now match H3
-* Destruction of wall segments will now remove ranged attack penalty
-* Force Field cast in front of drawbridge will now block it as in H3
-* Fixed computations for Behemoth defense reduction ability 
-* Bad luck (if enabled) will now multiple all damage by 50%, in line with other damage reducing mechanics
-* Fixed highlighting of movement range for creatures standing on a corpse
-* All battle animations now have same duration/speed as in H3
-* Added missing combat log message on resurrecting creatures
-* Fixed visibility of blue border around targeted creature when spellcaster is making turn
-* Fixed selection highlight when in targeted creature spellcasting mode
-* Hovering over hero now correctly shows hero cursor
-* Creature currently making turn is now highlighted in the Battle Queue 
-* Hovering over creature icon in Battle Queue will highlight this creature in the battlefield
-* New battle UI extension allows control over creatures' special abilities
-* Fixed crash on activating auto-combat in battle
-* Fixed visibility of unit creature amount labels and timing of their updates
-* Firewall will no longer hit double-wide units twice when passing through
-* Unicorn Magic Damper Aura ability now works multiplicatively with Resistance
-* Orb of Vulnerability will now negate Resistance skill
-
-### SPELLS:
-* Hero casting animation will play before spell effect
-* Fire Shield: added sound effect
-* Fire Shield: effect now correctly plays on defending creature
-* Earthquake: added sound effect
-* Earthquake: spell will not select sections that were already destroyed before cast
-* Remove Obstacles: fixed error message when casting on maps without obstacles
-* All area-effect spells (e.g. Fireball) will play their effect animation on top
-* Summoning spells: added fade-in effect for summoned creatures
-* Fixed timing of hit animation for damage-dealing spells
-* Obstacle-creating spells: UI is now locked during effect animation
-* Obstacle-creating spells: added sound effect
-* Added reverse death animation for spells that bring stack back to life
-* Bloodlust: implemented visual effect
-* Teleport: implemented visual fade-out and fade-in effect for teleporting
-* Berserk: Fixed duration of effect
-* Frost Ring: Fixed spell effect range
-* Fixed several cases where multiple different effects could play at the same time
-* All spells that can affecte multiple targets will now highlight affected stacks
-* Bless and Curse now provide +1 or -1 to base damage on Advanced & Expert levels
-
-### ABILITIES:
-* Rebirth (Phoenix): Sound will now play in the same time as animation effect
-* Master Genie spellcasting: Sound will now play in the same time as animation effect
-* Power Lich, Magogs: Sound will now play in the same time as attack animation effect
-* Dragon Breath attack now correctly uses different attack animation if multiple targets are hit
-* Petrification: implemented visual effect
-* Paralyze: added visual effect
-* Blind: Stacks will no longer retailate on attack that blinds them
-* Demon Summon: Added animation effect for summoning
-* Fire shield will no longer trigger on non-adjacent attacks, e.g. from Dragon Breath
-* Weakness now has correct visual effect 
-* Added damage bonus for opposite elements for Elementals
-* Added damage reduction for Magic Elemental attacks against creatures immune to magic
-* Added incoming damage reduction to Petrify
-* Added counter-attack damage reduction for Paralyze
-
-### MODDING:
-* All configurable objects from H3 now have their configuration in json
-* Improvements to functionality of configurable objects
-* Replaced `SECONDARY_SKILL_PREMY` bonus with separate bonuses for each skill.
-* Removed multiple bonuses that can be replaced with another bonus.
-* It is now possible to define new hero movement sounds in terrains
-* Implemented translation support for mods
-* Implemented translation support for .h3m maps and .h3c campaigns
-* Translation mods are now automatically disabled if player uses different language
-* Files with new Terrains, Roads and Rivers are now validated by game
-* Parameters controlling effect of attack and defences stats on damage are now configurable in defaultMods.json
-* New bonus: `LIMITED_SHOOTING_RANGE`. Creatures with this bonus can only use ranged attack within specified range
-* Battle window and Random Map Tab now have their layout defined in json file
-* Implemented code support for alternative actions mod
-* Implemented code support for improved random map dialog
-* It is now possible to configure number of creature stacks in heroes' starting armies
-* It is now possible to configure number of constructed dwellings in towns on map start
-* Game settings previously located in defaultMods.json are now loaded directly from mod.json
-* It is now possible for spellcaster units to have multiple spells (but only for targeting different units)
-* Fixed incorrect resolving of identifiers in commander abilities and stack experience definitions
-
-# 1.1.0 -> 1.1.1
-
-### GENERAL:
-* Fixed missing sound in Polish version from gog.com 
-* Fixed positioning of main menu buttons in localized versions of H3
-* Fixed crash on transferring artifact to commander
-* Fixed game freeze on receiving multiple artifact assembly dialogs after combat
-* Fixed potential game freeze on end of music playback
-* macOS/iOS: fixed sound glitches
-* Android: upgraded version of SDL library
-* Android: reworked right click gesture and relative pointer mode
-* Improved map loading speed
-* Ubuntu PPA: game will no longer crash on assertion failure
-
-### ADVENTURE MAP:
-* Fixed hero movement lag in single-player games
-* Fixed number of drowned troops on visiting Sirens to match H3
-* iOS: pinch gesture visits current object (Spacebar behavior) instead of activating in-game console
-
-### TOWNS:
-* Fixed displaying growth bonus from Statue of Legion
-* Growth bonus tooltip ordering now matches H3
-* Buy All Units dialog will now buy units starting from the highest level
-
-### LAUNCHER:
-* Local mods can be disabled or uninstalled
-* Fixed styling of Launcher interface
-
-### MAP EDITOR:
-* Fixed saving of roads and rivers
-* Fixed placement of heroes on map
-
-# 1.0.0 -> 1.1.0
-
-### GENERAL:
-* iOS is supported
-* Mods and their versions and serialized into save files. Game checks mod compatibility before loading
-* Logs are stored in system default logs directory
-* LUA/ERM libs are not compiled by default
-* FFMpeg dependency is optional now
-* Conan package manager is supported for MacOS and iOS
-
-### MULTIPLAYER:
-* Map is passed over network, so different platforms are compatible with each other
-* Server self-killing is more robust
-* Unlock in-game console while opponent's turn
-* Host can control game session by using console commands
-* Control over player is transferred to AI if client escaped the game
-* Reconnection mode for crashed client processes
-* Playing online is available using proxy server
-
-### ADVENTURE MAP:
-* Fix for digging while opponent's turn
-* Supported right click for quick recruit window
-* Fixed problem with quests are requiring identical artefacts
-* Bulk move and swap artifacts
-* Pause & resume for towns and terrains music themes
-* Feature to assemble/disassemble artefacts in backpack
-* Clickable status bar to send messages
-* Heroes no longer have chance to receive forbidden skill on leveling up
-* Fixed visibility of newly recruited heroes near town 
-* Fixed missing artifact slot in Artifact Merchant window
-
-### BATTLES:
-* Fix healing/regeneration behaviour and effect
-* Fix crashes related to auto battle
-* Implemented ray projectiles for shooters
-* Introduced default tower shooter icons
-* Towers destroyed during battle will no longer be listed as casualties
-
-### AI:
-* BattleAI: Target prioritizing is now based on damage difference instead of health difference
-* Nullkiller AI can retreat and surrender
-* Nullkiller AI doesn't visit allied dwellings anymore
-* Fixed a few freezes in Nullkiller AI
-
-### RANDOM MAP GENERATOR:
-* Speedup generation of random maps
-* Necromancy cannot be learned in Witch Hut on random maps
-
-### MODS:
-* Supported rewardable objects customization
-* Battleground obstacles are extendable now with VLC mechanism
-* Introduced "compatibility" section into mods settings
-* Fixed bonus system for custom advmap spells
-* Supported customisable town entrance placement
-* Fixed validation of mods with new adventure map objects
-
-### LAUNCHER:
-* Fixed problem with duplicated mods in the list
-* Launcher shows compatible mods only
-* Uninstall button was moved to the left of layout
-* Unsupported resolutions are not shown
-* Lobby for online gameplay is implemented
-
-### MAP EDITOR:
-* Basic version of Qt-based map editor
-
-# 0.99 -> 1.0.0
-
-### GENERAL:
-* Spectator mode was implemented through command-line options
-* Some main menu settings get saved after returning to main menu - last selected map, save etc.
-* Restart scenario button should work correctly now
-* Skyship Grail works now immediately after capturing without battle
-* Lodestar Grail implemented
-* Fixed Gargoyles immunity
-* New bonuses:
-* * SOUL_STEAL - "WoG ghost" ability, should work somewhat same as in H3
-* * TRANSMUTATION - "WoG werewolf"-like ability
-* * SUMMON_GUARDIANS - "WoG santa gremlin"-like ability + two-hex unit extension
-* * CATAPULT_EXTRA_SHOTS - defines number of extra wall attacks for units that can do so
-* * RANGED_RETALIATION - allows ranged counterattack
-* * BLOCKS_RANGED_RETALIATION - disallow enemy ranged counterattack
-* * SECONDARY_SKILL_VAL2 - set additional parameter for certain secondary skills
-* * MANUAL_CONTROL - grant manual control over war machine
-* * WIDE_BREATH - melee creature attacks affect many nearby hexes
-* * FIRST_STRIKE - creature counterattacks before attack if possible
-* * SYNERGY_TARGET - placeholder bonus for Mod Design Team (subject to removal in future)
-* * SHOOTS_ALL_ADJACENT - makes creature shots affect all neighbouring hexes
-* * BLOCK_MAGIC_BELOW - allows blocking spells below particular spell level. HotA cape artifact can be implemented with this
-* * DESTRUCTION - creature ability for killing extra units after hit, configurable
-
-### MULTIPLAYER:
-* Loading support. Save from single client could be used to load all clients.
-* Restart support. All clients will restart together on same server.
-* Hotseat mixed with network game. Multiple colors can be controlled by each client.
-
-### SPELLS:
-* Implemented cumulative effects for spells
-
-### MODS:
-* Improve support for WoG commander artifacts and skill descriptions
-* Added support for modding of original secondary skills and creation of new ones.
-* Map object sounds can now be configured via json
-* Added bonus updaters for hero specialties
-* Added allOf, anyOf and noneOf qualifiers for bonus limiters
-* Added bonus limiters: alignment, faction and terrain
-* Supported new terrains, new battlefields, custom water and rock terrains
-* Following special buildings becomes available in the fan towns:
-* * attackVisitingBonus
-* * defenceVisitingBonus
-* * spellPowerVisitingBonus
-* * knowledgeVisitingBonus
-* * experienceVisitingBonus
-* * lighthouse
-* * treasury
-
-### SOUND:
-* Fixed many mising or wrong pickup and visit sounds for map objects
-* All map objects now have ambient sounds identical to OH3
-
-### RANDOM MAP GENERATOR:
-* Random map generator supports water modes (normal, islands)
-* Added config randomMap.json with settings for map generator
-* Added parameter for template allowedWaterContent
-* Extra resource packs appear nearby mines
-* Underground can be player starting place for factions allowed to be placed underground
-* Improved obstacles placement aesthetics
-* Rivers are generated on the random maps
-* RMG works more stable, various crashes have been fixed
-* Treasures requiring guards are guaranteed to be protected
-
-### VCAI:
-* Reworked goal decomposition engine, fixing many loopholes. AI will now pick correct goals faster.
-* AI will now use universal pathfinding globally
-* AI can use Summon  Boat and Town Portal
-* AI can gather and save resources on purpose
-* AI will only buy army on demand instead of every turn
-* AI can distinguish the value of all map objects
-* General speed optimizations
-
-### BATTLES:
-* Towers should block ranged retaliation
-* AI can bypass broken wall with moat instead of standing and waiting until gate is destroyed
-* Towers do not attack war machines automatically
-* Draw is possible now as battle outcome in case the battle ends with only summoned creatures (both sides loose)
-
-### ADVENTURE MAP:
-* Added buttons and keyboard shortcuts to quickly exchange army and artifacts between heroes
-* Fix: Captured town should not be duplicated on the UI
-
-### LAUNCHER:
-* Implemented notifications about updates
-* Supported redirection links for downloading mods
-
-# 0.98 -> 0.99
-
-### GENERAL:
-* New Bonus NO_TERRAIN_PENALTY
-* Nomads will remove Sand movement penalty from army
-* Flying and water walking is now supported in pathfinder
-* New artifacts supported
-* * Angel Wings
-* * Boots of Levitation
-* Implemented rumors in tavern window
-* New cheat codes:
-* * vcmiglaurung - gives 5000 crystal dragons into each slot
-* * vcmiungoliant - conceal fog of war for current player
-* New console commands:
-* * gosolo - AI take control over human players and vice versa
-* * controlai - give control of one or all AIs to player
-* * set hideSystemMessages on/off - supress server messages in chat
-
-### BATTLES:
-* Drawbridge mechanics implemented (animation still missing)
-* Merging of town and visiting hero armies on siege implemented
-* Hero info tooltip for skills and mana implemented
-
-### ADVENTURE AI:
-* Fixed AI trying to go through underground rock
-* Fixed several cases causing AI wandering aimlessly
-* AI can again pick best artifacts and exchange artifacts between heroes
-* AI heroes with patrol enabled won't leave patrol area anymore
-
-### RANDOM MAP GENERATOR:
-* Changed fractalization algorithm so it can create cycles
-* Zones will not have straight paths anymore, they are totally random
-* Generated zones will have different size depending on template setting
-* Added Thieves Guild random object (1 per zone)
-* Added Seer Huts with quests that match OH3
-* RMG will guarantee at least 100 pairs of Monoliths are available even if there are not enough different defs
-
-# 0.97 -> 0.98
-
-### GENERAL:
-* Pathfinder can now find way using Monoliths and Whirlpools (only used if hero has protection)
-
-### ADVENTURE AI:
-* AI will try to use Monolith entrances for exploration
-* AI will now always revisit each exit of two way monolith if exit no longer visible
-* AI will eagerly pick guarded and blocked treasures
-
-### ADVENTURE MAP:
-* Implemented world view
-* Added graphical fading effects
-
-### SPELLS:
-* New spells handled:
-* * Earthquake
-* * View Air
-* * View Earth
-* * Visions
-* * Disguise
-* Implemented CURE spell negative dispell effect
-* Added LOCATION target for spells castable on any hex with new target modifiers
-
-### BATTLES:
-* Implemented OH3 stack split / upgrade formulas according to AlexSpl
-
-### RANDOM MAP GENERATOR:
-* Underground tunnels are working now
-* Implemented "junction" zone type
-* Improved zone placing algorithm
-* More balanced distribution of treasure piles
-* More obstacles within zones
-
-# 0.96 -> 0.97 (Nov 01 2014)
-
-### GENERAL:
-* (windows) Moved VCMI data directory from '%userprofile%\vcmi' to '%userprofile%\Documents\My Games\vcmi'
-* (windows) (OSX) Moved VCMI save directory from 'VCMI_DATA\Games' to 'VCMI_DATA\Saves'
-* (linux)
-* Changes in used librries:
-* * VCMI can now be compiled with SDL2
-* * Movies will use ffmpeg library
-* * change boost::bind to std::bind 
-* * removed boost::asign 
-* * Updated FuzzyLite to 5.0 
-* Multiplayer load support was implemented through command-line options
-
-### ADVENTURE AI:
-* Significantly optimized execution time, AI should be much faster now.
-
-### ADVENTURE MAP:
-* Non-latin characters can now be entered in chat window or used for save names.
-* Implemented separate speed for owned heroes and heroes owned by other players
-
-### GRAPHICS:
-* Better upscaling when running in fullscreen mode.
-* New creature/commader window
-* New resolutions and bonus icons are now part of a separate mod
-* Added graphics for GENERAL_DAMAGE_REDUCTION bonus (Kuririn)
-
-### RANDOM MAP GENERATOR:
-* Random map generator now creates complete and playable maps, should match original RMG
-* All important features from original map templates are implemented
-* Fixed major crash on removing objects
-* Undeground zones will look just like surface zones
-
-### LAUNCHER:
-* Implemented switch to disable intro movies in game
-
-# 0.95 -> 0.96 (Jul 01 2014)
-
-### GENERAL:
-* (linux) now VCMI follows XDG specifications. See http://forum.vcmi.eu/viewtopic.php?t=858
-
-### ADVENTURE AI:
-* Optimized speed and removed various bottlenecks.
-
-### ADVENTURE MAP:
-* Heroes auto-level primary and secondary skill levels according to experience
-
-### BATTLES:
-* Wall hit/miss sound will be played when using catapult during siege
-
-### SPELLS:
-* New configuration format
-
-### RANDOM MAP GENERATOR:
-* Towns from mods can be used
-* Reading connections, terrains, towns and mines from template
-* Zone placement
-* Zone borders and connections, fractalized paths inside zones
-* Guard generation
-* Treasue piles generation (so far only few removable objects)
-
-### MODS:
-* Support for submods - mod may have their own "submods" located in <modname>/Mods directory
-* Mods may provide their own changelogs and screenshots that will be visible in Launcher
-* Mods can now add new (offensive, buffs, debuffs) spells and change existing
-* Mods can use custom mage guild background pictures and videos for taverns, setting of resources daily income for buildings
-
-### GENERAL:
-* Added configuring of heroes quantity per player allowed in game
-
-# 0.94 -> 0.95 (Mar 01 2014)
-
-### GENERAL:
-* Components of combined artifacts will now display info about entire set.
-* Implements level limit
-* Added WoG creature abilities by Kuririn
-* Implemented a confirmation dialog when pressing Alt + F4 to quit the game 
-* Added precompiled header compilation for CMake (can be enabled per flag)
-* VCMI will detect changes in text files using crc-32 checksum
-* Basic support for unicode. Internally vcmi always uses utf-8
-* (linux) Launcher will be available as "VCMI" menu entry from system menu/launcher
-* (linux) Added a SIGSEV violation handler to vcmiserver executable for logging stacktrace (for convenience)
-
-### ADVENTURE AI:
-* AI will use fuzzy logic to compare and choose multiple possible subgoals.
-* AI will now use SectorMap to find a way to guarded / covered objects. 
-* Significantly improved exploration algorithm.
-* Locked heroes now try to decompose their goals exhaustively.
-* Fixed (common) issue when AI found neutral stacks infinitely strong.
-* Improvements for army exchange criteria.
-* GatherArmy may include building dwellings in town (experimental).
-* AI should now conquer map more aggressively and much faster
-* Fuzzy rules will be printed out at map launch (if AI log is enabled)
-
-### CAMPAIGNS:
-* Implemented move heroes to next scenario
-* Support for non-standard victory conditions for H3 campaigns
-* Campaigns use window with bonus & scenario selection than scenario information window from normal maps
-* Implemented hero recreate handling (e.g. Xeron will be recreated on AB campaign)
-* Moved place bonus hero before normal random hero and starting hero placement -> same behaviour as in OH3
-* Moved placing campaign heroes before random object generation -> same behaviour as in OH3 
-
-### TOWNS:
-* Extended building dependencies support
-
-### MODS:
-* Custom victory/loss conditions for maps or campaigns
-* 7 days without towns loss condition is no longer hardcoded
-* Only changed mods will be validated
-
-# 0.93 -> 0.94 (Oct 01 2013)
-
-### GENERAL:
-* New Launcher application, see 
-* Filesystem now supports zip archives. They can be loaded similarly to other archives in filesystem.json. Mods can use Content.zip instead of Content/ directory.
-* fixed "get txt" console command
-* command "extract" to extract file by name
-* command "def2bmp" to convert def into set of frames.
-* fixed crash related to cammander's SPELL_AFTER_ATTACK spell id not initialized properly (text id was resolved on copy of bonus)
-* fixed duels, added no-GUI mode for automatic AI testing
-* Sir Mullich is available at the start of the game
-* Upgrade cost will never be negative.
-* support for Chinese fonts (GBK 2-byte encoding)
-
-### ADVENTURE MAP:
-* if Quick Combat option is turned on, battles will be resolved by AI
-* first hero is awakened on new turn
-* fixed 3000 gems reward in shipwreck
-
-### BATTLES:
-* autofight implemented
-* most of the animations is time-based
-* simplified postioning of units in battle, should fix remaining issues with unit positioning
-* synchronized attack/defence animation
-* spell animation speed uses game settings
-* fixed disrupting ray duration
-* added logging domain for battle animations
-* Fixed crashes on Land Mines / Fire Wall casting.
-* UI will be correctly greyed-out during opponent turn
-* fixed remaining issues with blit order
-* Catapult attacks should be identical to H3. Catapult may miss and attack another part of wall instead (this is how it works in H3)
-* Fixed Remove Obstacle.
-* defeating hero will yield 500 XP
-* Added lots of missing spell immunities from Strategija
-* Added stone gaze immunity for Troglodytes (did you know about it?)
-* damage done by turrets is properly increased by built buldings
-* Wyverns will cast Poison instead of Stone Gaze.
-
-### TOWN:
-* Fixed issue that allowed to build multiple boats in town.
-* fix for lookout tower
-
-# 0.92 -> 0.93 (Jun 01 2013)
-
-### GENERAL:
-* Support for SoD-only installations, WoG becomes optional addition
-* New logging framework
-* Negative luck support, disabled by default
-* Several new icons for creature abilities (Fire Shield, Non-living, Magic Mirror, Spell-like Attack)
-* Fixed stack artifact (and related buttons) not displaying in creature window.
-* Fixed crash at month of double population.
-
-### MODS:
-* Improved json validation. Now it support most of features from latest json schema draft.
-* Icons use path to icon instead of image indexes.
-* It is possible to edit data of another mod or H3 data via mods.
-* Mods can access only ID's from dependenies, virtual "core" mod and itself (optional for some mods compatibility)
-* Removed no longer needed field "projectile spins"
-* Heroes: split heroes.json in manner similar to creatures\factions; string ID's for H3 heroes; h3 hero classes and artifacts can be modified via json.
-
-### BATTLES:
-* Fixed Death Stare of Commanders
-* Projectile blitting should be closer to original H3. But still not perfect.
-* Fixed missing Mirth effects
-* Stack affected by Berserk should not try to attack itself
-* Fixed several cases of incorrect positioning of creatures in battles
-* Fixed abilities of Efreet.
-* Fixed broken again palette in some battle backgrounds
-
-### TOWN:
-* VCMI will not crash if building selection area is smaller than def
-* Detection of transparency on selection area is closer to H3
-* Improved handling buildings with mode "auto":
-* * they will be properly processed (new creatures will be added if dwelling, spells learned if mage guild, and so on)
-* * transitive dependencies are handled (A makes B build, and B makes C and D)
-
-### SOUND:
-* Added missing WoG creature sounds (from Kuririn).
-* The Windows package comes with DLLs needed to play .ogg files
-* (linux) convertMP3 option for vcmibuilder for systems where SDL_Mixer can't play mp3's
-* some missing sounds for battle effects
-
-### ARTIFACTS:
-* Several fixes to combined artifacts added via mods.
-* Fixed Spellbinder's Hat giving level 1 spells instead of 5.
-* Fixed incorrect components of Cornucopia.
-* Cheat code with grant all artifacts, including the ones added by mods
-
-# 0.91 -> 0.92 (Mar 01 2013)
-
-### GENERAL:
-* hero crossover between missions in campaigns
-* introduction before missions in campaigns
-
-### MODS:
-* Added CREATURE_SPELL_POWER for commanders
-* Added spell modifiers to various spells: Hypnotize (Astral), Firewall (Luna), Landmine 
-* Fixed ENEMY_DEFENCE_REDUCTION, GENERAL_ATTACK_REDUCTION
-* Extended usefulness of ONLY_DISTANCE_FIGHT, ONLY_MELEE_FIGHT ranges
-* Double growth creatures are configurable now
-* Drain Life now has % effect depending on bonus value
-* Stack can use more than 2 attacks. Additional attacks can now be separated as "ONLY_MELEE_FIGHT and "ONLY_DISTANCE_FIGHT".
-* Moat damage configurable
-* More config options for spells:
-* * mind immunity handled by config
-* * direct damage immunity handled by config
-* * immunity icon configurable
-* * removed mind_spell flag 
-* creature config use string ids now. 
-* support for string subtype id in short bonus format
-* primary skill identifiers for bonuses
-
-# 0.9 -> 0.91 (Feb 01 2013)
-
-### GENERAL:
-* VCMI build on OS X is now supported
-* Completely removed autotools
-* Added RMG interace and ability to generate simplest working maps
-* Added loading screen
-
-### MODS:
-* Simplified mod structure. Mods from 0.9 will not be compatible.
-* Mods can be turned on and off in config/modSettings.json file
-* Support for new factions, including:
-* * New towns
-* * New hero classes
-* * New heroes
-* * New town-related external dwellings
-* Support for new artifact, including combined, commander and stack artifacts
-* Extended configuration options
-* * All game objects are referenced by string identifiers
-* * Subtype resolution for bonuses
-
-### BATTLES:
-* Support for "enchanted" WoG ability
-
-### ADVENTURE AI:
-* AI will try to use Subterranean Gate, Redwood Observatory and Cartographer for exploration
-* Improved exploration algorithm
-* AI will prioritize dwellings and mines when there are no opponents visible
-
-# 0.89 -> 0.9 (Oct 01 2012)
-
-### GENERAL:
-* Provisional support creature-adding mods
-* New filesystem allowing easier resource adding/replacing
-* Reorganized package for better compatibility with HotA and not affecting the original game
-* Moved many hard-coded settings into text config files
-* Commander level-up dialog
-* New Quest Log window
-* Fixed a number of bugs in campaigns, support for starting hero selection bonus. 
-
-### BATTLES:
-* New graphics for Stack Queue
-* Death Stare works identically to H3
-* No explosion when catapult fails to damage the wall
-* Fixed crash when attacking stack dies before counterattack
-* Fixed crash when attacking stack dies in the Moat just before the attack
-* Fixed Orb of Inhibition and Recanter's Cloak (they were incorrectly implemented)
-* Fleeing hero won't lose artifacts.
-* Spellbook won't be captured. 
-
-### ADVENTURE AI:
-* support for quests (Seer Huts, Quest Guardians, and so)
-* AI will now wander with all the heroes that have spare movement points. It should prevent stalling.
-* AI will now understand threat of Abandoned Mine.
-* AI can now exchange armies between heroes. By default, it will pass army to main hero.
-* Fixed strange case when AI found allied town extremely dangerous
-* Fixed crash when AI tried to "revisit" a Boat
-* Fixed crash when hero assigned to goal was lost when attempting realizing it
-* Fixed a possible freeze when exchanging resources at marketplace
-
-### BATTLE AI:
-* It is possible to select a battle AI module used by VCMI by typing into the console "setBattleAI <name>". The names of avaialble modules are "StupidAI" and "BattleAI". BattleAI may be a little smarter but less stable. By the default, StupidAI will be used, as in previous releases.
-* New battle AI module: "BattleAI" that is smarter and capable of casting some offensive and enchantment spells
-
-# 0.88 -> 0.89 (Jun 01 2012)
-
-### GENERAL:
-* Mostly implemented Commanders feature (missing level-up dialog)
-* Support for stack artifacts
-* New creature window graphics contributed by fishkebab
-* Config file may have multiple upgrades for creatures
-* CTRL+T will open marketplace window
-* G will open thieves guild window if player owns at least one town with tavern
-* Implemented restart functionality. CTRL+R will trigger a quick restart
-* Save game screen and returning to main menu will work if game was started with --start option
-* Simple mechanism for detecting game desynchronization after init
-* 1280x800 resolution graphics, contributed by Topas
-
-### ADVENTURE MAP:
-* Fixed monsters regenerating casualties from battle at the start of new week.
-* T in adventure map will switch to next town
-
-### BATTLES:
-* It's possible to switch active creature during tacts phase by clicking on stack
-* After battle artifacts of the defeated hero (and his army) will be taken by winner
-* Rewritten handling of battle obstacles. They will be now placed following H3 algorithm.
-* Fixed crash when death stare or acid breath activated on stack that was just killed
-* First aid tent can heal only creatures that suffered damage
-* War machines can't be healed by tent
-* Creatures casting spells won't try to cast them during tactic phase
-* Console tooltips for first aid tent
-* Console tooltips for teleport spell
-* Cursor is reset to pointer when action is requested
-* Fixed a few other missing or wrong tooltips/cursors
-* Implemented opening creature window by l-clicking on stack
-* Fixed crash on attacking walls with Cyclop Kings
-* Fixed and simplified Teleport casting
-* Fixed Remove Obstacle spell
-* New spells supported:
-* * Chain Lightning
-* * Fire Wall
-* * Force Field
-* * Land Mine
-* * Quicksands
-* * Sacrifice
-
-### TOWNS:
-* T in castle window will open a tavern window (if available)
-
-### PREGAME:
-* Pregame will use same resolution as main game
-* Support for scaling background image
-* Customization of graphics with config file.
-
-### ADVENTURE AI:
-* basic rule system for threat evaluation
-* new town development logic
-* AI can now use external dwellings
-* AI will weekly revisit dwellings & mills
-* AI will now always pick best stacks from towns
-* AI will recruit multiple heroes for exploration
-* AI won't try attacking its own heroes
-
-# 0.87 -> 0.88 (Mar 01 2012)
-
-* added an initial version of new adventure AI: VCAI
-* system settings window allows to change default resolution
-* introduced unified JSON-based settings system
-* fixed all known localization issues
-* Creature Window can handle descriptions of spellcasting abilities
-* Support for the clone spell
-
-# 0.86 -> 0.87 (Dec 01 2011)
-
-### GENERAL:
-* Pathfinder can find way using ships and subterranean gates
-* Hero reminder & sleep button
-
-### PREGAME:
-* Credits are implemented
-
-### BATTLES:
-* All attacked hexes will be highlighted
-* New combat abilities supported:
-* * Spell Resistance aura
-* * Random spellcaster (Genies)
-* * Mana channeling
-* * Daemon summoning
-* * Spellcaster (Archangel Ogre Mage, Elementals, Faerie Dragon)
-* * Fear
-* * Fearless
-* * No wall penalty
-* * Enchanter
-* * Bind
-* * Dispell helpful spells
-
-# 0.85 -> 0.86 (Sep 01 2011)
-
-### GENERAL:
-* Reinstated music support
-* Bonus system optimizations (caching)
-* converted many config files to JSON
-* .tga file support
-* New artifacts supported
-* * Admiral's Hat
-* * Statue of Legion
-* * Titan's Thunder
-
-### BATTLES:
-* Correct handling of siege obstacles
-* Catapult animation
-* New combat abilities supported
-* * Dragon Breath
-* * Three-headed Attack
-* * Attack all around
-* * Death Cloud / Fireball area attack
-* * Death Blow
-* * Lightning Strike
-* * Rebirth
-* New WoG abilities supported
-* * Defense Bonus
-* * Cast before attack
-* * Immunity to direct damage spells
-* New spells supported
-* * Magic Mirror
-* * Titan's Lightning Bolt
-
-# 0.84 -> 0.85 (Jun 01 2011)
-
-### GENERAL:
-* Support for stack experience
-* Implemented original campaign selection screens
-* New artifacts supported:
-* * Statesman's Medal
-* * Diplomat's Ring
-* * Ambassador's Sash
-
-### TOWNS:
-* Implemented animation for new town buildings
-* It's possible to sell artifacts at Artifact Merchants
-
-### BATTLES:
-* Neutral monsters will be split into multiple stacks
-* Hero can surrender battle to keep army
-* Support for Death Stare, Support for Poison, Age, Disease, Acid Breath, Fire / Water / Earth / Air immunities and Receptiveness
-* Partial support for Stone Gaze, Paralyze, Mana drain
-
-# 0.83 -> 0.84 (Mar 01 2011)
-
-### GENERAL:
-* Bonus system has been rewritten
-* Partial support for running VCMI in duel mode (no adventure map, only one battle, ATM only AI-AI battles)
-* New artifacts supported:
-* * Angellic Alliance
-* * Bird of Perception
-* * Emblem of Cognizance
-* * Spell Scroll
-* * Stoic Watchman
-
-### BATTLES:
-* Better animations handling
-* Defensive stance is supported
-
-### HERO:
-* New secondary skills supported:
-* * Artillery
-* * Eagle Eye
-* * Tactics
-
-### AI PLAYER:
-* new AI leading neutral creatures in combat, slightly better then previous
-
-# 0.82 -> 0.83 (Nov 01 2010)
-
-### GENERAL:
-* Alliances support
-* Week of / Month of events
-* Mostly done pregame for MP games (temporarily only for local clients)
-* Support for 16bpp displays
-* Campaigns:
-* * support for building bonus
-* * moving to next map after victory
-* Town Portal supported
-* Vial of Dragon Blood and Statue of Legion supported
-
-### HERO:
-* remaining specialities have been implemented
-
-### TOWNS:
-* town events supported
-* Support for new town structures: Deiety of Fire and Escape Tunnel 
-
-### BATTLES:
-* blocked retreating from castle
-
-# 0.81 -> 0.82 (Aug 01 2010)
-
-### GENERAL:
-* Some of the starting bonuses in campaigns are supported
-* It's possible to select difficulty level of mission in campaign
-* new cheat codes:
-* * vcmisilmaril - player wins
-* * vcmimelkor - player loses
-
-### ADVENTURE MAP:
-* Neutral armies growth implemented (10% weekly)
-* Power rating of neutral stacks
-* Favourable Winds reduce sailing cost
-
-### HERO:
-* Learning secondary skill supported.
-* Most of hero specialities are supported, including:
-* * Creature specialities (progressive, fixed, Sir Mullich)
-* * Spell damage specialities (Deemer), fixed bonus (Ciele)
-* * Secondary skill bonuses
-* * Creature Upgrades (Gelu)
-* * Resorce generation
-* * Starting Skill (Adrienne)
-
-### TOWNS:
-* Support for new town structures:
-* * Artifact Merchant
-* * Aurora Borealis
-* * Castle Gates
-* * Magic University
-* * Portal of Summoning 
-* * Skeleton transformer
-* * Veil of Darkness
-
-### OBJECTS:
-* Stables will now upgrade Cavaliers to Champions.
-* New object supported:
-* * Abandoned Mine
-* * Altar of Sacrifice
-* * Black Market
-* * Cover of Darkness
-* * Hill Fort
-* * Refugee Camp
-* * Sanctuary
-* * Tavern
-* * University
-* * Whirlpool
-
-# 0.8 -> 0.81 (Jun 01 2010)
-
-### GENERAL:
-* It's possible to start campaign
-* Support for build grail victory condition
-* New artifacts supported:
-* * Angel's Wings
-* * Boots of levitation
-* * Orb of Vulnerability
-* * Ammo cart
-* * Golden Bow
-* * Hourglass of Evil Hour
-* * Bow of Sharpshooter
-* * Armor of the Damned
-
-### ADVENTURE MAP:
-* Creatures now guard surrounding tiles
-* New adventura map spells supported:
-* * Summon Boat
-* * Scuttle Boat 
-* * Dimension Door
-* * Fly
-* * Water walk
-
-### BATTLES:
-* A number of new creature abilities supported
-* First Aid Tent is functional
-* Support for distance/wall/melee penalties & no * penalty abilities
-* Reworked damage calculation to fit OH3 formula better
-* Luck support
-* Teleportation spell
-
-### HERO:
-* First Aid secondary skill
-* Improved formula for necromancy to match better OH3
-
-### TOWNS:
-* Sending resources to other players by marketplace
-* Support for new town structures:
-* * Lighthouse
-* * Colossus
-* * Freelancer's Guild
-* * Guardian Spirit
-* * Necromancy Amplifier
-* * Soul Prison
-
-### OBJECTS:
-* New object supported:
-* * Freelancer's Guild
-* * Trading Post
-* * War Machine Factory
-
-# 0.75 -> 0.8 (Mar 01 2010)
-
-### GENERAL:
-* Victory and loss conditions are supported. It's now possible to win or lose the game.
-* Implemented assembling and disassembling of combination artifacts.
-* Kingdom Overview screen is now available.
-* Implemented Grail (puzzle map, digging, constructing ultimate building)
-* Replaced TTF fonts with original ones.
-
-### ADVENTURE MAP:
-* Implemented rivers animations (thx to GrayFace).
-
-### BATTLES:
-* Fire Shield spell (and creature ability) supported
-* affecting morale/luck and casting spell after attack creature abilities supported
-
-### HERO:
-* Implementation of Scholar secondary skill
-
-### TOWN:
-* New left-bottom info panel functionalities.
-
-### TOWNS:
-* new town structures supported:
-* * Ballista Yard
-* * Blood Obelisk
-* * Brimstone Clouds
-* * Dwarven Treasury
-* * Fountain of Fortune
-* * Glyphs of Fear
-* * Mystic Pond
-* * Thieves Guild
-* * Special Grail functionalities for Dungeon, Stronghold and Fortress
-
-### OBJECTS:
-* New objects supported:
-* * Border gate
-* * Den of Thieves
-* * Lighthouse
-* * Obelisk
-* * Quest Guard
-* * Seer hut
-
-A lot of of various bugfixes and improvements:
-http://bugs.vcmi.eu/changelog_page.php?version_id=14
-
-# 0.74 -> 0.75 (Dec 01 2009)
-
-### GENERAL:
-* Implemented "main menu" in-game option.
-* Hide the mouse cursor while displaying a popup window.
-* Better handling of huge and empty message boxes (still needs more changes)
-* Fixed several crashes when exiting.
-
-### ADVENTURE INTERFACE:
-* Movement cursor shown for unguarded enemy towns.
-* Battle cursor shown for guarded enemy garrisons.
-* Clicking on the border no longer opens an empty info windows
-
-### HERO WINDOW:
-* Improved artifact moving. Available slots are higlighted. Moved artifact is bound to mouse cursor. 
-
-### TOWNS:
-* new special town structures supported:
-* * Academy of Battle Scholars
-* * Cage of Warlords
-* * Mana Vortex
-* * Stables
-* * Skyship (revealing entire map only)
-
-### OBJECTS:
-* External dwellings increase town growth
-* Right-click info window for castles and garrisons you do not own shows a rough amount of creatures instead of none
-* Scholar won't give unavaliable spells anymore.
-
-A lot of of various bugfixes and improvements:
-http://bugs.vcmi.eu/changelog_page.php?version_id=2
-
-# 0.73 -> 0.74 (Oct 01 2009)
-
-### GENERAL:
-* Scenario Information window
-* Save Game window
-* VCMI window should start centered
-* support for Necromancy and Ballistics secondary skills
-* new artifacts supported, including those improving Necromancy, Legion Statue parts, Shackles of War and most of combination artifacts (but not combining)
-* VCMI client has its own icon (thx for graphic to Dikamilo)
-* Ellipsis won't be split when breaking text on several lines
-* split button will be grayed out when no creature is selected
-* fixed issue when splitting stack to the hero with only one creatures
-* a few fixes for shipyard window
-
-### ADVENTURE INTERFACE:
-* Cursor shows if tile is accesible and how many turns away
-* moving hero with arrow keys / numpad
-* fixed Next Hero button behaviour
-* fixed Surface/Underground switch button in higher resolutions
-
-### BATTLES:
-* partial siege support
-* new stack queue for higher resolutions (graphics made by Dru, thx!)
-* 'Q' pressing toggles the stack queue displaying (so it can be enabled/disabled it with single key press)
-* more creatures special abilities supported
-* battle settings will be stored
-* fixed crashes occurring on attacking two hex creatures from back
-* fixed crash when clicking on enemy stack without moving mouse just after receiving action
-* even large stack numbers will fit the boxes
-* when active stack is killed by spell, game behaves properly
-* shooters attacking twice (like Grand Elves) won't attack twice in melee 
-* ballista can shoot even if there's an enemy creature next to it 
-* improved obstacles placement, so they'll better fit hexes (thx to Ivan!)
-* selecting attack directions works as in H3
-* estimating damage that will be dealt while choosing stack to be attacked
-* modified the positioning of battle effects, they should look about right now.
-* after selecting a spell during combat, l-click is locked for any action other than casting. 
-* flying creatures will be blitted over all other creatures, obstacles and wall
-* obstacles and units should be printed in better order (not tested)
-* fixed armageddon animation
-* new spells supported:
-* * Anti-Magic
-* * Cure
-* * Resurrection 
-* * Animate Dead 
-* * Counterstrike 
-* * Berserk 
-* * Hypnotize 
-* * Blind 
-* * Fire Elemental 
-* * Earth Elemental 
-* * Water Elemental 
-* * Air Elemental 
-* * Remove obstacle
-
-### TOWNS:
-* enemy castle can be taken over
-* only one capitol per player allowed (additional ones will be lost)
-* garrisoned hero can buy a spellbook
-* heroes available in tavern should be always different
-* ship bought in town will be correctly placed
-* new special town structures supported:
-* * Lookout Tower
-* * Temple of Valhalla
-* * Wall of Knowledge
-* * Order of Fire
-
-### HERO WINDOW:
-* war machines cannot be unequiped
-
-### PREGAME:
-* sorting: a second click on the column header sorts in descending order.
-* advanced options tab: r-click popups for selected town, hero and bonus
-* starting scenario / game by double click
-* arrows in options tab are hidden when not available 
-* subtitles for chosen hero/town/bonus in pregame
-
-### OBJECTS:
-* fixed pairing Subterranean Gates
-* New objects supported:
-* * Borderguard & Keymaster Tent
-* * Cartographer
-* * Creature banks
-* * Eye of the Magi & Hut of the Magi
-* * Garrison
-* * Stables
-* * Pandora Box
-* * Pyramid
-
-# 0.72 -> 0.73 (Aug 01 2009)
-
-### GENERAL:
-* infowindow popup will be completely on screen
-* fixed possible crash with in game console
-* fixed crash when gaining artifact after r-click on hero in tavern
-* Estates / hero bonuses won't give resources on first day. 
-* video handling (intro, main menu animation, tavern animation, spellbook animation, battle result window)
-* hero meeting window allowing exchanging armies and artifacts between heroes on adventure map
-* 'T' hotkey opens marketplace window
-* giving starting spells for heroes
-* pressing enter or escape close spellbook
-* removed redundant quotation marks from skills description and artifact events texts
-* disabled autosaving on first turn
-* bonuses from bonus artifacts
-* increased char per line limit for subtitles under components
-* corrected some exp/level values
-* primary skills cannot be negative
-* support for new artifacts: Ring of Vitality, Ring of Life, Vial of Lifeblood, Garniture of Interference, Surcoat of Counterpoise, Boots of Polarity
-* fixed timed events reappearing
-* saving system options
-* saving hero direction
-* r-click popups on enemy heroes and towns
-* hero leveling formula matches the H3
-
-### ADVENTURE INTERFACE:
-* Garrisoning, then removing hero from garrison move him at the end of the heroes list
-* The size of the frame around the map depends on the screen size.
-* spellbook shows adventure spells when opened on adventure map
-* erasing path after picking objects with last movement point
-
-### BATTLES:
-* spell resistance supported (secondary skill, artifacts, creature skill)
-* corrected damage inflicted by spells and ballista
-* added some missing projectile infos
-* added native terrain bonuses in battles
-* number of units in stack in battle should better fit the box
-* non-living and undead creatures have now always 0 morale
-* displaying luck effect animation
-* support for battleground overlays:
-* * cursed ground
-* * magic plains
-* * fiery fields
-* * rock lands
-* * magic clouds
-* * lucid pools
-* * holy ground
-* * clover field
-* * evil fog
-
-### TOWNS:
-* fixes for horde buildings
-* garrisoned hero can buy a spellbook if he is selected or if there is no visiting hero
-* capitol bar in town hall is grey (not red) if already one exists
-* fixed crash on entering hall when town was near map edge
-
-### HERO WINDOW:
-* garrisoned heroes won't be shown on the list
-* artifacts will be present on morale/luck bonuses list
-
-### PREGAME:
-* saves are sorted primary by map format, secondary by name
-* fixed displaying date of saved game (uses local time, removed square character)
-
-### OBJECTS:
-* Fixed primary/secondary skill levels given by a scholar.
-* fixed problems with 3-tiles monoliths
-* fixed crash with flaggable building next to map edge
-* fixed some descriptions for events
-* New objects supported:
-* * Buoy
-* * Creature Generators
-* * Flotsam
-* * Mermaid
-* * Ocean bottle
-* * Sea Chest 
-* * Shipwreck Survivor
-* * Shipyard
-* * Sirens 
-
-# 0.71 -> 0.72 (Jun 1 2009)
-
-### GENERAL:
-* many sound effects and music
-* autosave (to 5 subsequent files)
-* artifacts support (most of them)
-* added internal game console (activated on TAB)
-* fixed 8 hero limit to check only for wandering heroes (not garrisoned)
-* improved randomization
-* fixed crash on closing application
-* VCMI won't always give all three stacks in the starting armies
-* fix for drawing starting army creatures count
-* Diplomacy secondary skill support
-* timed events won't cause resources amount to be negative
-* support for sorcery secondary skill
-* reduntant quotation marks from artifact descriptions are removed
-* no income at the first day
-
-### ADVENTURE INTERFACE:
-* fixed crasbug occurring on revisiting objects (by pressing space)
-* always restoring default cursor when movng mouse out of the terrain
-* fixed map scrolling with ctrl+arrows when some windows are opened
-* clicking scrolling arrows in town/hero list won't open town/hero window
-* pathfinder will now look for a path going via printed positions of roads when it's possible
-* enter can be used to open window with selected hero/town
-
-### BATTLES:
-* many creatures special skills implemented
-* battle will end when one side has only war machines
-* fixed some problems with handling obstacles info
-* fixed bug with defending / waiting while no stack is active
-* spellbook button is inactive when hero cannot cast any spell
-* obstacles will be placed more properly when resolution is different than 800x600
-* canceling of casting a spell by pressing Escape or R-click (R-click on a creatures does not cancel a spell)
-* spellbook cannot be opened by L-click on hero in battle when it shouldn't be possible
-* new spells:
-* * frost ring
-* * fireball
-* * inferno
-* * meteor shower
-* * death ripple
-* * destroy undead
-* * dispel
-* * armageddon
-* * disrupting ray
-* * protection from air
-* * protection from fire
-* * protection from water
-* * protection from earth
-* * precision
-* * slayer
-
-### TOWNS:
-* resting in town with mage guild will replenih all the mana points
-* fixed Blacksmith
-* the number of creatures at the beginning of game is their base growth
-* it's possible to enter Tavern via Brotherhood of Sword
-
-### HERO WINDOW:
-* fixed mana limit info in the hero window
-* war machines can't be removed
-* fixed problems with removing artifacts when all visible slots in backpack are full
-
-### PREGAME:
-* clicking on "advanced options" a second time now closes the tab instead of refreshing it.
-* Fix position of maps names. 
-* Made the slider cursor much more responsive. Speedup the map select screen.
-* Try to behave when no maps/saves are present.
-* Page Up / Page Down / Home / End hotkeys for scrolling through scenarios / games list
-
-### OBJECTS:
-* Neutral creatures can join or escape depending on hero strength (escape formula needs to be improved)
-* leaving guardians in flagged mines.
-* support for Scholar object
-* support for School of Magic
-* support for School of War
-* support for Pillar of Fire
-* support for Corpse
-* support for Lean To
-* support for Wagon
-* support for Warrior's Tomb
-* support for Event
-* Corpse (Skeleton) will be accessible from all directions
-
-# 0.7 -> 0.71 (Apr 01 2009)
-
-### GENERAL:
-* fixed scrolling behind window problem (now it's possible to scroll with CTRL + arrows) 
-* morale/luck system and corresponding sec. skills supported 
-* fixed crash when hero get level and has less than two sec. skills to choose between 
-* added keybindings for components in selection window (eg. for treasure chest dialog): 1, 2, and so on. Selection dialog can be closed with Enter key
-* proper handling of custom portraits of heroes
-* fixed problems with non-hero/town defs not present in def list but present on map (occurring probably only in case of def substitution in map editor)
-* fixed crash when there was no hero available to hire for some player 
-* fixed problems with 1024x600 screen resolution
-* updating blockmap/visitmap of randomized objects 
-* fixed crashes on loading maps with flag all mines/dwelling victory condition
-* further fixes for leveling-up (stability and identical offered skills bug)
-* splitting window allows to rebalance two stack with the same creatures
-* support for numpad keyboard
-* support for timed events
-
-### ADVENTURE INTERFACE:
-* added "Next hero" button functionality
-* added missing path arrows
-* corrected centering on hero's position 
-* recalculating hero path after reselecting hero
-* further changes in pathfinder making it more like original one
-* orientation of hero can't be change if movement points are exhausted 
-* campfire, borderguard, bordergate, questguard will be accessible from the top
-* new movement cost calculation algorithm
-* fixed sight radious calculation
-* it's possible to stop hero movement
-* faster minimap refreshing 
-* provisional support for "Save" button in System Options Window
-* it's possible to revisit object under hero by pressing Space
-
-### BATTLES:
-* partial support for battle obstacles
-* only one spell can be casted per turn
-* blocked opening sepllbook if hero doesn't have a one
-* spells not known by hero can't be casted 
-* spell books won't be placed in War Machine slots after battle
-* attack is now possible when hex under cursor is not displayed 
-* glowing effect of yellow border around creatures
-* blue glowing border around hovered creature
-* made animation on battlefield more smooth
-* standing stacks have more static animation
-* probably fixed problem with displaying corpses on battlefield
-* fixes for two-hex creatures actions
-* fixed hero casting spell animation
-* corrected stack death animation
-* battle settings will be remembered between battles
-* improved damage calculation formula
-* correct handling of flying creatures in battles
-* a few tweaks in battle path/available hexes calculation (more of them is needed)
-* amounts of units taking actions / being an object of actions won't be shown until action ends
-* fixed positions of stack queue and battle result window when resolution is != 800x600 
-* corrected duration of frenzy spell which was incorrect in certain cases 
-* corrected hero spell casting animation
-* better support for battle backgrounds 
-* blocked "save" command during battle 
-* spellbook displays only spells known by Hero
-* New spells supported:
-* * Mirth
-* * Sorrow
-* * Fortune
-* * Misfortune
-
-### TOWN INTERFACE:
-* cannot build more than one capitol
-* cannot build shipyard if town is not near water
-* Rampart's Treasury requires Miner's Guild 
-* minor improvements in Recruitment Window
-* fixed crash occurring when clicking on hero portrait in Tavern Window, minor improvements for Tavern Window
-* proper updating resdatabar after building structure in town or buying creatures (non 800x600 res)
-* fixed blinking resdatabar in town screen when buying (800x600) 
-* fixed horde buildings displaying in town hall
-* forbidden buildings will be shown as forbidden, even if there are no res / other conditions are not fulfilled
-
-### PREGAME:
-* added scrolling scenario list with mouse wheel
-* fixed mouse slow downs
-* cannot select heroes for computer player (pregame) 
-* no crash if uses gives wrong resolution ID number
-* minor fixes
-
-### OBJECTS:
-* windmill gives 500 gold only during first week ever (not every month)
-* After the first visit to the Witch Hut, right-click/hover tip mentions the skill available. 
-* New objects supported:
-* * Prison
-* * Magic Well
-* * Faerie Ring
-* * Swan Pond
-* * Idol of Fortune
-* * Fountain of Fortune
-* * Rally Flag
-* * Oasis
-* * Temple
-* * Watering Hole
-* * Fountain of Youth
-* * support for Redwood Observatory
-* * support for Shrine of Magic Incantation / Gesture / Thought
-* * support for Sign / Ocean Bottle
-
-### AI PLAYER:
-* Minor improvements and fixes.
-
-# 0.64 -> 0.7 (Feb 01 2009)
-
-### GENERAL:
-* move some settings to the config/settings.txt file
-* partial support for new screen resolutions
-* it's possible to set game resolution in pregame (type 'resolution' in the console) 
-* /Data and /Sprites subfolders can be used for adding files not present in .lod archives
-* fixed crashbug occurring when hero levelled above 15 level
-* support for non-standard screen resolutions
-* F4 toggles between full-screen and windowed mode
-* minor improvements in creature card window
-* splitting stacks with the shift+click 
-* creature card window contains info about modified speed 
-
-### ADVENTURE INTERFACE:
-* added water animation
-* speed of scrolling map and hero movement can be adjusted in the System Options Window
-* partial handling r-clicks on adventure map
-
-### TOWN INTERFACE:
-* the scroll tab won't remain hanged to our mouse position if we move the mouse is away from the scroll bar
-* fixed cloning creatures bug in garrisons (and related issues)
-
-### BATTLES:
-* support for the Wait command
-* magic arrow *really* works
-* war machines support partially added
-* queue of stacks narrowed
-* spell effect animation displaying improvements
-* positive/negative spells cannot be cast on hostile/our stacks
-* showing spell effects affecting stack in creature info window
-* more appropriate coloring of stack amount box when stack is affected by a spell
-* battle console displays notifications about wait/defend commands 
-* several reported bugs fixed
-* new spells supported:
-* * Haste
-* * lightning bolt
-* * ice bolt
-* * slow
-* * implosion
-* * forgetfulness
-* * shield
-* * air shield
-* * bless
-* * curse
-* * bloodlust
-* * weakness
-* * stone skin
-* * prayer
-* * frenzy
-
-### AI PLAYER:
-* Genius AI (first VCMI AI) will control computer creatures during the combat.
-
-### OBJECTS:
-* Guardians property for resources is handled
-* support for Witch Hut
-* support for Arena
-* support for Library of Enlightenment 
-
-And a lot of minor fixes
-
-# 0.63 -> 0.64 (Nov 01 2008)
-
-### GENERAL:
-* sprites from /Sprites folder are handled correctly
-* several fixes for pathfinder and path arrows
-* better handling disposed/predefined heroes
-* heroes regain 1 mana point each turn
-* support for mistycisim and intelligence skills
-* hero hiring possible
-* added support for a number of hotkeys
-* it's not possible anymore to leave hero level-up window without selecting secondary skill
-* many minor improvements
-
-* Added some kind of simple chatting functionality through console. Implemented several WoG cheats equivalents:
-* * woggaladriel -> vcmiainur
-* * wogoliphaunt -> vcminoldor
-* * wogshadowfax -> vcminahar
-* * wogeyeofsauron -> vcmieagles
-* * wogisengard -> vcmiformenos
-* * wogsaruman -> vcmiistari
-* * wogpathofthedead -> vcmiangband 
-* * woggandalfwhite -> vcmiglorfindel
-
-### ADVENTURE INTERFACE:
-* clicking on a tile in advmap view when a path is shown will not only hide it but also calculate a new one 
-* slowed map scrolling 
-* blocked scrolling adventure map with mouse when left ctrl is pressed
-* blocked map scrolling when dialog window is opened
-* scholar will be accessible from the top
-
-### TOWN INTERFACE:
-* partially done tavern window (only hero hiring functionality)
-
-### BATTLES:
-* water elemental will really be treated as 2 hex creature
-* potential infinite loop in reverseCreature removed
-* better handling of battle cursor 
-* fixed blocked shooter behavior
-* it's possible in battles to check remeaining HP of neutral stacks
-* partial support for Magic Arrow spell
-* fixed bug with dying unit
-* stack queue hotkey is now 'Q'
-* added shots limit 
-
-# 0.62 -> 0.63 (Oct 01 2008)
-
-### GENERAL:
-* coloured console output, logging all info to txt files
-* it's possible to use other port than 3030 by passing it as an additional argument
-* removed some redundant warnings
-* partially done spellbook
-* Alt+F4 quits the game
-* some crashbugs was fixed
-* added handling of navigation, logistics, pathfinding, scouting end estates secondary skill
-* magical hero are given spellbook at the beginning
-* added initial secondary skills for heroes 
-
-### BATTLES:
-* very significant optimization of battles 
-* battle summary window
-* fixed crashbug occurring sometimes on exiting battle 
-* confirm window is shown before retreat 
-* graphic stack queue in battle (shows when 'c' key is pressed)
-* it's possible to attack enemy hero
-* neutral monster army disappears when defeated
-* casualties among hero army and neutral creatures are saved
-* better animation handling in battles
-* directional attack in battles 
-* mostly done battle options (although they're not saved)
-* added receiving exp (and leveling-up) after a won battle 
-* added support for archery, offence and armourer secondary abilities 
-* hero's primary skills accounted for damage dealt by creatures in battle
-
-### TOWNS:
-* mostly done marketplace 
-* fixed crashbug with battles on swamps and rough terrain
-* counterattacks 
-* heroes can learn new spells in towns
-* working resource silo
-* fixed bug with the mage guild when no spells available
-* it's possible to build lighthouse
-
-### HERO WINDOW:
-* setting army formation
-* tooltips for artifacts in backpack
-
-### ADVENTURE INTERFACE:
-* fixed bug with disappearing head of a hero in adventure map
-* some objects are no longer accessible from the top 
-* no tooltips for objects under FoW
-* events won't be shown
-* working Subterranean Gates, Monoliths
-* minimap shows all flaggable objects (towns, mines, etc.) 
-* artifacts we pick up go to the appropriate slot (if free)
-
-# 0.61 -> 0.62 (Sep 01 2008)
-
-### GENERAL:
-* restructured to the server-client model
-* support for heroes placed in towns
-* upgrading creatures
-* working gaining levels for heroes (including dialog with skill selection)
-* added graphical cursor
-* showing creature amount in the creature info window
-* giving starting bonus
-
-### CASTLES:
-* icon in infobox showing that there is hero in town garrison
-* fort/citadel/castle screen
-* taking last stack from the heroes army should be impossible (or at least harder)
-* fixed reading forbidden structures
-* randomizing spells in towns
-* viewing hero window in the town screen
-* possibility of moving hero into the garrison
-* mage guild screen 
-* support for blacksmith
-* if hero doesn't have a spell book, he can buy one in a mage guild
-* it's possible to build glyph of fear in fortress
-* creatures placeholders work properly
-
-### ADVENTURE INTERFACE:
-* hopefully fixed problems with wrong town defs (village/fort/capitol)
-
-### HERO WINDOW:
-* bugfix: splitting stacks works in hero window
-* removed bug causing significant increase of CPU consumption
-
-### BATTLES:
-* shooting
-* removed some displaying problems
-* showing last group of frames in creature animation won't crash
-* added start moving and end moving animations
-* fixed moving two-hex creatures
-* showing/hiding graphic cursor
-* a part of using graphic cursor
-* slightly optimized showing of battle interface
-* animation of getting hit / death by shooting is displayed when it should be
-* improved pathfinding in battles, removed problems with displaying movement, adventure map interface won't be called during battles.
-* minor optimizations
-
-### PREGAME:
-* updates settings when selecting new map after changing sorting criteria
-* if sorting not by name, name will be used as a secondary criteria
-* when filter is applied a first available map is selected automatically
-* slider position updated after sorting in pregame
-
-### OBJECTS:
-* support for the Tree of knowledge
-* support for Campfires
-* added event message when picking artifact
-
-# 0.6 -> 0.61 (Jun 15 2008)
-
-### IMPROVEMENTS:
-* improved attacking in the battles
-* it's possible to kill hostile stack
-* animations won't go in the same phase
-* Better pathfinder
-* "%s" substitutions in Right-click information in town hall
-* windmill won't give wood
-* hover text for heroes
-* support for ZSoft-style PCX files in /Data
-* Splitting: when moving slider to the right so that 0 is left in old slot the army is moved
-* in the townlist in castle selected town will by placed on the 2nd place (not 3rd)
-* stack at the limit of unit's range can now be attacked
-* range of unit is now properly displayed
-* battle log is scrolled down when new event occurs
-* console is closed when application exits
-
-### BUGFIXES:
-* stack at the limit of unit's range can now be attacked
-* good background for the town hall screen in Stronghold
-* fixed typo in hall.txt
-* VCMI won't crash when r-click neutral stack during the battle
-* water won't blink behind shipyard in the Castle
-* fixed several memory leaks
-* properly displaying two-hex creatures in recruit/split/info window
-* corrupted map file won't cause crash on initializing main menu
-
-# 0.59 -> 0.6 (Jun 1 2008)
-
-* partially done attacking in battles
-* screen isn't now refreshed while blitting creature info window
-* r-click creature info windows in battles
-* no more division by 0 in slider
-* "plural" reference names for Conflux creatures (starting armies of Conflux heroes should now be working)
-* fixed estate problems
-* fixed blinking mana vortex
-* grail increases creature growths
-* new pathfinder
-* several minor improvements
-
-# 0.58 -> 0.59 (May 24 2008 - closed, test release)
-
-* fixed memory leak in battles
-* blitting creature animations to rects in the recruitment window
-* fixed wrong creatures def names
-* better battle pathfinder and unit reversing
-* improved slider ( #58 )
-* fixed problems with horde buildings (won't block original dwellings)
-* giving primary skill when hero get level (but there is still no dialog)
-* if an upgraded creature is available it'll be shown as the first in a recruitment window
-* creature levels not messed in Fortress
-* war machines are added to the hero's inventory, not to the garrison
-* support for H3-style PCX graphics in Data/
-* VCMI won't crash when is unable to initialize audio system
-* fixed displaying wrong town defs
-* improvements in recruitment window (slider won't allow to select more creatures than we can afford)
-* creature info window (only r-click)
-* callback for buttons/lists based on boost::function
-* a lot of minor improvements
-
-# 0.55 -> 0.58 (Apr 20 2008 - closed, test release)
-
-### TOWNS:
-* recruiting creatures
-* working creature growths (including castle and horde building influences)
-* towns give income
-* town hall screen
-* building buildings (requirements and cost are handled)
-* hints for structures
-* updating town infobox
-
-### GARRISONS:
-* merging stacks
-* splitting stacks
-
-### BATTLES:
-* starting battles
-* displaying terrain, animations of heroes, units, grid, range of units, battle menu with console, amounts of units in stacks
-* leaving battle by pressing flee button
-* moving units in battles and displaying their ranges
-* defend command for units
-
-### GENERAL:
-* a number of minor fixes and improvements
-
-# 0.54 -> 0.55 (Feb 29 2008)
-
-* Sprites/ folder works for h3sprite.lod same as Data/ for h3bitmap.lod (but it's still experimental)
-* randomization quantity of creatures on the map
-* fix of Pandora's Box handling
-* reading disposed/predefined heroes
-* new command - "get txt" - VCMI will extract all .txt files from h3bitmap.lod to the Extracted_txts/ folder.
-* more detailed logs
-* reported problems with hero flags resolved
-* heroes cannot occupy the same tile
-* hints for most of creature generators
-* some minor stuff
-
-# 0.53b -> 0.54 (Feb 23 2008 - first public release)
-* given hero is placed in the town entrance
-* some objects such as river delta won't be blitted "on" hero
-* tiles under FoW are inaccessible
-* giving random hero on RoE maps
-* improved protection against hero duplication
-* fixed starting values of primary abilities of random heroes on RoE/AB maps
-* right click popups with infoboxes for heroes/towns lists
-* new interface coloring (many thanks to GrayFace ;])
-* fixed bug in object flag's coloring
-* added hints in town lists
-* eliminated square from city hints
-
-# 0.53 - 0.53b (Feb 20 2008)
-
-* added giving default buildings in towns
-* town infobox won't crash on empty town
-
-# 0.52 - 0.53 (Feb 18 2008):
-
-* hopefully the last bugfix of Pandora's Box
-* fixed blockmaps of generated heroes
-* disposed hero cannot be chosen in scenario settings (unless he is in prison)
-* fixed town randomization
-* fixed hero randomization
-* fixed displaying heroes in preGame
-* fixed selecting/deselecting artifact slots in hero window
-* much faster pathfinder
-* memory usage and load time significantly decreased
-* it's impossible to select empty artifact slot in hero window
-* fixed problem with FoW displaying on minimap on L-sized maps
-* fixed crashbug in hero list connected with heroes dismissing
-* mostly done town infobox
-* town daily income is properly calculated
-
-# 0.51 - 0.52 (Feb 7 2008):
-
-* [feature] giving starting hero
-* [feature] VCMI will try to use files from /Data folder instead of those from h3bitmap.lod
-* [feature] picked artifacts are added to hero's backpack
-* [feature] possibility of choosing player to play
-* [bugfix] ZELP.TXT file *should* be handled correctly even it is non-english
-* [bugfix] fixed crashbug in reading defs with negativ left/right margins
-* [bugfix] improved randomization
-* [bugfix] pathfinder can't be cheated (what caused errors)
-
-# 0.5 - 0.51 (Feb 3 2008):
-
-* close button properly closes (same does 'q' key)
-* two players can't have selected same hero
-* double click on "Show Available Scenarios" won't reset options
-* fixed possible crashbug in town/hero lists
-* fixed crashbug in initializing game caused by wrong prisons handling
-* fixed crashbug on reading hero's custom artifacts in RoE maps
-* fixed crashbug on reading custom Pandora's Box in RoE maps
-* fixed crashbug on reading blank Quest Guards
-* better console messages
-* map reading speed up (though it's still slow, especially on bigger maps)
-
-# 0.0 -> 0.5 (Feb 2 2008 - first closed release):
-
-* Main menu and New game screens
-* Scenario selection, part of advanced options support
-* Partially done adventure map, town and hero interfaces
-* Moving hero
-* Interactions with several objects (mines, resources, mills, and others)
+# 1.3.1 -> 1.3.2
+
+### GENERAL
+* VCMI now uses new application icon
+* Added initial version of Czech translation
+* Game will now use tile hero is moving from for movement cost calculations, in line with H3
+* Added option to open hero backpack window in hero screen
+* Added detection of misclicks for touch inputs to make hitting small UI elements easier
+* Hero commander will now gain option to learn perks on reaching master level in corresponding abilities
+* It is no longer possible to stop movement while moving over water with Water Walk
+* Game will now automatically update hero path if it was blocked by another hero
+* Added "vcmiartifacts angelWings" form to "give artifacts" cheat
+
+### STABILITY
+* Fixed freeze in Launcher on repository checkout and on mod install
+* Fixed crash on loading VCMI map with placed Abandoned Mine
+* Fixed crash on loading VCMI map with neutral towns
+* Fixed crash on attempting to visit unknown object, such as Market of Time
+* Fixed crash on attempting to teleport unit that is immune to a spell
+* Fixed crash on switching fullscreen mode during AI turn
+
+### CAMPAIGNS
+* Fixed reorderging of hero primary skills after moving to next scenario in campaigns
+
+### BATTLES
+* Conquering a town will now correctly award additional 500 experience points
+* Quick combat is now enabled by default
+* Fixed invisible creatures from SUMMON_GUARDIANS and TRANSMUTATION bonuses
+* Added option to toggle spell usage by AI in quick combat
+* Fixed updating of spell point of enemy hero in game interface after spell cast
+* Fixed wrong creature spellcasting shortcut (now set to "F")
+* It is now possible to perform melee attack by creatures with spells, especially area spells
+* Right-click will now properly end spellcast mode
+* Fixed cursor preview when casting spell using touchscreen
+* Long tap during spell casting will now properly abort the spell
+
+### INTERFACE
+* Added "Fill all empty slots with 1 creature" option to radial wheel in garrison windows
+* Context popup for adventure map monsters will now show creature icon
+* Game will now show correct victory message for gather troops victory condition
+* Fixed incorrect display of number of owned Sawmills in Kingdom Overview window
+* Fixed incorrect color of resource bar in hotseat mode
+* Fixed broken toggle map level button in world view mode
+* Fixed corrupted interface after opening puzzle window from world view mode
+* Fixed blocked interface after attempt to start invalid map
+* Add yellow border to selected commander grandmaster ability
+* Always use bonus description for commander abilities instead of not provided wog-specific translation  
+* Fix scrolling when commander has large number of grandmaster abilities
+* Fixed corrupted message on another player defeat
+* Fixed unavailable Quest Log button on maps with quests
+* Fixed incorrect values on a difficulty selector in save load screen
+* Removed invalid error message on attempting to move non-existing unit in exchange window
+
+### RANDOM MAP GENERATOR
+* Fixed bug leading to unreachable resources around mines
+
+### MAP EDITOR
+* Fixed crash on maps containing abandoned mines
+* Fixed crash on maps containing neutral objects
+* Fixed problem with random map initialized in map editor
+* Fixed problem with initialization of random dwellings
+
+# 1.3.0 -> 1.3.1
+
+### GENERAL:
+* Fixed framerate drops on hero movement with active hota mod
+* Fade-out animations will now be skipped when instant hero movement speed is used
+* Restarting loaded campaing scenario will now correctly reapply starting bonus
+* Reverted FPS limit on mobile systems back to 60 fps
+* Fixed loading of translations for maps and campaigns
+* Fixed loading of preconfigured starting army for heroes with preconfigured spells
+* Background battlefield obstacles will now appear below creatures
+* it is now possible to load save game located inside mod
+* Added option to configure reserved screen area in Launcher on iOS
+* Fixed border scrolling when game window is maximized
+
+### AI PLAYER:
+* BattleAI: Improved performance of AI spell selection
+* NKAI: Fixed freeze on attempt to exchange army between garrisoned and visiting hero
+* NKAI: Fixed town threat calculation
+* NKAI: Fixed recruitment of new heroes
+* VCAI: Added workaround to avoid freeze on attempting to reach unreachable location
+* VCAI: Fixed spellcasting by Archangels
+
+### RANDOM MAP GENERATOR:
+* Fixed placement of roads inside rock in underground
+* Fixed placement of shifted creature animations from HotA
+* Fixed placement of treasures at the boundary of wide connections
+* Added more potential locations for quest artifacts in zone
+
+### STABILITY:
+* When starting client without H3 data game will now show message instead of silently crashing
+* When starting invalid map in campaign, game will now show message instead of silently crashing
+* Blocked loading of saves made with different set of mods to prevent crashes
+* Fixed crash on starting game with outdated mods
+* Fixed crash on attempt to sacrifice all your artifacts in Altar of Sacrifice
+* Fixed crash on leveling up after winning battle as defender
+* Fixed possible crash on end of battle opening sound
+* Fixed crash on accepting battle result after winning battle as defender
+* Fixed possible crash on casting spell in battle by AI
+* Fixed multiple possible crashes on managing mods on Android
+* Fixed multiple possible crashes on importing data on Android
+* Fixed crash on refusing rewards from town building
+* Fixed possible crash on threat evaluation by NKAI
+* Fixed crash on using haptic feedback on some Android systems
+* Fixed crash on right-clicking flags area in RMG setup mode
+* Fixed crash on opening Blacksmith window and Build Structure dialogs in some localizations
+* Fixed possible crash on displaying animated main menu
+* Fixed crash on recruiting hero in town located on the border of map
+
+# 1.2.1 -> 1.3.0
+
+### GENERAL:
+* Implemented automatic interface scaling to any resolution supported by monitor
+* Implemented UI scaling option to scale game interface
+* Game resolution and UI scaling can now be changed without game restart
+* Fixed multiple issues with borderless fullscreen mode
+* On mobile systems game will now always run at native resolution with configurable UI scaling
+* Implemented support for Horn of the Abyss map format
+* Implemented option to replay results of quick combat
+* Added translations to French and Chinese
+* All in-game cheats are now case-insensitive
+* Added high-definition icon for Windows
+* Fix crash on connecting to server on FreeBSD and Flatpak builds
+* Save games now consist of a single file
+* Added H3:SOD cheat codes as alternative to vcmi cheats
+* Fixed several possible crashes caused by autocombat activation
+* Fixed artifact lock icon in localized versions of the game
+* Fixed possible crash on changing hardware cursor
+
+### TOUCHSCREEN SUPPORT:
+* VCMI will now properly recognizes touch screen input
+* Implemented long tap gesture that shows popup window. Tap once more to close popup
+* Long tap gesture duration can now be configured in settings
+* Implemented radial menu for army management, activated via swiping creature icon
+* Implemented swipe gesture for scrolling through lists
+* All windows that have sliders in UI can now be scrolled using swipe gesture
+* Implemented swipe gesture for attack direction selection: swipe from enemy position to position you want to attack from
+* Implemented pinch gesture for zooming adventure map
+* Implemented haptic feedback (vibration) for long press gesture
+
+### LAUNCHER:
+* Launcher will now attempt to automatically detect language of OS on first launch
+* Added "About" tab with information about project and environment
+* Added separate options for Allied AI and Enemy AI for adventure map
+* Patially fixed displaying of download progress for mods
+* Fixed potential crash on opening mod information for mods with a changelog
+* Added option to configure number of autosaves
+
+### MAP EDITOR:
+* Fixed crash on cutting random town
+* Added option to export entire map as an image
+* Added validation for placing multiple heroes into starting town
+* It is now possible to have single player on a map
+* It is now possible to configure teams in editor
+
+### AI PLAYER:
+* Fixed potential crash on accessing market (VCAI)
+* Fixed potentially infinite turns (VCAI)
+* Reworked object prioritizing
+* Improved town defense against enemy heroes
+* Improved town building (mage guild and horde)
+* Various behavior fixes
+
+### GAME MECHANICS
+* Hero retreating after end of 7th turn will now correctly appear in tavern
+* Implemented hero backpack limit (disabled by default)
+* Fixed Admiral's Hat movement points calculation
+* It is now possible to access Shipwrecks from coast
+* Hero path will now be correctly updated on equipping/unequipping Levitation Boots or Angel Wings
+* It is no longer possible to abort movement while hero is flying over water
+* Fixed digging for Grail
+* Implemented "Survive beyond a time limit" victory condition
+* Implemented "Defeat all monsters" victory condition
+* 100% damage resistance or damage reduction will make unit immune to a spell
+* Game will now randomly select obligatory skill for hero on levelup instead of always picking Fire Magic
+* Fixed duration of bonuses from visitable object such as Idol of Fortune
+* Rescued hero from prison will now correctly reveal map around him
+* Lighthouses will no longer give movement bonus on land
+
+### CAMPAIGNS:
+* Fixed transfer of artifacts into next scenario
+* Fixed crash on advancing to next scenario with heroes from mods
+* Fixed handling of "Start with building" campaign bonus
+* Fixed incorrect starting level of heroes in campaigns
+* Game will now play correct music track on scenario selection window
+* Dracon woll now correctly start without spellbook in Dragon Slayer campaign
+* Fixed frequent crash on moving to next scenario during campaign
+* Fixed inability to dismiss heroes on maps with "capture town" victory condition
+
+### RANDOM MAP GENERATOR:
+* Improved zone placement, shape and connections
+* Improved zone passability for better gameplay
+* Improved treasure distribution and treasure values to match SoD closely
+* Navigation and water-specific spells are now banned on maps without water
+* RMG will now respect road settings set in menu
+* Tweaked many original templates so they allow new terrains and factions
+* Added "bannedTowns", "bannedTerrains", "bannedMonsters" zone properties
+* Added "road" property to connections
+* Added monster strength "none"
+* Support for "wide" connections
+* Support for new "fictive" and "repulsive" connections
+* RMG will now run faster, utilizing many CPU cores
+* Removed random seed number from random map description
+
+### INTERFACE:
+* Adventure map is now scalable and can be used with any resolution without mods
+* Adventure map interface is now correctly blocked during enemy turn
+* Visiting creature banks will now show amount of guards in bank
+* It is now possible to arrange army using status window
+* It is now possible to zoom in or out using mouse wheel or pinch gesture
+* It is now possible to reset zoom via Backspace hotkey
+* Receiving a message in chat will now play sound
+* Map grid will now correctly display on map start
+* Fixed multiple issues with incorrect updates of save/load game screen
+* Fixed missing fortifications level icon in town tooltip
+* Fixed positioning of resource label in Blacksmith window
+* Status bar on inactive windows will no longer show any tooltip from active window
+* Fixed highlighting of possible artifact placements when exchanging with allied hero
+* Implemented sound of flying movement (for Fly spell or Angel Wings)
+* Last symbol of entered cheat/chat message will no longer trigger hotkey
+* Right-clicking map name in scenario selection will now show file name
+* Right-clicking save game in save/load screen will now show file name and creation date
+* Right-clicking in town fort window will now show creature information popup
+* Implemented pasting from clipboard (Ctrl+V) for text input
+
+### BATTLES:
+* Implemented Tower moat (Land Mines)
+* Implemented defence reduction for units in moat
+* Added option to always show hero status window
+* Battle opening sound can now be skipped with mouse click
+* Fixed movement through moat of double-hexed units
+* Fixed removal of Land Mines and Fire Walls
+* Obstacles will now corectly show up either below or above unit
+* It is now possible to teleport a unit through destroyed walls
+* Added distinct overlay image for showing movement range of highlighted unit
+* Added overlay for displaying shooting range penalties of units
+
+### MODDING:
+* Implemented initial version of VCMI campaign format
+* Implemented spell cast as possible reward for configurable object
+* Implemented support for configurable buildings in towns
+* Implemented support for placing prison, tavern and heroes on water
+* Implemented support for new boat types
+* It is now possible for boats to use other movement layers, such as "air"
+* It is now possible to use growing artifacts on artifacts that can be used by hero
+* It is now possible to configure town moat
+* Palette-cycling animation of terrains and rivers can now be configured in json
+* Game will now correctly resolve identifier in unexpected form (e.g. 'bless' vs 'spell.bless' vs 'core:bless')
+* Creature specialties that use short form ( "creature" : "pikeman" ) will now correctly affect all creature upgrades
+* It is now possible to configure spells for Shrines
+* It is now possible to configure upgrade costs per level for Hill Forts
+* It is now possible to configure boat type for Shipyards on adventure map and in town
+* Implemented support for HotA-style adventure map images for monsters, with offset
+* Replaced (SCHOOL)_SPELL_DMG_PREMY with SPELL_DAMAGE bonus (uses school as subtype).
+* Removed bonuses (SCHOOL)_SPELLS - replaced with SPELLS_OF_SCHOOL
+* Removed DIRECT_DAMAGE_IMMUNITY bonus - replaced by 100% spell damage resistance
+* MAGIC_SCHOOL_SKILL subtype has been changed for consistency with other spell school bonuses
+* Configurable objects can now be translated
+* Fixed loading of custom battlefield identifiers for map objects
+
+# 1.2.0 -> 1.2.1
+
+### GENERAL:
+* Implemented spell range overlay for Dimension Door and Scuttle Boat
+* Fixed movement cost penalty from terrain
+* Fixed empty Black Market on game start
+* Fixed bad morale happening after waiting
+* Fixed good morale happening after defeating last enemy unit
+* Fixed death animation of Efreeti killed by petrification attack
+* Fixed crash on leaving to main menu from battle in hotseat mode
+* Fixed music playback on switching between towns
+* Special months (double growth and plague) will now appear correctly
+* Adventure map spells are no longer visible on units in battle
+* Attempt to cast spell with no valid targets in hotseat will show appropriate error message
+* RMG settings will now show all existing in game templates and not just those suitable for current settings
+* RMG settings (map size and two-level maps) that are not compatible with current template will be blocked
+* Fixed centering of scenario information window
+* Fixed crash on empty save game list after filtering
+* Fixed blocked progress in Launcher on language detection failure
+* Launcher will now correctly handle selection of Ddata directory in H3 install
+* Map editor will now correctly save message property for events and pandoras
+* Fixed incorrect saving of heroes portraits in editor
+
+# 1.1.1 -> 1.2.0
+
+### GENERAL:
+* Adventure map rendering was entirely rewritten with better, more functional code
+* Client battle code was heavily reworked, leading to better visual look & feel and fixing multiple minor battle bugs / glitches
+* Client mechanics are now framerate-independent, rather than speeding up with higher framerate
+* Implemented hardware cursor support
+* Heroes III language can now be detected automatically
+* Increased targeted framerate from 48 to 60
+* Increased performance of UI updates
+* Fixed bonus values of heroes who specialize in secondary skills
+* Fixed bonus values of heroes who specialize in creatures
+* Fixed damage increase from Adela's Bless specialty
+* Fixed missing obstacles in battles on subterranean terrain 
+* Video files now play at correct speed
+* Fixed crash on switching to second mission in campaigns
+* New cheat code: vcmiazure - give 5000 azure dragons in every empty slot
+* New cheat code: vcmifaerie - give 5000 faerie dragons in every empty slot
+* New cheat code: vcmiarmy or vcminissi - give specified creatures in every empty slot. EG: vcmiarmy imp
+* New cheat code: vcmiexp or vcmiolorin - give specified amount of experience to current hero. EG: vcmiexp 10000
+* Fixed oversided message window from Scholar skill that had confirmation button outside game window
+* Fixed loading of prebuilt creature hordes from h3m maps
+* Fixed volume of ambient sounds when changing game sounds volume
+* Fixed might&magic affinities of Dungeon heroes
+* Fixed Roland's specialty to affect Swordsmen/Crusaders instead of Griffins
+* Buying boat in town of an ally now correctly uses own resources instead of stealing them from ally
+* Default game difficulty is now set to "normal" instead of "easy"
+* Fixed crash on missing music files
+
+### MAP EDITOR:
+* Added translations to German, Polish, Russian, Spanish, Ukrainian
+* Implemented cut/copy/paste operations
+* Implemented lasso brush for terrain editing
+* Toolbar actions now have names
+* Added basic victory and lose conditions
+
+### LAUNCHER:
+* Added initial Welcome/Setup screen for new players
+* Added option to install translation mod if such mod exists and player's H3 version has different language
+* Icons now have higher resolution, to prevent upscaling artifacts
+* Added translations to German, Polish, Russian, Spanish, Ukrainian
+* Mods tab layout has been adjusted based on feedback from players
+* Settings tab layout has been redesigned to support longer texts
+* Added button to start map editor directly from Launcher
+* Simplified game starting flow from online lobby
+* Mod description will now show list of languages supported by mod
+* Launcher now uses separate mod repository from vcmi-1.1 version to prevent mod updates to unsupported versions
+* Size of mod list and mod details sub-windows can now be adjusted by player
+
+### AI PLAYER:
+* Nullkiller AI is now used by default
+* AI should now be more active in destroying heroes causing treat on AI towns
+* AI now has higher priority for resource-producing mines
+* Increased AI priority of town dwelling upgrades
+* AI will now de-prioritize town hall upgrades when low on resources
+* Messages from cheats used by AI are now hidden
+* Improved army gathering from towns
+* AI will now attempt to exchange armies between main heroes to get the strongest hero with the strongest army.
+* Improved Pandora handling
+* AI takes into account fort level now when evaluating enemy town capturing priority.
+* AI can not use allied shipyard now to avoid freeze
+* AI will avoid attacking creatures standing on draw-bridge tile during siege if the bridge is closed.
+* AI will consider retreat during siege if it can not do anything (catapult is destroyed, no destroyed walls exist)
+
+### RANDOM MAP GENERATOR
+* Random map generator can now be used without vcmi-extras mod
+* RMG will no longer place shipyards or boats at very small lakes
+* Fixed placement of shipyards in invalid locations
+* Fixed potential game hang on generation of random map
+* RMG will now generate addditional monolith pairs to create required number of zone connections
+* RMG will try to place Subterranean Gates as far away from other objects (including each other) as possible
+* RMG will now try to place objects as far as possible in both zones sharing a guard, not only the first one.
+* Use only one template for an object in zone
+* Objects with limited per-map count will be distributed evenly among zones with suitable terrain
+* Objects above zone treasure value will not be considered for placement
+* RMG will prefer terrain-specific templates for objects placement
+* RMG will place Towns and Monoliths first in order to generate long roads across the zone.
+* Adjust the position of center town in the zone for better look & feel on S maps.
+* Description of random map will correctly show number of levels
+* Fixed amount of creatures found in Pandora Boxes to match H3
+* Visitable objects will no longer be placed on top of the map, obscured by map border
+
+### ADVENTURE MAP:
+* Added option to replace popup messages on object visiting with messages in status window
+* Implemented different hero movement sounds for offroad movement
+* Cartographers now reveal terrain in the same way as in H3
+* Status bar will now show movement points information on pressing ALT or after enabling option in settings
+* It is now not possible to receive rewards from School of War without required gold amount
+* Owned objects, like Mines and Dwellings will always show their owner in status bar
+* It is now possible to interact with on-map Shipyard when no hero is selected
+* Added option to show amount of creatures as numeric range rather than adjective
+* Added option to show map grid
+* Map swipe is no longer exclusive for phones and can be enabled on desktop platforms
+* Added more graduated settigns for hero movement speed
+* Map scrolling is now more graduated and scrolls with pixel-level precision
+* Hero movement speed now matches H3
+* Improved performance of adventure map rendering
+* Fixed embarking and disembarking sounds
+* Fixed selection of "new week" animation for status window
+* Object render order now mostly matches H3
+* Fixed movement cost calculation when using "Fly" spell or "Angel Wings"
+* Fixed game freeze on using Town Portal to teleport into town with unvisited Battle Scholar Academy
+* Fixed invalid ambient sound of Whirlpool
+* Hero path will now be correctly removed on defeating monsters that are at the end of hero path
+* Seer Hut tooltips will now show messages for correct quest type
+
+### INTERFACE
+* Implemented new settings window
+* Added framerate display option
+* Fixed white status bar on server connection screen
+* Buttons in battle window now correctly show tooltip in status bar
+* Fixed cursor image during enemy turn in combat
+* Game will no longer promt to assemble artifacts if they fall into backpack
+* It is now possible to use in-game console for vcmi commands
+* Stacks sized 1000-9999 units will not be displayed as "1k"
+* It is now possible to select destination town for Town Portal via double-click
+* Implemented extended options for random map tab: generate G+U size, select RMG template, manage teams and roads
+
+### HERO SCREEN
+* Fixed cases of incorrect artifact slot highlighting
+* Improved performance of artifact exchange operation
+* Picking up composite artifact will immediately unlock slots
+* It is now possible to swap two composite artifacts
+
+### TOWN SCREEN
+* Fixed gradual fade-in of a newly built building
+* Fixed duration of building fade-in to match H3
+* Fixed rendering of Shipyard in Castle
+* Blacksmith purchase button is now properly locked if artifact slot is occupied by another warmachine
+* Added option to show number of available creatures in place of growth
+* Fixed possible interaction with hero / town list from adventure map while in town screen
+* Fixed missing left-click message popup for some town buildings
+* Moving hero from garrison by pressing space will now correctly show message "Cannot have more than 8 adventuring heroes"
+
+### BATTLES:
+* Added settings for even faster animation speed than in H3
+* Added display of potential kills numbers into attack tooltip in status bar
+* Added option to skip battle opening music entirely
+* All effects will now wait for battle opening sound before playing
+* Hex highlighting will now be disabled during enemy turn
+* Fixed incorrect log message when casting spell that kills zero units
+* Implemented animated cursor for spellcasting
+* Fixed multiple issues related to ordering of creature animations
+* Fixed missing flags from hero animations when opening menus
+* Fixed rendering order of moat and grid shadow
+* Jousting bonus from Champions will now be correctly accounted for in damage estimation
+* Building Castle building will now provide walls with additional health point
+* Speed of all battle animations should now match H3
+* Fixed missing obstacles on subterranean terrain
+* Ballistics mechanics now matches H3 logic
+* Arrow Tower base damage should now match H3
+* Destruction of wall segments will now remove ranged attack penalty
+* Force Field cast in front of drawbridge will now block it as in H3
+* Fixed computations for Behemoth defense reduction ability 
+* Bad luck (if enabled) will now multiple all damage by 50%, in line with other damage reducing mechanics
+* Fixed highlighting of movement range for creatures standing on a corpse
+* All battle animations now have same duration/speed as in H3
+* Added missing combat log message on resurrecting creatures
+* Fixed visibility of blue border around targeted creature when spellcaster is making turn
+* Fixed selection highlight when in targeted creature spellcasting mode
+* Hovering over hero now correctly shows hero cursor
+* Creature currently making turn is now highlighted in the Battle Queue 
+* Hovering over creature icon in Battle Queue will highlight this creature in the battlefield
+* New battle UI extension allows control over creatures' special abilities
+* Fixed crash on activating auto-combat in battle
+* Fixed visibility of unit creature amount labels and timing of their updates
+* Firewall will no longer hit double-wide units twice when passing through
+* Unicorn Magic Damper Aura ability now works multiplicatively with Resistance
+* Orb of Vulnerability will now negate Resistance skill
+
+### SPELLS:
+* Hero casting animation will play before spell effect
+* Fire Shield: added sound effect
+* Fire Shield: effect now correctly plays on defending creature
+* Earthquake: added sound effect
+* Earthquake: spell will not select sections that were already destroyed before cast
+* Remove Obstacles: fixed error message when casting on maps without obstacles
+* All area-effect spells (e.g. Fireball) will play their effect animation on top
+* Summoning spells: added fade-in effect for summoned creatures
+* Fixed timing of hit animation for damage-dealing spells
+* Obstacle-creating spells: UI is now locked during effect animation
+* Obstacle-creating spells: added sound effect
+* Added reverse death animation for spells that bring stack back to life
+* Bloodlust: implemented visual effect
+* Teleport: implemented visual fade-out and fade-in effect for teleporting
+* Berserk: Fixed duration of effect
+* Frost Ring: Fixed spell effect range
+* Fixed several cases where multiple different effects could play at the same time
+* All spells that can affecte multiple targets will now highlight affected stacks
+* Bless and Curse now provide +1 or -1 to base damage on Advanced & Expert levels
+
+### ABILITIES:
+* Rebirth (Phoenix): Sound will now play in the same time as animation effect
+* Master Genie spellcasting: Sound will now play in the same time as animation effect
+* Power Lich, Magogs: Sound will now play in the same time as attack animation effect
+* Dragon Breath attack now correctly uses different attack animation if multiple targets are hit
+* Petrification: implemented visual effect
+* Paralyze: added visual effect
+* Blind: Stacks will no longer retailate on attack that blinds them
+* Demon Summon: Added animation effect for summoning
+* Fire shield will no longer trigger on non-adjacent attacks, e.g. from Dragon Breath
+* Weakness now has correct visual effect 
+* Added damage bonus for opposite elements for Elementals
+* Added damage reduction for Magic Elemental attacks against creatures immune to magic
+* Added incoming damage reduction to Petrify
+* Added counter-attack damage reduction for Paralyze
+
+### MODDING:
+* All configurable objects from H3 now have their configuration in json
+* Improvements to functionality of configurable objects
+* Replaced `SECONDARY_SKILL_PREMY` bonus with separate bonuses for each skill.
+* Removed multiple bonuses that can be replaced with another bonus.
+* It is now possible to define new hero movement sounds in terrains
+* Implemented translation support for mods
+* Implemented translation support for .h3m maps and .h3c campaigns
+* Translation mods are now automatically disabled if player uses different language
+* Files with new Terrains, Roads and Rivers are now validated by game
+* Parameters controlling effect of attack and defences stats on damage are now configurable in defaultMods.json
+* New bonus: `LIMITED_SHOOTING_RANGE`. Creatures with this bonus can only use ranged attack within specified range
+* Battle window and Random Map Tab now have their layout defined in json file
+* Implemented code support for alternative actions mod
+* Implemented code support for improved random map dialog
+* It is now possible to configure number of creature stacks in heroes' starting armies
+* It is now possible to configure number of constructed dwellings in towns on map start
+* Game settings previously located in defaultMods.json are now loaded directly from mod.json
+* It is now possible for spellcaster units to have multiple spells (but only for targeting different units)
+* Fixed incorrect resolving of identifiers in commander abilities and stack experience definitions
+
+# 1.1.0 -> 1.1.1
+
+### GENERAL:
+* Fixed missing sound in Polish version from gog.com 
+* Fixed positioning of main menu buttons in localized versions of H3
+* Fixed crash on transferring artifact to commander
+* Fixed game freeze on receiving multiple artifact assembly dialogs after combat
+* Fixed potential game freeze on end of music playback
+* macOS/iOS: fixed sound glitches
+* Android: upgraded version of SDL library
+* Android: reworked right click gesture and relative pointer mode
+* Improved map loading speed
+* Ubuntu PPA: game will no longer crash on assertion failure
+
+### ADVENTURE MAP:
+* Fixed hero movement lag in single-player games
+* Fixed number of drowned troops on visiting Sirens to match H3
+* iOS: pinch gesture visits current object (Spacebar behavior) instead of activating in-game console
+
+### TOWNS:
+* Fixed displaying growth bonus from Statue of Legion
+* Growth bonus tooltip ordering now matches H3
+* Buy All Units dialog will now buy units starting from the highest level
+
+### LAUNCHER:
+* Local mods can be disabled or uninstalled
+* Fixed styling of Launcher interface
+
+### MAP EDITOR:
+* Fixed saving of roads and rivers
+* Fixed placement of heroes on map
+
+# 1.0.0 -> 1.1.0
+
+### GENERAL:
+* iOS is supported
+* Mods and their versions and serialized into save files. Game checks mod compatibility before loading
+* Logs are stored in system default logs directory
+* LUA/ERM libs are not compiled by default
+* FFMpeg dependency is optional now
+* Conan package manager is supported for MacOS and iOS
+
+### MULTIPLAYER:
+* Map is passed over network, so different platforms are compatible with each other
+* Server self-killing is more robust
+* Unlock in-game console while opponent's turn
+* Host can control game session by using console commands
+* Control over player is transferred to AI if client escaped the game
+* Reconnection mode for crashed client processes
+* Playing online is available using proxy server
+
+### ADVENTURE MAP:
+* Fix for digging while opponent's turn
+* Supported right click for quick recruit window
+* Fixed problem with quests are requiring identical artefacts
+* Bulk move and swap artifacts
+* Pause & resume for towns and terrains music themes
+* Feature to assemble/disassemble artefacts in backpack
+* Clickable status bar to send messages
+* Heroes no longer have chance to receive forbidden skill on leveling up
+* Fixed visibility of newly recruited heroes near town 
+* Fixed missing artifact slot in Artifact Merchant window
+
+### BATTLES:
+* Fix healing/regeneration behaviour and effect
+* Fix crashes related to auto battle
+* Implemented ray projectiles for shooters
+* Introduced default tower shooter icons
+* Towers destroyed during battle will no longer be listed as casualties
+
+### AI:
+* BattleAI: Target prioritizing is now based on damage difference instead of health difference
+* Nullkiller AI can retreat and surrender
+* Nullkiller AI doesn't visit allied dwellings anymore
+* Fixed a few freezes in Nullkiller AI
+
+### RANDOM MAP GENERATOR:
+* Speedup generation of random maps
+* Necromancy cannot be learned in Witch Hut on random maps
+
+### MODS:
+* Supported rewardable objects customization
+* Battleground obstacles are extendable now with VLC mechanism
+* Introduced "compatibility" section into mods settings
+* Fixed bonus system for custom advmap spells
+* Supported customisable town entrance placement
+* Fixed validation of mods with new adventure map objects
+
+### LAUNCHER:
+* Fixed problem with duplicated mods in the list
+* Launcher shows compatible mods only
+* Uninstall button was moved to the left of layout
+* Unsupported resolutions are not shown
+* Lobby for online gameplay is implemented
+
+### MAP EDITOR:
+* Basic version of Qt-based map editor
+
+# 0.99 -> 1.0.0
+
+### GENERAL:
+* Spectator mode was implemented through command-line options
+* Some main menu settings get saved after returning to main menu - last selected map, save etc.
+* Restart scenario button should work correctly now
+* Skyship Grail works now immediately after capturing without battle
+* Lodestar Grail implemented
+* Fixed Gargoyles immunity
+* New bonuses:
+* * SOUL_STEAL - "WoG ghost" ability, should work somewhat same as in H3
+* * TRANSMUTATION - "WoG werewolf"-like ability
+* * SUMMON_GUARDIANS - "WoG santa gremlin"-like ability + two-hex unit extension
+* * CATAPULT_EXTRA_SHOTS - defines number of extra wall attacks for units that can do so
+* * RANGED_RETALIATION - allows ranged counterattack
+* * BLOCKS_RANGED_RETALIATION - disallow enemy ranged counterattack
+* * SECONDARY_SKILL_VAL2 - set additional parameter for certain secondary skills
+* * MANUAL_CONTROL - grant manual control over war machine
+* * WIDE_BREATH - melee creature attacks affect many nearby hexes
+* * FIRST_STRIKE - creature counterattacks before attack if possible
+* * SYNERGY_TARGET - placeholder bonus for Mod Design Team (subject to removal in future)
+* * SHOOTS_ALL_ADJACENT - makes creature shots affect all neighbouring hexes
+* * BLOCK_MAGIC_BELOW - allows blocking spells below particular spell level. HotA cape artifact can be implemented with this
+* * DESTRUCTION - creature ability for killing extra units after hit, configurable
+
+### MULTIPLAYER:
+* Loading support. Save from single client could be used to load all clients.
+* Restart support. All clients will restart together on same server.
+* Hotseat mixed with network game. Multiple colors can be controlled by each client.
+
+### SPELLS:
+* Implemented cumulative effects for spells
+
+### MODS:
+* Improve support for WoG commander artifacts and skill descriptions
+* Added support for modding of original secondary skills and creation of new ones.
+* Map object sounds can now be configured via json
+* Added bonus updaters for hero specialties
+* Added allOf, anyOf and noneOf qualifiers for bonus limiters
+* Added bonus limiters: alignment, faction and terrain
+* Supported new terrains, new battlefields, custom water and rock terrains
+* Following special buildings becomes available in the fan towns:
+* * attackVisitingBonus
+* * defenceVisitingBonus
+* * spellPowerVisitingBonus
+* * knowledgeVisitingBonus
+* * experienceVisitingBonus
+* * lighthouse
+* * treasury
+
+### SOUND:
+* Fixed many mising or wrong pickup and visit sounds for map objects
+* All map objects now have ambient sounds identical to OH3
+
+### RANDOM MAP GENERATOR:
+* Random map generator supports water modes (normal, islands)
+* Added config randomMap.json with settings for map generator
+* Added parameter for template allowedWaterContent
+* Extra resource packs appear nearby mines
+* Underground can be player starting place for factions allowed to be placed underground
+* Improved obstacles placement aesthetics
+* Rivers are generated on the random maps
+* RMG works more stable, various crashes have been fixed
+* Treasures requiring guards are guaranteed to be protected
+
+### VCAI:
+* Reworked goal decomposition engine, fixing many loopholes. AI will now pick correct goals faster.
+* AI will now use universal pathfinding globally
+* AI can use Summon  Boat and Town Portal
+* AI can gather and save resources on purpose
+* AI will only buy army on demand instead of every turn
+* AI can distinguish the value of all map objects
+* General speed optimizations
+
+### BATTLES:
+* Towers should block ranged retaliation
+* AI can bypass broken wall with moat instead of standing and waiting until gate is destroyed
+* Towers do not attack war machines automatically
+* Draw is possible now as battle outcome in case the battle ends with only summoned creatures (both sides loose)
+
+### ADVENTURE MAP:
+* Added buttons and keyboard shortcuts to quickly exchange army and artifacts between heroes
+* Fix: Captured town should not be duplicated on the UI
+
+### LAUNCHER:
+* Implemented notifications about updates
+* Supported redirection links for downloading mods
+
+# 0.98 -> 0.99
+
+### GENERAL:
+* New Bonus NO_TERRAIN_PENALTY
+* Nomads will remove Sand movement penalty from army
+* Flying and water walking is now supported in pathfinder
+* New artifacts supported
+* * Angel Wings
+* * Boots of Levitation
+* Implemented rumors in tavern window
+* New cheat codes:
+* * vcmiglaurung - gives 5000 crystal dragons into each slot
+* * vcmiungoliant - conceal fog of war for current player
+* New console commands:
+* * gosolo - AI take control over human players and vice versa
+* * controlai - give control of one or all AIs to player
+* * set hideSystemMessages on/off - supress server messages in chat
+
+### BATTLES:
+* Drawbridge mechanics implemented (animation still missing)
+* Merging of town and visiting hero armies on siege implemented
+* Hero info tooltip for skills and mana implemented
+
+### ADVENTURE AI:
+* Fixed AI trying to go through underground rock
+* Fixed several cases causing AI wandering aimlessly
+* AI can again pick best artifacts and exchange artifacts between heroes
+* AI heroes with patrol enabled won't leave patrol area anymore
+
+### RANDOM MAP GENERATOR:
+* Changed fractalization algorithm so it can create cycles
+* Zones will not have straight paths anymore, they are totally random
+* Generated zones will have different size depending on template setting
+* Added Thieves Guild random object (1 per zone)
+* Added Seer Huts with quests that match OH3
+* RMG will guarantee at least 100 pairs of Monoliths are available even if there are not enough different defs
+
+# 0.97 -> 0.98
+
+### GENERAL:
+* Pathfinder can now find way using Monoliths and Whirlpools (only used if hero has protection)
+
+### ADVENTURE AI:
+* AI will try to use Monolith entrances for exploration
+* AI will now always revisit each exit of two way monolith if exit no longer visible
+* AI will eagerly pick guarded and blocked treasures
+
+### ADVENTURE MAP:
+* Implemented world view
+* Added graphical fading effects
+
+### SPELLS:
+* New spells handled:
+* * Earthquake
+* * View Air
+* * View Earth
+* * Visions
+* * Disguise
+* Implemented CURE spell negative dispell effect
+* Added LOCATION target for spells castable on any hex with new target modifiers
+
+### BATTLES:
+* Implemented OH3 stack split / upgrade formulas according to AlexSpl
+
+### RANDOM MAP GENERATOR:
+* Underground tunnels are working now
+* Implemented "junction" zone type
+* Improved zone placing algorithm
+* More balanced distribution of treasure piles
+* More obstacles within zones
+
+# 0.96 -> 0.97 (Nov 01 2014)
+
+### GENERAL:
+* (windows) Moved VCMI data directory from '%userprofile%\vcmi' to '%userprofile%\Documents\My Games\vcmi'
+* (windows) (OSX) Moved VCMI save directory from 'VCMI_DATA\Games' to 'VCMI_DATA\Saves'
+* (linux)
+* Changes in used librries:
+* * VCMI can now be compiled with SDL2
+* * Movies will use ffmpeg library
+* * change boost::bind to std::bind 
+* * removed boost::asign 
+* * Updated FuzzyLite to 5.0 
+* Multiplayer load support was implemented through command-line options
+
+### ADVENTURE AI:
+* Significantly optimized execution time, AI should be much faster now.
+
+### ADVENTURE MAP:
+* Non-latin characters can now be entered in chat window or used for save names.
+* Implemented separate speed for owned heroes and heroes owned by other players
+
+### GRAPHICS:
+* Better upscaling when running in fullscreen mode.
+* New creature/commader window
+* New resolutions and bonus icons are now part of a separate mod
+* Added graphics for GENERAL_DAMAGE_REDUCTION bonus (Kuririn)
+
+### RANDOM MAP GENERATOR:
+* Random map generator now creates complete and playable maps, should match original RMG
+* All important features from original map templates are implemented
+* Fixed major crash on removing objects
+* Undeground zones will look just like surface zones
+
+### LAUNCHER:
+* Implemented switch to disable intro movies in game
+
+# 0.95 -> 0.96 (Jul 01 2014)
+
+### GENERAL:
+* (linux) now VCMI follows XDG specifications. See http://forum.vcmi.eu/viewtopic.php?t=858
+
+### ADVENTURE AI:
+* Optimized speed and removed various bottlenecks.
+
+### ADVENTURE MAP:
+* Heroes auto-level primary and secondary skill levels according to experience
+
+### BATTLES:
+* Wall hit/miss sound will be played when using catapult during siege
+
+### SPELLS:
+* New configuration format
+
+### RANDOM MAP GENERATOR:
+* Towns from mods can be used
+* Reading connections, terrains, towns and mines from template
+* Zone placement
+* Zone borders and connections, fractalized paths inside zones
+* Guard generation
+* Treasue piles generation (so far only few removable objects)
+
+### MODS:
+* Support for submods - mod may have their own "submods" located in <modname>/Mods directory
+* Mods may provide their own changelogs and screenshots that will be visible in Launcher
+* Mods can now add new (offensive, buffs, debuffs) spells and change existing
+* Mods can use custom mage guild background pictures and videos for taverns, setting of resources daily income for buildings
+
+### GENERAL:
+* Added configuring of heroes quantity per player allowed in game
+
+# 0.94 -> 0.95 (Mar 01 2014)
+
+### GENERAL:
+* Components of combined artifacts will now display info about entire set.
+* Implements level limit
+* Added WoG creature abilities by Kuririn
+* Implemented a confirmation dialog when pressing Alt + F4 to quit the game 
+* Added precompiled header compilation for CMake (can be enabled per flag)
+* VCMI will detect changes in text files using crc-32 checksum
+* Basic support for unicode. Internally vcmi always uses utf-8
+* (linux) Launcher will be available as "VCMI" menu entry from system menu/launcher
+* (linux) Added a SIGSEV violation handler to vcmiserver executable for logging stacktrace (for convenience)
+
+### ADVENTURE AI:
+* AI will use fuzzy logic to compare and choose multiple possible subgoals.
+* AI will now use SectorMap to find a way to guarded / covered objects. 
+* Significantly improved exploration algorithm.
+* Locked heroes now try to decompose their goals exhaustively.
+* Fixed (common) issue when AI found neutral stacks infinitely strong.
+* Improvements for army exchange criteria.
+* GatherArmy may include building dwellings in town (experimental).
+* AI should now conquer map more aggressively and much faster
+* Fuzzy rules will be printed out at map launch (if AI log is enabled)
+
+### CAMPAIGNS:
+* Implemented move heroes to next scenario
+* Support for non-standard victory conditions for H3 campaigns
+* Campaigns use window with bonus & scenario selection than scenario information window from normal maps
+* Implemented hero recreate handling (e.g. Xeron will be recreated on AB campaign)
+* Moved place bonus hero before normal random hero and starting hero placement -> same behaviour as in OH3
+* Moved placing campaign heroes before random object generation -> same behaviour as in OH3 
+
+### TOWNS:
+* Extended building dependencies support
+
+### MODS:
+* Custom victory/loss conditions for maps or campaigns
+* 7 days without towns loss condition is no longer hardcoded
+* Only changed mods will be validated
+
+# 0.93 -> 0.94 (Oct 01 2013)
+
+### GENERAL:
+* New Launcher application, see 
+* Filesystem now supports zip archives. They can be loaded similarly to other archives in filesystem.json. Mods can use Content.zip instead of Content/ directory.
+* fixed "get txt" console command
+* command "extract" to extract file by name
+* command "def2bmp" to convert def into set of frames.
+* fixed crash related to cammander's SPELL_AFTER_ATTACK spell id not initialized properly (text id was resolved on copy of bonus)
+* fixed duels, added no-GUI mode for automatic AI testing
+* Sir Mullich is available at the start of the game
+* Upgrade cost will never be negative.
+* support for Chinese fonts (GBK 2-byte encoding)
+
+### ADVENTURE MAP:
+* if Quick Combat option is turned on, battles will be resolved by AI
+* first hero is awakened on new turn
+* fixed 3000 gems reward in shipwreck
+
+### BATTLES:
+* autofight implemented
+* most of the animations is time-based
+* simplified postioning of units in battle, should fix remaining issues with unit positioning
+* synchronized attack/defence animation
+* spell animation speed uses game settings
+* fixed disrupting ray duration
+* added logging domain for battle animations
+* Fixed crashes on Land Mines / Fire Wall casting.
+* UI will be correctly greyed-out during opponent turn
+* fixed remaining issues with blit order
+* Catapult attacks should be identical to H3. Catapult may miss and attack another part of wall instead (this is how it works in H3)
+* Fixed Remove Obstacle.
+* defeating hero will yield 500 XP
+* Added lots of missing spell immunities from Strategija
+* Added stone gaze immunity for Troglodytes (did you know about it?)
+* damage done by turrets is properly increased by built buldings
+* Wyverns will cast Poison instead of Stone Gaze.
+
+### TOWN:
+* Fixed issue that allowed to build multiple boats in town.
+* fix for lookout tower
+
+# 0.92 -> 0.93 (Jun 01 2013)
+
+### GENERAL:
+* Support for SoD-only installations, WoG becomes optional addition
+* New logging framework
+* Negative luck support, disabled by default
+* Several new icons for creature abilities (Fire Shield, Non-living, Magic Mirror, Spell-like Attack)
+* Fixed stack artifact (and related buttons) not displaying in creature window.
+* Fixed crash at month of double population.
+
+### MODS:
+* Improved json validation. Now it support most of features from latest json schema draft.
+* Icons use path to icon instead of image indexes.
+* It is possible to edit data of another mod or H3 data via mods.
+* Mods can access only ID's from dependenies, virtual "core" mod and itself (optional for some mods compatibility)
+* Removed no longer needed field "projectile spins"
+* Heroes: split heroes.json in manner similar to creatures\factions; string ID's for H3 heroes; h3 hero classes and artifacts can be modified via json.
+
+### BATTLES:
+* Fixed Death Stare of Commanders
+* Projectile blitting should be closer to original H3. But still not perfect.
+* Fixed missing Mirth effects
+* Stack affected by Berserk should not try to attack itself
+* Fixed several cases of incorrect positioning of creatures in battles
+* Fixed abilities of Efreet.
+* Fixed broken again palette in some battle backgrounds
+
+### TOWN:
+* VCMI will not crash if building selection area is smaller than def
+* Detection of transparency on selection area is closer to H3
+* Improved handling buildings with mode "auto":
+* * they will be properly processed (new creatures will be added if dwelling, spells learned if mage guild, and so on)
+* * transitive dependencies are handled (A makes B build, and B makes C and D)
+
+### SOUND:
+* Added missing WoG creature sounds (from Kuririn).
+* The Windows package comes with DLLs needed to play .ogg files
+* (linux) convertMP3 option for vcmibuilder for systems where SDL_Mixer can't play mp3's
+* some missing sounds for battle effects
+
+### ARTIFACTS:
+* Several fixes to combined artifacts added via mods.
+* Fixed Spellbinder's Hat giving level 1 spells instead of 5.
+* Fixed incorrect components of Cornucopia.
+* Cheat code with grant all artifacts, including the ones added by mods
+
+# 0.91 -> 0.92 (Mar 01 2013)
+
+### GENERAL:
+* hero crossover between missions in campaigns
+* introduction before missions in campaigns
+
+### MODS:
+* Added CREATURE_SPELL_POWER for commanders
+* Added spell modifiers to various spells: Hypnotize (Astral), Firewall (Luna), Landmine 
+* Fixed ENEMY_DEFENCE_REDUCTION, GENERAL_ATTACK_REDUCTION
+* Extended usefulness of ONLY_DISTANCE_FIGHT, ONLY_MELEE_FIGHT ranges
+* Double growth creatures are configurable now
+* Drain Life now has % effect depending on bonus value
+* Stack can use more than 2 attacks. Additional attacks can now be separated as "ONLY_MELEE_FIGHT and "ONLY_DISTANCE_FIGHT".
+* Moat damage configurable
+* More config options for spells:
+* * mind immunity handled by config
+* * direct damage immunity handled by config
+* * immunity icon configurable
+* * removed mind_spell flag 
+* creature config use string ids now. 
+* support for string subtype id in short bonus format
+* primary skill identifiers for bonuses
+
+# 0.9 -> 0.91 (Feb 01 2013)
+
+### GENERAL:
+* VCMI build on OS X is now supported
+* Completely removed autotools
+* Added RMG interace and ability to generate simplest working maps
+* Added loading screen
+
+### MODS:
+* Simplified mod structure. Mods from 0.9 will not be compatible.
+* Mods can be turned on and off in config/modSettings.json file
+* Support for new factions, including:
+* * New towns
+* * New hero classes
+* * New heroes
+* * New town-related external dwellings
+* Support for new artifact, including combined, commander and stack artifacts
+* Extended configuration options
+* * All game objects are referenced by string identifiers
+* * Subtype resolution for bonuses
+
+### BATTLES:
+* Support for "enchanted" WoG ability
+
+### ADVENTURE AI:
+* AI will try to use Subterranean Gate, Redwood Observatory and Cartographer for exploration
+* Improved exploration algorithm
+* AI will prioritize dwellings and mines when there are no opponents visible
+
+# 0.89 -> 0.9 (Oct 01 2012)
+
+### GENERAL:
+* Provisional support creature-adding mods
+* New filesystem allowing easier resource adding/replacing
+* Reorganized package for better compatibility with HotA and not affecting the original game
+* Moved many hard-coded settings into text config files
+* Commander level-up dialog
+* New Quest Log window
+* Fixed a number of bugs in campaigns, support for starting hero selection bonus. 
+
+### BATTLES:
+* New graphics for Stack Queue
+* Death Stare works identically to H3
+* No explosion when catapult fails to damage the wall
+* Fixed crash when attacking stack dies before counterattack
+* Fixed crash when attacking stack dies in the Moat just before the attack
+* Fixed Orb of Inhibition and Recanter's Cloak (they were incorrectly implemented)
+* Fleeing hero won't lose artifacts.
+* Spellbook won't be captured. 
+
+### ADVENTURE AI:
+* support for quests (Seer Huts, Quest Guardians, and so)
+* AI will now wander with all the heroes that have spare movement points. It should prevent stalling.
+* AI will now understand threat of Abandoned Mine.
+* AI can now exchange armies between heroes. By default, it will pass army to main hero.
+* Fixed strange case when AI found allied town extremely dangerous
+* Fixed crash when AI tried to "revisit" a Boat
+* Fixed crash when hero assigned to goal was lost when attempting realizing it
+* Fixed a possible freeze when exchanging resources at marketplace
+
+### BATTLE AI:
+* It is possible to select a battle AI module used by VCMI by typing into the console "setBattleAI <name>". The names of avaialble modules are "StupidAI" and "BattleAI". BattleAI may be a little smarter but less stable. By the default, StupidAI will be used, as in previous releases.
+* New battle AI module: "BattleAI" that is smarter and capable of casting some offensive and enchantment spells
+
+# 0.88 -> 0.89 (Jun 01 2012)
+
+### GENERAL:
+* Mostly implemented Commanders feature (missing level-up dialog)
+* Support for stack artifacts
+* New creature window graphics contributed by fishkebab
+* Config file may have multiple upgrades for creatures
+* CTRL+T will open marketplace window
+* G will open thieves guild window if player owns at least one town with tavern
+* Implemented restart functionality. CTRL+R will trigger a quick restart
+* Save game screen and returning to main menu will work if game was started with --start option
+* Simple mechanism for detecting game desynchronization after init
+* 1280x800 resolution graphics, contributed by Topas
+
+### ADVENTURE MAP:
+* Fixed monsters regenerating casualties from battle at the start of new week.
+* T in adventure map will switch to next town
+
+### BATTLES:
+* It's possible to switch active creature during tacts phase by clicking on stack
+* After battle artifacts of the defeated hero (and his army) will be taken by winner
+* Rewritten handling of battle obstacles. They will be now placed following H3 algorithm.
+* Fixed crash when death stare or acid breath activated on stack that was just killed
+* First aid tent can heal only creatures that suffered damage
+* War machines can't be healed by tent
+* Creatures casting spells won't try to cast them during tactic phase
+* Console tooltips for first aid tent
+* Console tooltips for teleport spell
+* Cursor is reset to pointer when action is requested
+* Fixed a few other missing or wrong tooltips/cursors
+* Implemented opening creature window by l-clicking on stack
+* Fixed crash on attacking walls with Cyclop Kings
+* Fixed and simplified Teleport casting
+* Fixed Remove Obstacle spell
+* New spells supported:
+* * Chain Lightning
+* * Fire Wall
+* * Force Field
+* * Land Mine
+* * Quicksands
+* * Sacrifice
+
+### TOWNS:
+* T in castle window will open a tavern window (if available)
+
+### PREGAME:
+* Pregame will use same resolution as main game
+* Support for scaling background image
+* Customization of graphics with config file.
+
+### ADVENTURE AI:
+* basic rule system for threat evaluation
+* new town development logic
+* AI can now use external dwellings
+* AI will weekly revisit dwellings & mills
+* AI will now always pick best stacks from towns
+* AI will recruit multiple heroes for exploration
+* AI won't try attacking its own heroes
+
+# 0.87 -> 0.88 (Mar 01 2012)
+
+* added an initial version of new adventure AI: VCAI
+* system settings window allows to change default resolution
+* introduced unified JSON-based settings system
+* fixed all known localization issues
+* Creature Window can handle descriptions of spellcasting abilities
+* Support for the clone spell
+
+# 0.86 -> 0.87 (Dec 01 2011)
+
+### GENERAL:
+* Pathfinder can find way using ships and subterranean gates
+* Hero reminder & sleep button
+
+### PREGAME:
+* Credits are implemented
+
+### BATTLES:
+* All attacked hexes will be highlighted
+* New combat abilities supported:
+* * Spell Resistance aura
+* * Random spellcaster (Genies)
+* * Mana channeling
+* * Daemon summoning
+* * Spellcaster (Archangel Ogre Mage, Elementals, Faerie Dragon)
+* * Fear
+* * Fearless
+* * No wall penalty
+* * Enchanter
+* * Bind
+* * Dispell helpful spells
+
+# 0.85 -> 0.86 (Sep 01 2011)
+
+### GENERAL:
+* Reinstated music support
+* Bonus system optimizations (caching)
+* converted many config files to JSON
+* .tga file support
+* New artifacts supported
+* * Admiral's Hat
+* * Statue of Legion
+* * Titan's Thunder
+
+### BATTLES:
+* Correct handling of siege obstacles
+* Catapult animation
+* New combat abilities supported
+* * Dragon Breath
+* * Three-headed Attack
+* * Attack all around
+* * Death Cloud / Fireball area attack
+* * Death Blow
+* * Lightning Strike
+* * Rebirth
+* New WoG abilities supported
+* * Defense Bonus
+* * Cast before attack
+* * Immunity to direct damage spells
+* New spells supported
+* * Magic Mirror
+* * Titan's Lightning Bolt
+
+# 0.84 -> 0.85 (Jun 01 2011)
+
+### GENERAL:
+* Support for stack experience
+* Implemented original campaign selection screens
+* New artifacts supported:
+* * Statesman's Medal
+* * Diplomat's Ring
+* * Ambassador's Sash
+
+### TOWNS:
+* Implemented animation for new town buildings
+* It's possible to sell artifacts at Artifact Merchants
+
+### BATTLES:
+* Neutral monsters will be split into multiple stacks
+* Hero can surrender battle to keep army
+* Support for Death Stare, Support for Poison, Age, Disease, Acid Breath, Fire / Water / Earth / Air immunities and Receptiveness
+* Partial support for Stone Gaze, Paralyze, Mana drain
+
+# 0.83 -> 0.84 (Mar 01 2011)
+
+### GENERAL:
+* Bonus system has been rewritten
+* Partial support for running VCMI in duel mode (no adventure map, only one battle, ATM only AI-AI battles)
+* New artifacts supported:
+* * Angellic Alliance
+* * Bird of Perception
+* * Emblem of Cognizance
+* * Spell Scroll
+* * Stoic Watchman
+
+### BATTLES:
+* Better animations handling
+* Defensive stance is supported
+
+### HERO:
+* New secondary skills supported:
+* * Artillery
+* * Eagle Eye
+* * Tactics
+
+### AI PLAYER:
+* new AI leading neutral creatures in combat, slightly better then previous
+
+# 0.82 -> 0.83 (Nov 01 2010)
+
+### GENERAL:
+* Alliances support
+* Week of / Month of events
+* Mostly done pregame for MP games (temporarily only for local clients)
+* Support for 16bpp displays
+* Campaigns:
+* * support for building bonus
+* * moving to next map after victory
+* Town Portal supported
+* Vial of Dragon Blood and Statue of Legion supported
+
+### HERO:
+* remaining specialities have been implemented
+
+### TOWNS:
+* town events supported
+* Support for new town structures: Deiety of Fire and Escape Tunnel 
+
+### BATTLES:
+* blocked retreating from castle
+
+# 0.81 -> 0.82 (Aug 01 2010)
+
+### GENERAL:
+* Some of the starting bonuses in campaigns are supported
+* It's possible to select difficulty level of mission in campaign
+* new cheat codes:
+* * vcmisilmaril - player wins
+* * vcmimelkor - player loses
+
+### ADVENTURE MAP:
+* Neutral armies growth implemented (10% weekly)
+* Power rating of neutral stacks
+* Favourable Winds reduce sailing cost
+
+### HERO:
+* Learning secondary skill supported.
+* Most of hero specialities are supported, including:
+* * Creature specialities (progressive, fixed, Sir Mullich)
+* * Spell damage specialities (Deemer), fixed bonus (Ciele)
+* * Secondary skill bonuses
+* * Creature Upgrades (Gelu)
+* * Resorce generation
+* * Starting Skill (Adrienne)
+
+### TOWNS:
+* Support for new town structures:
+* * Artifact Merchant
+* * Aurora Borealis
+* * Castle Gates
+* * Magic University
+* * Portal of Summoning 
+* * Skeleton transformer
+* * Veil of Darkness
+
+### OBJECTS:
+* Stables will now upgrade Cavaliers to Champions.
+* New object supported:
+* * Abandoned Mine
+* * Altar of Sacrifice
+* * Black Market
+* * Cover of Darkness
+* * Hill Fort
+* * Refugee Camp
+* * Sanctuary
+* * Tavern
+* * University
+* * Whirlpool
+
+# 0.8 -> 0.81 (Jun 01 2010)
+
+### GENERAL:
+* It's possible to start campaign
+* Support for build grail victory condition
+* New artifacts supported:
+* * Angel's Wings
+* * Boots of levitation
+* * Orb of Vulnerability
+* * Ammo cart
+* * Golden Bow
+* * Hourglass of Evil Hour
+* * Bow of Sharpshooter
+* * Armor of the Damned
+
+### ADVENTURE MAP:
+* Creatures now guard surrounding tiles
+* New adventura map spells supported:
+* * Summon Boat
+* * Scuttle Boat 
+* * Dimension Door
+* * Fly
+* * Water walk
+
+### BATTLES:
+* A number of new creature abilities supported
+* First Aid Tent is functional
+* Support for distance/wall/melee penalties & no * penalty abilities
+* Reworked damage calculation to fit OH3 formula better
+* Luck support
+* Teleportation spell
+
+### HERO:
+* First Aid secondary skill
+* Improved formula for necromancy to match better OH3
+
+### TOWNS:
+* Sending resources to other players by marketplace
+* Support for new town structures:
+* * Lighthouse
+* * Colossus
+* * Freelancer's Guild
+* * Guardian Spirit
+* * Necromancy Amplifier
+* * Soul Prison
+
+### OBJECTS:
+* New object supported:
+* * Freelancer's Guild
+* * Trading Post
+* * War Machine Factory
+
+# 0.75 -> 0.8 (Mar 01 2010)
+
+### GENERAL:
+* Victory and loss conditions are supported. It's now possible to win or lose the game.
+* Implemented assembling and disassembling of combination artifacts.
+* Kingdom Overview screen is now available.
+* Implemented Grail (puzzle map, digging, constructing ultimate building)
+* Replaced TTF fonts with original ones.
+
+### ADVENTURE MAP:
+* Implemented rivers animations (thx to GrayFace).
+
+### BATTLES:
+* Fire Shield spell (and creature ability) supported
+* affecting morale/luck and casting spell after attack creature abilities supported
+
+### HERO:
+* Implementation of Scholar secondary skill
+
+### TOWN:
+* New left-bottom info panel functionalities.
+
+### TOWNS:
+* new town structures supported:
+* * Ballista Yard
+* * Blood Obelisk
+* * Brimstone Clouds
+* * Dwarven Treasury
+* * Fountain of Fortune
+* * Glyphs of Fear
+* * Mystic Pond
+* * Thieves Guild
+* * Special Grail functionalities for Dungeon, Stronghold and Fortress
+
+### OBJECTS:
+* New objects supported:
+* * Border gate
+* * Den of Thieves
+* * Lighthouse
+* * Obelisk
+* * Quest Guard
+* * Seer hut
+
+A lot of of various bugfixes and improvements:
+http://bugs.vcmi.eu/changelog_page.php?version_id=14
+
+# 0.74 -> 0.75 (Dec 01 2009)
+
+### GENERAL:
+* Implemented "main menu" in-game option.
+* Hide the mouse cursor while displaying a popup window.
+* Better handling of huge and empty message boxes (still needs more changes)
+* Fixed several crashes when exiting.
+
+### ADVENTURE INTERFACE:
+* Movement cursor shown for unguarded enemy towns.
+* Battle cursor shown for guarded enemy garrisons.
+* Clicking on the border no longer opens an empty info windows
+
+### HERO WINDOW:
+* Improved artifact moving. Available slots are higlighted. Moved artifact is bound to mouse cursor. 
+
+### TOWNS:
+* new special town structures supported:
+* * Academy of Battle Scholars
+* * Cage of Warlords
+* * Mana Vortex
+* * Stables
+* * Skyship (revealing entire map only)
+
+### OBJECTS:
+* External dwellings increase town growth
+* Right-click info window for castles and garrisons you do not own shows a rough amount of creatures instead of none
+* Scholar won't give unavaliable spells anymore.
+
+A lot of of various bugfixes and improvements:
+http://bugs.vcmi.eu/changelog_page.php?version_id=2
+
+# 0.73 -> 0.74 (Oct 01 2009)
+
+### GENERAL:
+* Scenario Information window
+* Save Game window
+* VCMI window should start centered
+* support for Necromancy and Ballistics secondary skills
+* new artifacts supported, including those improving Necromancy, Legion Statue parts, Shackles of War and most of combination artifacts (but not combining)
+* VCMI client has its own icon (thx for graphic to Dikamilo)
+* Ellipsis won't be split when breaking text on several lines
+* split button will be grayed out when no creature is selected
+* fixed issue when splitting stack to the hero with only one creatures
+* a few fixes for shipyard window
+
+### ADVENTURE INTERFACE:
+* Cursor shows if tile is accesible and how many turns away
+* moving hero with arrow keys / numpad
+* fixed Next Hero button behaviour
+* fixed Surface/Underground switch button in higher resolutions
+
+### BATTLES:
+* partial siege support
+* new stack queue for higher resolutions (graphics made by Dru, thx!)
+* 'Q' pressing toggles the stack queue displaying (so it can be enabled/disabled it with single key press)
+* more creatures special abilities supported
+* battle settings will be stored
+* fixed crashes occurring on attacking two hex creatures from back
+* fixed crash when clicking on enemy stack without moving mouse just after receiving action
+* even large stack numbers will fit the boxes
+* when active stack is killed by spell, game behaves properly
+* shooters attacking twice (like Grand Elves) won't attack twice in melee 
+* ballista can shoot even if there's an enemy creature next to it 
+* improved obstacles placement, so they'll better fit hexes (thx to Ivan!)
+* selecting attack directions works as in H3
+* estimating damage that will be dealt while choosing stack to be attacked
+* modified the positioning of battle effects, they should look about right now.
+* after selecting a spell during combat, l-click is locked for any action other than casting. 
+* flying creatures will be blitted over all other creatures, obstacles and wall
+* obstacles and units should be printed in better order (not tested)
+* fixed armageddon animation
+* new spells supported:
+* * Anti-Magic
+* * Cure
+* * Resurrection 
+* * Animate Dead 
+* * Counterstrike 
+* * Berserk 
+* * Hypnotize 
+* * Blind 
+* * Fire Elemental 
+* * Earth Elemental 
+* * Water Elemental 
+* * Air Elemental 
+* * Remove obstacle
+
+### TOWNS:
+* enemy castle can be taken over
+* only one capitol per player allowed (additional ones will be lost)
+* garrisoned hero can buy a spellbook
+* heroes available in tavern should be always different
+* ship bought in town will be correctly placed
+* new special town structures supported:
+* * Lookout Tower
+* * Temple of Valhalla
+* * Wall of Knowledge
+* * Order of Fire
+
+### HERO WINDOW:
+* war machines cannot be unequiped
+
+### PREGAME:
+* sorting: a second click on the column header sorts in descending order.
+* advanced options tab: r-click popups for selected town, hero and bonus
+* starting scenario / game by double click
+* arrows in options tab are hidden when not available 
+* subtitles for chosen hero/town/bonus in pregame
+
+### OBJECTS:
+* fixed pairing Subterranean Gates
+* New objects supported:
+* * Borderguard & Keymaster Tent
+* * Cartographer
+* * Creature banks
+* * Eye of the Magi & Hut of the Magi
+* * Garrison
+* * Stables
+* * Pandora Box
+* * Pyramid
+
+# 0.72 -> 0.73 (Aug 01 2009)
+
+### GENERAL:
+* infowindow popup will be completely on screen
+* fixed possible crash with in game console
+* fixed crash when gaining artifact after r-click on hero in tavern
+* Estates / hero bonuses won't give resources on first day. 
+* video handling (intro, main menu animation, tavern animation, spellbook animation, battle result window)
+* hero meeting window allowing exchanging armies and artifacts between heroes on adventure map
+* 'T' hotkey opens marketplace window
+* giving starting spells for heroes
+* pressing enter or escape close spellbook
+* removed redundant quotation marks from skills description and artifact events texts
+* disabled autosaving on first turn
+* bonuses from bonus artifacts
+* increased char per line limit for subtitles under components
+* corrected some exp/level values
+* primary skills cannot be negative
+* support for new artifacts: Ring of Vitality, Ring of Life, Vial of Lifeblood, Garniture of Interference, Surcoat of Counterpoise, Boots of Polarity
+* fixed timed events reappearing
+* saving system options
+* saving hero direction
+* r-click popups on enemy heroes and towns
+* hero leveling formula matches the H3
+
+### ADVENTURE INTERFACE:
+* Garrisoning, then removing hero from garrison move him at the end of the heroes list
+* The size of the frame around the map depends on the screen size.
+* spellbook shows adventure spells when opened on adventure map
+* erasing path after picking objects with last movement point
+
+### BATTLES:
+* spell resistance supported (secondary skill, artifacts, creature skill)
+* corrected damage inflicted by spells and ballista
+* added some missing projectile infos
+* added native terrain bonuses in battles
+* number of units in stack in battle should better fit the box
+* non-living and undead creatures have now always 0 morale
+* displaying luck effect animation
+* support for battleground overlays:
+* * cursed ground
+* * magic plains
+* * fiery fields
+* * rock lands
+* * magic clouds
+* * lucid pools
+* * holy ground
+* * clover field
+* * evil fog
+
+### TOWNS:
+* fixes for horde buildings
+* garrisoned hero can buy a spellbook if he is selected or if there is no visiting hero
+* capitol bar in town hall is grey (not red) if already one exists
+* fixed crash on entering hall when town was near map edge
+
+### HERO WINDOW:
+* garrisoned heroes won't be shown on the list
+* artifacts will be present on morale/luck bonuses list
+
+### PREGAME:
+* saves are sorted primary by map format, secondary by name
+* fixed displaying date of saved game (uses local time, removed square character)
+
+### OBJECTS:
+* Fixed primary/secondary skill levels given by a scholar.
+* fixed problems with 3-tiles monoliths
+* fixed crash with flaggable building next to map edge
+* fixed some descriptions for events
+* New objects supported:
+* * Buoy
+* * Creature Generators
+* * Flotsam
+* * Mermaid
+* * Ocean bottle
+* * Sea Chest 
+* * Shipwreck Survivor
+* * Shipyard
+* * Sirens 
+
+# 0.71 -> 0.72 (Jun 1 2009)
+
+### GENERAL:
+* many sound effects and music
+* autosave (to 5 subsequent files)
+* artifacts support (most of them)
+* added internal game console (activated on TAB)
+* fixed 8 hero limit to check only for wandering heroes (not garrisoned)
+* improved randomization
+* fixed crash on closing application
+* VCMI won't always give all three stacks in the starting armies
+* fix for drawing starting army creatures count
+* Diplomacy secondary skill support
+* timed events won't cause resources amount to be negative
+* support for sorcery secondary skill
+* reduntant quotation marks from artifact descriptions are removed
+* no income at the first day
+
+### ADVENTURE INTERFACE:
+* fixed crasbug occurring on revisiting objects (by pressing space)
+* always restoring default cursor when movng mouse out of the terrain
+* fixed map scrolling with ctrl+arrows when some windows are opened
+* clicking scrolling arrows in town/hero list won't open town/hero window
+* pathfinder will now look for a path going via printed positions of roads when it's possible
+* enter can be used to open window with selected hero/town
+
+### BATTLES:
+* many creatures special skills implemented
+* battle will end when one side has only war machines
+* fixed some problems with handling obstacles info
+* fixed bug with defending / waiting while no stack is active
+* spellbook button is inactive when hero cannot cast any spell
+* obstacles will be placed more properly when resolution is different than 800x600
+* canceling of casting a spell by pressing Escape or R-click (R-click on a creatures does not cancel a spell)
+* spellbook cannot be opened by L-click on hero in battle when it shouldn't be possible
+* new spells:
+* * frost ring
+* * fireball
+* * inferno
+* * meteor shower
+* * death ripple
+* * destroy undead
+* * dispel
+* * armageddon
+* * disrupting ray
+* * protection from air
+* * protection from fire
+* * protection from water
+* * protection from earth
+* * precision
+* * slayer
+
+### TOWNS:
+* resting in town with mage guild will replenih all the mana points
+* fixed Blacksmith
+* the number of creatures at the beginning of game is their base growth
+* it's possible to enter Tavern via Brotherhood of Sword
+
+### HERO WINDOW:
+* fixed mana limit info in the hero window
+* war machines can't be removed
+* fixed problems with removing artifacts when all visible slots in backpack are full
+
+### PREGAME:
+* clicking on "advanced options" a second time now closes the tab instead of refreshing it.
+* Fix position of maps names. 
+* Made the slider cursor much more responsive. Speedup the map select screen.
+* Try to behave when no maps/saves are present.
+* Page Up / Page Down / Home / End hotkeys for scrolling through scenarios / games list
+
+### OBJECTS:
+* Neutral creatures can join or escape depending on hero strength (escape formula needs to be improved)
+* leaving guardians in flagged mines.
+* support for Scholar object
+* support for School of Magic
+* support for School of War
+* support for Pillar of Fire
+* support for Corpse
+* support for Lean To
+* support for Wagon
+* support for Warrior's Tomb
+* support for Event
+* Corpse (Skeleton) will be accessible from all directions
+
+# 0.7 -> 0.71 (Apr 01 2009)
+
+### GENERAL:
+* fixed scrolling behind window problem (now it's possible to scroll with CTRL + arrows) 
+* morale/luck system and corresponding sec. skills supported 
+* fixed crash when hero get level and has less than two sec. skills to choose between 
+* added keybindings for components in selection window (eg. for treasure chest dialog): 1, 2, and so on. Selection dialog can be closed with Enter key
+* proper handling of custom portraits of heroes
+* fixed problems with non-hero/town defs not present in def list but present on map (occurring probably only in case of def substitution in map editor)
+* fixed crash when there was no hero available to hire for some player 
+* fixed problems with 1024x600 screen resolution
+* updating blockmap/visitmap of randomized objects 
+* fixed crashes on loading maps with flag all mines/dwelling victory condition
+* further fixes for leveling-up (stability and identical offered skills bug)
+* splitting window allows to rebalance two stack with the same creatures
+* support for numpad keyboard
+* support for timed events
+
+### ADVENTURE INTERFACE:
+* added "Next hero" button functionality
+* added missing path arrows
+* corrected centering on hero's position 
+* recalculating hero path after reselecting hero
+* further changes in pathfinder making it more like original one
+* orientation of hero can't be change if movement points are exhausted 
+* campfire, borderguard, bordergate, questguard will be accessible from the top
+* new movement cost calculation algorithm
+* fixed sight radious calculation
+* it's possible to stop hero movement
+* faster minimap refreshing 
+* provisional support for "Save" button in System Options Window
+* it's possible to revisit object under hero by pressing Space
+
+### BATTLES:
+* partial support for battle obstacles
+* only one spell can be casted per turn
+* blocked opening sepllbook if hero doesn't have a one
+* spells not known by hero can't be casted 
+* spell books won't be placed in War Machine slots after battle
+* attack is now possible when hex under cursor is not displayed 
+* glowing effect of yellow border around creatures
+* blue glowing border around hovered creature
+* made animation on battlefield more smooth
+* standing stacks have more static animation
+* probably fixed problem with displaying corpses on battlefield
+* fixes for two-hex creatures actions
+* fixed hero casting spell animation
+* corrected stack death animation
+* battle settings will be remembered between battles
+* improved damage calculation formula
+* correct handling of flying creatures in battles
+* a few tweaks in battle path/available hexes calculation (more of them is needed)
+* amounts of units taking actions / being an object of actions won't be shown until action ends
+* fixed positions of stack queue and battle result window when resolution is != 800x600 
+* corrected duration of frenzy spell which was incorrect in certain cases 
+* corrected hero spell casting animation
+* better support for battle backgrounds 
+* blocked "save" command during battle 
+* spellbook displays only spells known by Hero
+* New spells supported:
+* * Mirth
+* * Sorrow
+* * Fortune
+* * Misfortune
+
+### TOWN INTERFACE:
+* cannot build more than one capitol
+* cannot build shipyard if town is not near water
+* Rampart's Treasury requires Miner's Guild 
+* minor improvements in Recruitment Window
+* fixed crash occurring when clicking on hero portrait in Tavern Window, minor improvements for Tavern Window
+* proper updating resdatabar after building structure in town or buying creatures (non 800x600 res)
+* fixed blinking resdatabar in town screen when buying (800x600) 
+* fixed horde buildings displaying in town hall
+* forbidden buildings will be shown as forbidden, even if there are no res / other conditions are not fulfilled
+
+### PREGAME:
+* added scrolling scenario list with mouse wheel
+* fixed mouse slow downs
+* cannot select heroes for computer player (pregame) 
+* no crash if uses gives wrong resolution ID number
+* minor fixes
+
+### OBJECTS:
+* windmill gives 500 gold only during first week ever (not every month)
+* After the first visit to the Witch Hut, right-click/hover tip mentions the skill available. 
+* New objects supported:
+* * Prison
+* * Magic Well
+* * Faerie Ring
+* * Swan Pond
+* * Idol of Fortune
+* * Fountain of Fortune
+* * Rally Flag
+* * Oasis
+* * Temple
+* * Watering Hole
+* * Fountain of Youth
+* * support for Redwood Observatory
+* * support for Shrine of Magic Incantation / Gesture / Thought
+* * support for Sign / Ocean Bottle
+
+### AI PLAYER:
+* Minor improvements and fixes.
+
+# 0.64 -> 0.7 (Feb 01 2009)
+
+### GENERAL:
+* move some settings to the config/settings.txt file
+* partial support for new screen resolutions
+* it's possible to set game resolution in pregame (type 'resolution' in the console) 
+* /Data and /Sprites subfolders can be used for adding files not present in .lod archives
+* fixed crashbug occurring when hero levelled above 15 level
+* support for non-standard screen resolutions
+* F4 toggles between full-screen and windowed mode
+* minor improvements in creature card window
+* splitting stacks with the shift+click 
+* creature card window contains info about modified speed 
+
+### ADVENTURE INTERFACE:
+* added water animation
+* speed of scrolling map and hero movement can be adjusted in the System Options Window
+* partial handling r-clicks on adventure map
+
+### TOWN INTERFACE:
+* the scroll tab won't remain hanged to our mouse position if we move the mouse is away from the scroll bar
+* fixed cloning creatures bug in garrisons (and related issues)
+
+### BATTLES:
+* support for the Wait command
+* magic arrow *really* works
+* war machines support partially added
+* queue of stacks narrowed
+* spell effect animation displaying improvements
+* positive/negative spells cannot be cast on hostile/our stacks
+* showing spell effects affecting stack in creature info window
+* more appropriate coloring of stack amount box when stack is affected by a spell
+* battle console displays notifications about wait/defend commands 
+* several reported bugs fixed
+* new spells supported:
+* * Haste
+* * lightning bolt
+* * ice bolt
+* * slow
+* * implosion
+* * forgetfulness
+* * shield
+* * air shield
+* * bless
+* * curse
+* * bloodlust
+* * weakness
+* * stone skin
+* * prayer
+* * frenzy
+
+### AI PLAYER:
+* Genius AI (first VCMI AI) will control computer creatures during the combat.
+
+### OBJECTS:
+* Guardians property for resources is handled
+* support for Witch Hut
+* support for Arena
+* support for Library of Enlightenment 
+
+And a lot of minor fixes
+
+# 0.63 -> 0.64 (Nov 01 2008)
+
+### GENERAL:
+* sprites from /Sprites folder are handled correctly
+* several fixes for pathfinder and path arrows
+* better handling disposed/predefined heroes
+* heroes regain 1 mana point each turn
+* support for mistycisim and intelligence skills
+* hero hiring possible
+* added support for a number of hotkeys
+* it's not possible anymore to leave hero level-up window without selecting secondary skill
+* many minor improvements
+
+* Added some kind of simple chatting functionality through console. Implemented several WoG cheats equivalents:
+* * woggaladriel -> vcmiainur
+* * wogoliphaunt -> vcminoldor
+* * wogshadowfax -> vcminahar
+* * wogeyeofsauron -> vcmieagles
+* * wogisengard -> vcmiformenos
+* * wogsaruman -> vcmiistari
+* * wogpathofthedead -> vcmiangband 
+* * woggandalfwhite -> vcmiglorfindel
+
+### ADVENTURE INTERFACE:
+* clicking on a tile in advmap view when a path is shown will not only hide it but also calculate a new one 
+* slowed map scrolling 
+* blocked scrolling adventure map with mouse when left ctrl is pressed
+* blocked map scrolling when dialog window is opened
+* scholar will be accessible from the top
+
+### TOWN INTERFACE:
+* partially done tavern window (only hero hiring functionality)
+
+### BATTLES:
+* water elemental will really be treated as 2 hex creature
+* potential infinite loop in reverseCreature removed
+* better handling of battle cursor 
+* fixed blocked shooter behavior
+* it's possible in battles to check remeaining HP of neutral stacks
+* partial support for Magic Arrow spell
+* fixed bug with dying unit
+* stack queue hotkey is now 'Q'
+* added shots limit 
+
+# 0.62 -> 0.63 (Oct 01 2008)
+
+### GENERAL:
+* coloured console output, logging all info to txt files
+* it's possible to use other port than 3030 by passing it as an additional argument
+* removed some redundant warnings
+* partially done spellbook
+* Alt+F4 quits the game
+* some crashbugs was fixed
+* added handling of navigation, logistics, pathfinding, scouting end estates secondary skill
+* magical hero are given spellbook at the beginning
+* added initial secondary skills for heroes 
+
+### BATTLES:
+* very significant optimization of battles 
+* battle summary window
+* fixed crashbug occurring sometimes on exiting battle 
+* confirm window is shown before retreat 
+* graphic stack queue in battle (shows when 'c' key is pressed)
+* it's possible to attack enemy hero
+* neutral monster army disappears when defeated
+* casualties among hero army and neutral creatures are saved
+* better animation handling in battles
+* directional attack in battles 
+* mostly done battle options (although they're not saved)
+* added receiving exp (and leveling-up) after a won battle 
+* added support for archery, offence and armourer secondary abilities 
+* hero's primary skills accounted for damage dealt by creatures in battle
+
+### TOWNS:
+* mostly done marketplace 
+* fixed crashbug with battles on swamps and rough terrain
+* counterattacks 
+* heroes can learn new spells in towns
+* working resource silo
+* fixed bug with the mage guild when no spells available
+* it's possible to build lighthouse
+
+### HERO WINDOW:
+* setting army formation
+* tooltips for artifacts in backpack
+
+### ADVENTURE INTERFACE:
+* fixed bug with disappearing head of a hero in adventure map
+* some objects are no longer accessible from the top 
+* no tooltips for objects under FoW
+* events won't be shown
+* working Subterranean Gates, Monoliths
+* minimap shows all flaggable objects (towns, mines, etc.) 
+* artifacts we pick up go to the appropriate slot (if free)
+
+# 0.61 -> 0.62 (Sep 01 2008)
+
+### GENERAL:
+* restructured to the server-client model
+* support for heroes placed in towns
+* upgrading creatures
+* working gaining levels for heroes (including dialog with skill selection)
+* added graphical cursor
+* showing creature amount in the creature info window
+* giving starting bonus
+
+### CASTLES:
+* icon in infobox showing that there is hero in town garrison
+* fort/citadel/castle screen
+* taking last stack from the heroes army should be impossible (or at least harder)
+* fixed reading forbidden structures
+* randomizing spells in towns
+* viewing hero window in the town screen
+* possibility of moving hero into the garrison
+* mage guild screen 
+* support for blacksmith
+* if hero doesn't have a spell book, he can buy one in a mage guild
+* it's possible to build glyph of fear in fortress
+* creatures placeholders work properly
+
+### ADVENTURE INTERFACE:
+* hopefully fixed problems with wrong town defs (village/fort/capitol)
+
+### HERO WINDOW:
+* bugfix: splitting stacks works in hero window
+* removed bug causing significant increase of CPU consumption
+
+### BATTLES:
+* shooting
+* removed some displaying problems
+* showing last group of frames in creature animation won't crash
+* added start moving and end moving animations
+* fixed moving two-hex creatures
+* showing/hiding graphic cursor
+* a part of using graphic cursor
+* slightly optimized showing of battle interface
+* animation of getting hit / death by shooting is displayed when it should be
+* improved pathfinding in battles, removed problems with displaying movement, adventure map interface won't be called during battles.
+* minor optimizations
+
+### PREGAME:
+* updates settings when selecting new map after changing sorting criteria
+* if sorting not by name, name will be used as a secondary criteria
+* when filter is applied a first available map is selected automatically
+* slider position updated after sorting in pregame
+
+### OBJECTS:
+* support for the Tree of knowledge
+* support for Campfires
+* added event message when picking artifact
+
+# 0.6 -> 0.61 (Jun 15 2008)
+
+### IMPROVEMENTS:
+* improved attacking in the battles
+* it's possible to kill hostile stack
+* animations won't go in the same phase
+* Better pathfinder
+* "%s" substitutions in Right-click information in town hall
+* windmill won't give wood
+* hover text for heroes
+* support for ZSoft-style PCX files in /Data
+* Splitting: when moving slider to the right so that 0 is left in old slot the army is moved
+* in the townlist in castle selected town will by placed on the 2nd place (not 3rd)
+* stack at the limit of unit's range can now be attacked
+* range of unit is now properly displayed
+* battle log is scrolled down when new event occurs
+* console is closed when application exits
+
+### BUGFIXES:
+* stack at the limit of unit's range can now be attacked
+* good background for the town hall screen in Stronghold
+* fixed typo in hall.txt
+* VCMI won't crash when r-click neutral stack during the battle
+* water won't blink behind shipyard in the Castle
+* fixed several memory leaks
+* properly displaying two-hex creatures in recruit/split/info window
+* corrupted map file won't cause crash on initializing main menu
+
+# 0.59 -> 0.6 (Jun 1 2008)
+
+* partially done attacking in battles
+* screen isn't now refreshed while blitting creature info window
+* r-click creature info windows in battles
+* no more division by 0 in slider
+* "plural" reference names for Conflux creatures (starting armies of Conflux heroes should now be working)
+* fixed estate problems
+* fixed blinking mana vortex
+* grail increases creature growths
+* new pathfinder
+* several minor improvements
+
+# 0.58 -> 0.59 (May 24 2008 - closed, test release)
+
+* fixed memory leak in battles
+* blitting creature animations to rects in the recruitment window
+* fixed wrong creatures def names
+* better battle pathfinder and unit reversing
+* improved slider ( #58 )
+* fixed problems with horde buildings (won't block original dwellings)
+* giving primary skill when hero get level (but there is still no dialog)
+* if an upgraded creature is available it'll be shown as the first in a recruitment window
+* creature levels not messed in Fortress
+* war machines are added to the hero's inventory, not to the garrison
+* support for H3-style PCX graphics in Data/
+* VCMI won't crash when is unable to initialize audio system
+* fixed displaying wrong town defs
+* improvements in recruitment window (slider won't allow to select more creatures than we can afford)
+* creature info window (only r-click)
+* callback for buttons/lists based on boost::function
+* a lot of minor improvements
+
+# 0.55 -> 0.58 (Apr 20 2008 - closed, test release)
+
+### TOWNS:
+* recruiting creatures
+* working creature growths (including castle and horde building influences)
+* towns give income
+* town hall screen
+* building buildings (requirements and cost are handled)
+* hints for structures
+* updating town infobox
+
+### GARRISONS:
+* merging stacks
+* splitting stacks
+
+### BATTLES:
+* starting battles
+* displaying terrain, animations of heroes, units, grid, range of units, battle menu with console, amounts of units in stacks
+* leaving battle by pressing flee button
+* moving units in battles and displaying their ranges
+* defend command for units
+
+### GENERAL:
+* a number of minor fixes and improvements
+
+# 0.54 -> 0.55 (Feb 29 2008)
+
+* Sprites/ folder works for h3sprite.lod same as Data/ for h3bitmap.lod (but it's still experimental)
+* randomization quantity of creatures on the map
+* fix of Pandora's Box handling
+* reading disposed/predefined heroes
+* new command - "get txt" - VCMI will extract all .txt files from h3bitmap.lod to the Extracted_txts/ folder.
+* more detailed logs
+* reported problems with hero flags resolved
+* heroes cannot occupy the same tile
+* hints for most of creature generators
+* some minor stuff
+
+# 0.53b -> 0.54 (Feb 23 2008 - first public release)
+* given hero is placed in the town entrance
+* some objects such as river delta won't be blitted "on" hero
+* tiles under FoW are inaccessible
+* giving random hero on RoE maps
+* improved protection against hero duplication
+* fixed starting values of primary abilities of random heroes on RoE/AB maps
+* right click popups with infoboxes for heroes/towns lists
+* new interface coloring (many thanks to GrayFace ;])
+* fixed bug in object flag's coloring
+* added hints in town lists
+* eliminated square from city hints
+
+# 0.53 - 0.53b (Feb 20 2008)
+
+* added giving default buildings in towns
+* town infobox won't crash on empty town
+
+# 0.52 - 0.53 (Feb 18 2008):
+
+* hopefully the last bugfix of Pandora's Box
+* fixed blockmaps of generated heroes
+* disposed hero cannot be chosen in scenario settings (unless he is in prison)
+* fixed town randomization
+* fixed hero randomization
+* fixed displaying heroes in preGame
+* fixed selecting/deselecting artifact slots in hero window
+* much faster pathfinder
+* memory usage and load time significantly decreased
+* it's impossible to select empty artifact slot in hero window
+* fixed problem with FoW displaying on minimap on L-sized maps
+* fixed crashbug in hero list connected with heroes dismissing
+* mostly done town infobox
+* town daily income is properly calculated
+
+# 0.51 - 0.52 (Feb 7 2008):
+
+* [feature] giving starting hero
+* [feature] VCMI will try to use files from /Data folder instead of those from h3bitmap.lod
+* [feature] picked artifacts are added to hero's backpack
+* [feature] possibility of choosing player to play
+* [bugfix] ZELP.TXT file *should* be handled correctly even it is non-english
+* [bugfix] fixed crashbug in reading defs with negativ left/right margins
+* [bugfix] improved randomization
+* [bugfix] pathfinder can't be cheated (what caused errors)
+
+# 0.5 - 0.51 (Feb 3 2008):
+
+* close button properly closes (same does 'q' key)
+* two players can't have selected same hero
+* double click on "Show Available Scenarios" won't reset options
+* fixed possible crashbug in town/hero lists
+* fixed crashbug in initializing game caused by wrong prisons handling
+* fixed crashbug on reading hero's custom artifacts in RoE maps
+* fixed crashbug on reading custom Pandora's Box in RoE maps
+* fixed crashbug on reading blank Quest Guards
+* better console messages
+* map reading speed up (though it's still slow, especially on bigger maps)
+
+# 0.0 -> 0.5 (Feb 2 2008 - first closed release):
+
+* Main menu and New game screens
+* Scenario selection, part of advanced options support
+* Partially done adventure map, town and hero interfaces
+* Moving hero
+* Interactions with several objects (mines, resources, mills, and others)

+ 692 - 692
Global.h

@@ -1,692 +1,692 @@
-/*
- * Global.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
-
-/* ---------------------------------------------------------------------------- */
-/* Compiler detection */
-/* ---------------------------------------------------------------------------- */
-// Fixed width bool data type is important for serialization
-static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
-
-/* ---------------------------------------------------------------------------- */
-/* System detection. */
-/* ---------------------------------------------------------------------------- */
-// Based on: http://sourceforge.net/p/predef/wiki/OperatingSystems/
-//	 and on: http://stackoverflow.com/questions/5919996/how-to-detect-reliably-mac-os-x-ios-linux-windows-in-c-preprocessor
-// TODO?: Should be moved to vstd\os_detect.h (and then included by Global.h)
-#ifdef _WIN16			// Defined for 16-bit environments
-#  error "16-bit Windows isn't supported"
-#elif defined(_WIN64)	// Defined for 64-bit environments
-#  define VCMI_WINDOWS
-#  define VCMI_WINDOWS_64
-#elif defined(_WIN32)	// Defined for both 32-bit and 64-bit environments
-#  define VCMI_WINDOWS
-#  define VCMI_WINDOWS_32
-#elif defined(_WIN32_WCE)
-#  error "Windows CE isn't supported"
-#elif defined(__linux__) || defined(__gnu_linux__) || defined(linux) || defined(__linux)
-#  define VCMI_UNIX
-#  define VCMI_XDG
-#  if defined(__ANDROID__) || defined(ANDROID)
-#    define VCMI_ANDROID
-#  endif
-#elif defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
-#  define VCMI_UNIX
-#  define VCMI_XDG
-#  define VCMI_FREEBSD
-#elif defined(__HAIKU__)
-#  define VCMI_UNIX
-#  define VCMI_XDG
-#  define VCMI_HAIKU
-#elif defined(__GNU__) || defined(__gnu_hurd__) || (defined(__MACH__) && !defined(__APPLE__))
-#  define VCMI_UNIX
-#  define VCMI_XDG
-#  define VCMI_HURD
-#elif defined(__APPLE__) && defined(__MACH__)
-#  define VCMI_UNIX
-#  define VCMI_APPLE
-#  include "TargetConditionals.h"
-#  if TARGET_OS_SIMULATOR || TARGET_IPHONE_SIMULATOR
-#    define VCMI_IOS
-#    define VCMI_IOS_SIM
-#  elif TARGET_OS_IPHONE
-#    define VCMI_IOS
-#  elif TARGET_OS_MAC
-#    define VCMI_MAC
-#  else
-//#  warning "Unknown Apple target."?
-#  endif
-#else
-#  error "This platform isn't supported"
-#endif
-
-#if defined(VCMI_ANDROID) || defined(VCMI_IOS)
-#define VCMI_MOBILE
-#endif
-
-/* ---------------------------------------------------------------------------- */
-/* Commonly used C++, Boost headers */
-/* ---------------------------------------------------------------------------- */
-#ifdef VCMI_WINDOWS
-#  ifndef WIN32_LEAN_AND_MEAN
-#    define WIN32_LEAN_AND_MEAN		 // Exclude rarely-used stuff from Windows headers - delete this line if something is missing.
-#  endif
-#  ifndef NOMINMAX
-#    define NOMINMAX				 // Exclude min/max macros from <Windows.h>. Use std::[min/max] from <algorithm> instead.
-#  endif
-#  ifndef _NO_W32_PSEUDO_MODIFIERS
-#    define _NO_W32_PSEUDO_MODIFIERS // Exclude more macros for compiling with MinGW on Linux.
-#  endif
-#endif
-
-/* ---------------------------------------------------------------------------- */
-/* A macro to force inlining some of our functions */
-/* ---------------------------------------------------------------------------- */
-// Compiler (at least MSVC) is not so smart here-> without that displaying is MUCH slower
-#ifdef _MSC_VER
-#  define STRONG_INLINE __forceinline
-#elif __GNUC__
-#  define STRONG_INLINE inline __attribute__((always_inline))
-#else
-#  define STRONG_INLINE inline
-#endif
-
-#define _USE_MATH_DEFINES
-
-#include <algorithm>
-#include <any>
-#include <array>
-#include <atomic>
-#include <bitset>
-#include <cassert>
-#include <climits>
-#include <cmath>
-#include <codecvt>
-#include <cstdlib>
-#include <cstdio>
-#include <fstream>
-#include <functional>
-#include <iomanip>
-#include <iostream>
-#include <map>
-#include <memory>
-#include <mutex>
-#include <numeric>
-#include <optional>
-#include <queue>
-#include <random>
-#include <regex>
-#include <set>
-#include <sstream>
-#include <string>
-#include <unordered_map>
-#include <unordered_set>
-#include <utility>
-#include <variant>
-#include <vector>
-
-//The only available version is 3, as of Boost 1.50
-#include <boost/version.hpp>
-
-#define BOOST_FILESYSTEM_VERSION 3
-#if BOOST_VERSION > 105000
-#  define BOOST_THREAD_VERSION 3
-#endif
-#define BOOST_THREAD_DONT_PROVIDE_THREAD_DESTRUCTOR_CALLS_TERMINATE_IF_JOINABLE 1
-//need to link boost thread dynamically to avoid https://stackoverflow.com/questions/35978572/boost-thread-interupt-does-not-work-when-crossing-a-dll-boundary
-#define BOOST_THREAD_USE_DLL //for example VCAI::finish() may freeze on thread join after interrupt when linking this statically
-#define BOOST_BIND_NO_PLACEHOLDERS
-
-#if BOOST_VERSION >= 106600
-#define BOOST_ASIO_ENABLE_OLD_SERVICES
-#endif
-
-#include <boost/algorithm/hex.hpp>
-#include <boost/algorithm/string.hpp>
-#include <boost/crc.hpp>
-#include <boost/current_function.hpp>
-#include <boost/date_time/posix_time/posix_time_types.hpp>
-#include <boost/date_time/posix_time/time_formatters.hpp>
-#include <boost/filesystem.hpp>
-#include <boost/filesystem/fstream.hpp>
-#include <boost/filesystem/path.hpp>
-#include <boost/format.hpp>
-#include <boost/functional/hash.hpp>
-#include <boost/lexical_cast.hpp>
-#ifdef VCMI_WINDOWS
-#include <boost/locale/generator.hpp>
-#endif
-#include <boost/logic/tribool.hpp>
-#include <boost/multi_array.hpp>
-#include <boost/range/adaptor/filtered.hpp>
-#include <boost/range/adaptor/reversed.hpp>
-#include <boost/range/algorithm.hpp>
-#include <boost/thread/thread_only.hpp>
-#include <boost/thread/shared_mutex.hpp>
-#include <boost/thread/recursive_mutex.hpp>
-#include <boost/thread/once.hpp>
-
-#ifndef M_PI
-#  define M_PI 3.14159265358979323846
-#endif
-
-/* ---------------------------------------------------------------------------- */
-/* Usings */
-/* ---------------------------------------------------------------------------- */
-using namespace std::placeholders;
-namespace range = boost::range;
-
-/* ---------------------------------------------------------------------------- */
-/* Typedefs */
-/* ---------------------------------------------------------------------------- */
-// Integral data types
-typedef uint64_t ui64; //unsigned int 64 bits (8 bytes)
-typedef uint32_t ui32;  //unsigned int 32 bits (4 bytes)
-typedef uint16_t ui16; //unsigned int 16 bits (2 bytes)
-typedef uint8_t ui8; //unsigned int 8 bits (1 byte)
-typedef int64_t si64; //signed int 64 bits (8 bytes)
-typedef int32_t si32; //signed int 32 bits (4 bytes)
-typedef int16_t si16; //signed int 16 bits (2 bytes)
-typedef int8_t si8; //signed int 8 bits (1 byte)
-
-// Lock typedefs
-using TLockGuard = std::lock_guard<std::mutex>;
-using TLockGuardRec = std::lock_guard<std::recursive_mutex>;
-
-/* ---------------------------------------------------------------------------- */
-/* Macros */
-/* ---------------------------------------------------------------------------- */
-// Import + Export macro declarations
-#ifdef VCMI_WINDOWS
-#ifdef VCMI_DLL_STATIC
-#    define DLL_IMPORT
-#    define DLL_EXPORT
-#elif defined(__GNUC__)
-#    define DLL_IMPORT __attribute__((dllimport))
-#    define DLL_EXPORT __attribute__((dllexport))
-#  else
-#    define DLL_IMPORT __declspec(dllimport)
-#    define DLL_EXPORT __declspec(dllexport)
-#  endif
-#  define ELF_VISIBILITY
-#else
-#  ifdef __GNUC__
-#    define DLL_IMPORT	__attribute__ ((visibility("default")))
-#    define DLL_EXPORT __attribute__ ((visibility("default")))
-#    define ELF_VISIBILITY __attribute__ ((visibility("default")))
-#  endif
-#endif
-
-#ifdef VCMI_DLL
-#  define DLL_LINKAGE DLL_EXPORT
-#else
-#  define DLL_LINKAGE DLL_IMPORT
-#endif
-
-#define THROW_FORMAT(message, formatting_elems)  throw std::runtime_error(boost::str(boost::format(message) % formatting_elems))
-
-// old iOS SDKs compatibility
-#ifdef VCMI_IOS
-#include <AvailabilityVersions.h>
-
-#ifndef __IPHONE_13_0
-#define __IPHONE_13_0 130000
-#endif
-#endif // VCMI_IOS
-
-// single-process build makes 2 copies of the main lib by wrapping it in a namespace
-#ifdef VCMI_LIB_NAMESPACE
-#define VCMI_LIB_NAMESPACE_BEGIN namespace VCMI_LIB_NAMESPACE {
-#define VCMI_LIB_NAMESPACE_END }
-#define VCMI_LIB_USING_NAMESPACE using namespace VCMI_LIB_NAMESPACE;
-#define VCMI_LIB_WRAP_NAMESPACE(x) VCMI_LIB_NAMESPACE::x
-#else
-#define VCMI_LIB_NAMESPACE_BEGIN
-#define VCMI_LIB_NAMESPACE_END
-#define VCMI_LIB_USING_NAMESPACE
-#define VCMI_LIB_WRAP_NAMESPACE(x) ::x
-#endif
-
-/* ---------------------------------------------------------------------------- */
-/* VCMI standard library */
-/* ---------------------------------------------------------------------------- */
-#include <vstd/CLoggerBase.h>
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-namespace vstd
-{
-	// combine hashes. Present in boost but not in std
-	template <class T>
-	inline void hash_combine(std::size_t& seed, const T& v)
-	{
-		std::hash<T> hasher;
-		seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);
-	}
-
-	//returns true if container c contains item i
-	template <typename Container, typename Item>
-	bool contains(const Container & c, const Item &i)
-	{
-		return std::find(std::begin(c), std::end(c),i) != std::end(c);
-	}
-
-	//returns true if container c contains item i
-	template <typename Container, typename Pred>
-	bool contains_if(const Container & c, Pred p)
-	{
-		return std::find_if(std::begin(c), std::end(c), p) != std::end(c);
-	}
-
-	//returns true if map c contains item i
-	template <typename V, typename Item, typename Item2>
-	bool contains(const std::map<Item,V> & c, const Item2 &i)
-	{
-		return c.find(i)!=c.end();
-	}
-
-	//returns true if unordered set c contains item i
-	template <typename Item>
-	bool contains(const std::unordered_set<Item> & c, const Item &i)
-	{
-		return c.find(i)!=c.end();
-	}
-
-	template <typename V, typename Item, typename Item2>
-	bool contains(const std::unordered_map<Item,V> & c, const Item2 &i)
-	{
-		return c.find(i)!=c.end();
-	}
-
-	//returns position of first element in vector c equal to s, if there is no such element, -1 is returned
-	template <typename Container, typename T2>
-	int find_pos(const Container & c, const T2 &s)
-	{
-		int i=0;
-		for (auto iter = std::begin(c); iter != std::end(c); iter++, i++)
-			if(*iter == s)
-				return i;
-		return -1;
-	}
-
-	//Func f tells if element matches
-	template <typename Container, typename Func>
-	int find_pos_if(const Container & c, const Func &f)
-	{
-		auto ret = boost::range::find_if(c, f);
-		if(ret != std::end(c))
-			return std::distance(std::begin(c), ret);
-
-		return -1;
-	}
-
-	//returns iterator to the given element if present in container, end() if not
-	template <typename Container, typename Item>
-	typename Container::iterator find(Container & c, const Item &i)
-	{
-		return std::find(c.begin(),c.end(),i);
-	}
-
-	//returns const iterator to the given element if present in container, end() if not
-	template <typename Container, typename Item>
-	typename Container::const_iterator find(const Container & c, const Item &i)
-	{
-		return std::find(c.begin(),c.end(),i);
-	}
-
-	//returns first key that maps to given value if present, returns success via found if provided
-	template <typename Key, typename T>
-	Key findKey(const std::map<Key, T> & map, const T & value, bool * found = nullptr)
-	{
-		for(auto iter = map.cbegin(); iter != map.cend(); iter++)
-		{
-			if(iter->second == value)
-			{
-				if(found)
-					*found = true;
-				return iter->first;
-			}
-		}
-		if(found)
-			*found = false;
-		return Key();
-	}
-
-	//removes element i from container c, returns false if c does not contain i
-	template <typename Container, typename Item>
-	typename Container::size_type operator-=(Container &c, const Item &i)
-	{
-		typename Container::iterator itr = find(c,i);
-		if(itr == c.end())
-			return false;
-		c.erase(itr);
-		return true;
-	}
-
-	//assigns greater of (a, b) to a and returns maximum of (a, b)
-	template <typename t1, typename t2>
-	t1 &amax(t1 &a, const t2 &b)
-	{
-		if(a >= (t1)b)
-			return a;
-		else
-		{
-			a = t1(b);
-			return a;
-		}
-	}
-
-	//assigns smaller of (a, b) to a and returns minimum of (a, b)
-	template <typename t1, typename t2>
-	t1 &amin(t1 &a, const t2 &b)
-	{
-		if(a <= (t1)b)
-			return a;
-		else
-		{
-			a = t1(b);
-			return a;
-		}
-	}
-
-	//makes a to fit the range <b, c>
-	template <typename T>
-	void abetween(T &value, const T &min, const T &max)
-	{
-		value = std::clamp(value, min, max);
-	}
-
-	//checks if a is between b and c
-	template <typename t1, typename t2, typename t3>
-	bool isbetween(const t1 &value, const t2 &min, const t3 &max)
-	{
-		return value > (t1)min && value < (t1)max;
-	}
-
-	//checks if a is within b and c
-	template <typename t1, typename t2, typename t3>
-	bool iswithin(const t1 &value, const t2 &min, const t3 &max)
-	{
-		return value >= (t1)min && value <= (t1)max;
-	}
-
-	template <typename t1, typename t2>
-	struct assigner
-	{
-	public:
-		t1 &op1;
-		t2 op2;
-		assigner(t1 &a1, const t2 & a2)
-			:op1(a1), op2(a2)
-		{}
-		void operator()()
-		{
-			op1 = op2;
-		}
-	};
-
-	//deleted pointer and sets it to nullptr
-	template <typename T>
-	void clear_pointer(T* &ptr)
-	{
-		delete ptr;
-		ptr = nullptr;
-	}
-
-	template <typename Container>
-	typename Container::const_reference circularAt(const Container &r, size_t index)
-	{
-		assert(r.size());
-		index %= r.size();
-		auto itr = std::begin(r);
-		std::advance(itr, index);
-		return *itr;
-	}
-
-	template <typename Container, typename Item>
-	void erase(Container &c, const Item &item)
-	{
-		c.erase(boost::remove(c, item), c.end());
-	}
-
-	template<typename Range, typename Predicate>
-	void erase_if(Range &vec, Predicate pred)
-	{
-		vec.erase(boost::remove_if(vec, pred),vec.end());
-	}
-
-	template<typename Elem, typename Predicate>
-	void erase_if(std::set<Elem> &setContainer, Predicate pred)
-	{
-		auto itr = setContainer.begin();
-		auto endItr = setContainer.end();
-		while(itr != endItr)
-		{
-			auto tmpItr = itr++;
-			if(pred(*tmpItr))
-				setContainer.erase(tmpItr);
-		}
-	}
-
-	template<typename Elem, typename Predicate>
-	void erase_if(std::unordered_set<Elem> &setContainer, Predicate pred)
-	{
-		auto itr = setContainer.begin();
-		auto endItr = setContainer.end();
-		while(itr != endItr)
-		{
-			auto tmpItr = itr++;
-			if(pred(*tmpItr))
-				setContainer.erase(tmpItr);
-		}
-	}
-
-	//works for map and std::map, maybe something else
-	template<typename Key, typename Val, typename Predicate>
-	void erase_if(std::map<Key, Val> &container, Predicate pred)
-	{
-		auto itr = container.begin();
-		auto endItr = container.end();
-		while(itr != endItr)
-		{
-			auto tmpItr = itr++;
-			if(pred(*tmpItr))
-				container.erase(tmpItr);
-		}
-	}
-
-	template<typename InputRange, typename OutputIterator, typename Predicate>
-	OutputIterator copy_if(const InputRange &input, OutputIterator result, Predicate pred)
-	{
-		return std::copy_if(std::cbegin(input), std::end(input), result, pred);
-	}
-
-	template <typename Container>
-	std::insert_iterator<Container> set_inserter(Container &c)
-	{
-		return std::inserter(c, c.end());
-	}
-
-	//Returns iterator to the element for which the value of ValueFunction is minimal
-	template<class ForwardRange, class ValueFunction>
-	auto minElementByFun(const ForwardRange& rng, ValueFunction vf) -> decltype(std::begin(rng))
-	{
-		/* Clang crashes when instantiating this function template and having PCH compilation enabled.
-		 * There is a bug report here: http://llvm.org/bugs/show_bug.cgi?id=18744
-		 * Current bugfix is to don't use a typedef for decltype(*std::begin(rng)) and to use decltype
-		 * directly for both function parameters.
-		 */
-		return boost::min_element(rng, [&] (decltype(*std::begin(rng)) lhs, decltype(*std::begin(rng)) rhs) -> bool
-		{
-			return vf(lhs) < vf(rhs);
-		});
-	}
-
-	//Returns iterator to the element for which the value of ValueFunction is maximal
-	template<class ForwardRange, class ValueFunction>
-	auto maxElementByFun(const ForwardRange& rng, ValueFunction vf) -> decltype(std::begin(rng))
-	{
-		/* Clang crashes when instantiating this function template and having PCH compilation enabled.
-		 * There is a bug report here: http://llvm.org/bugs/show_bug.cgi?id=18744
-		 * Current bugfix is to don't use a typedef for decltype(*std::begin(rng)) and to use decltype
-		 * directly for both function parameters.
-		 */
-		return boost::max_element(rng, [&] (decltype(*std::begin(rng)) lhs, decltype(*std::begin(rng)) rhs) -> bool
-		{
-			return vf(lhs) < vf(rhs);
-		});
-	}
-
-	/// Increments value by specific delta
-	/// similar to std::next but works with other types, e.g. enum class
-	template<typename T>
-	T next(const T &obj, int change)
-	{
-		return static_cast<T>(static_cast<ptrdiff_t>(obj) + change);
-	}
-
-	template <typename Container>
-	typename Container::value_type backOrNull(const Container &c) //returns last element of container or nullptr if it is empty (to be used with containers of pointers)
-	{
-		if(c.size())
-			return c.back();
-		else
-			return typename Container::value_type();
-	}
-
-	template <typename Container>
-	typename Container::value_type frontOrNull(const Container &c) //returns first element of container or nullptr if it is empty (to be used with containers of pointers)
-	{
-		if(c.size())
-			return c.front();
-		else
-			return nullptr;
-	}
-
-	template <typename Container, typename Index>
-	bool isValidIndex(const Container &c, Index i)
-	{
-		return i >= 0  &&  i < c.size();
-	}
-
-	template <typename Container>
-	typename Container::const_reference atOrDefault(const Container &r, size_t index, const typename Container::const_reference &defaultValue)
-	{
-		if(index < r.size())
-			return r[index];
-
-		return defaultValue;
-	}
-
-	template <typename Container, typename Item>
-	bool erase_if_present(Container &c, const Item &item)
-	{
-		auto i = std::find(c.begin(), c.end(), item);
-		if (i != c.end())
-		{
-			c.erase(i);
-			return true;
-		}
-
-		return false;
-	}
-
-	template <typename V, typename Item, typename Item2>
-	bool erase_if_present(std::map<Item,V> & c, const Item2 &item)
-	{
-		auto i = c.find(item);
-		if (i != c.end())
-		{
-			c.erase(i);
-			return true;
-		}
-		return false;
-	}
-
-	template<typename T>
-	void removeDuplicates(std::vector<T> &vec)
-	{
-		std::sort(vec.begin(), vec.end());
-		vec.erase(std::unique(vec.begin(), vec.end()), vec.end());
-	}
-
-	template <typename T>
-	void concatenate(std::vector<T> &dest, const std::vector<T> &src)
-	{
-		dest.reserve(dest.size() + src.size());
-		dest.insert(dest.end(), src.begin(), src.end());
-	}
-
-	template <typename T>
-	std::vector<T> intersection(std::vector<T> &v1, std::vector<T> &v2)
-	{
-		std::vector<T> v3;
-		std::sort(v1.begin(), v1.end());
-		std::sort(v2.begin(), v2.end());
-		std::set_intersection(v1.begin(), v1.end(), v2.begin(), v2.end(), std::back_inserter(v3));
-		return v3;
-	}
-
-	template <typename T>
-	std::set<T> difference(const std::set<T> &s1, const std::set<T> s2)
-	{
-		std::set<T> s3;
-		std::set_difference(s1.begin(), s1.end(), s2.begin(), s2.end(), std::inserter(s3, s3.end()));
-		return s3;
-	}
-
-	template <typename Key, typename V>
-	bool containsMapping(const std::multimap<Key,V> & map, const std::pair<const Key,V> & mapping)
-	{
-		auto range = map.equal_range(mapping.first);
-		for(auto contained = range.first; contained != range.second; contained++)
-		{
-			if(mapping.second == contained->second)
-				return true;
-		}
-		return false;
-	}
-
-	template<class M, class Key, class F>
-	typename M::mapped_type & getOrCompute(M & m, const Key & k, F f)
-	{
-		typedef typename M::mapped_type V;
-
-		std::pair<typename M::iterator, bool> r = m.insert(typename M::value_type(k, V()));
-		V & v = r.first->second;
-
-		if(r.second)
-			f(v);
-
-		return v;
-	}
-
-	//c++20 feature
-	template<typename Arithmetic, typename Floating>
-	Arithmetic lerp(const Arithmetic & a, const Arithmetic & b, const Floating & f)
-	{
-		return a + (b - a) * f;
-	}
-
-	///compile-time version of std::abs for ints for int3, in clang++15 std::abs is constexpr
-	static constexpr int abs(int i) {
-		if(i < 0) return -i;
-		return i;
-	}
-
-	///C++23
-	template< class Enum > constexpr std::underlying_type_t<Enum> to_underlying( Enum e ) noexcept
-	{
-		return static_cast<std::underlying_type_t<Enum>>(e);
-	}
-}
-using vstd::operator-=;
-
-VCMI_LIB_NAMESPACE_END
+/*
+ * Global.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
+
+/* ---------------------------------------------------------------------------- */
+/* Compiler detection */
+/* ---------------------------------------------------------------------------- */
+// Fixed width bool data type is important for serialization
+static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
+
+/* ---------------------------------------------------------------------------- */
+/* System detection. */
+/* ---------------------------------------------------------------------------- */
+// Based on: http://sourceforge.net/p/predef/wiki/OperatingSystems/
+//	 and on: http://stackoverflow.com/questions/5919996/how-to-detect-reliably-mac-os-x-ios-linux-windows-in-c-preprocessor
+// TODO?: Should be moved to vstd\os_detect.h (and then included by Global.h)
+#ifdef _WIN16			// Defined for 16-bit environments
+#  error "16-bit Windows isn't supported"
+#elif defined(_WIN64)	// Defined for 64-bit environments
+#  define VCMI_WINDOWS
+#  define VCMI_WINDOWS_64
+#elif defined(_WIN32)	// Defined for both 32-bit and 64-bit environments
+#  define VCMI_WINDOWS
+#  define VCMI_WINDOWS_32
+#elif defined(_WIN32_WCE)
+#  error "Windows CE isn't supported"
+#elif defined(__linux__) || defined(__gnu_linux__) || defined(linux) || defined(__linux)
+#  define VCMI_UNIX
+#  define VCMI_XDG
+#  if defined(__ANDROID__) || defined(ANDROID)
+#    define VCMI_ANDROID
+#  endif
+#elif defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
+#  define VCMI_UNIX
+#  define VCMI_XDG
+#  define VCMI_FREEBSD
+#elif defined(__HAIKU__)
+#  define VCMI_UNIX
+#  define VCMI_XDG
+#  define VCMI_HAIKU
+#elif defined(__GNU__) || defined(__gnu_hurd__) || (defined(__MACH__) && !defined(__APPLE__))
+#  define VCMI_UNIX
+#  define VCMI_XDG
+#  define VCMI_HURD
+#elif defined(__APPLE__) && defined(__MACH__)
+#  define VCMI_UNIX
+#  define VCMI_APPLE
+#  include "TargetConditionals.h"
+#  if TARGET_OS_SIMULATOR || TARGET_IPHONE_SIMULATOR
+#    define VCMI_IOS
+#    define VCMI_IOS_SIM
+#  elif TARGET_OS_IPHONE
+#    define VCMI_IOS
+#  elif TARGET_OS_MAC
+#    define VCMI_MAC
+#  else
+//#  warning "Unknown Apple target."?
+#  endif
+#else
+#  error "This platform isn't supported"
+#endif
+
+#if defined(VCMI_ANDROID) || defined(VCMI_IOS)
+#define VCMI_MOBILE
+#endif
+
+/* ---------------------------------------------------------------------------- */
+/* Commonly used C++, Boost headers */
+/* ---------------------------------------------------------------------------- */
+#ifdef VCMI_WINDOWS
+#  ifndef WIN32_LEAN_AND_MEAN
+#    define WIN32_LEAN_AND_MEAN		 // Exclude rarely-used stuff from Windows headers - delete this line if something is missing.
+#  endif
+#  ifndef NOMINMAX
+#    define NOMINMAX				 // Exclude min/max macros from <Windows.h>. Use std::[min/max] from <algorithm> instead.
+#  endif
+#  ifndef _NO_W32_PSEUDO_MODIFIERS
+#    define _NO_W32_PSEUDO_MODIFIERS // Exclude more macros for compiling with MinGW on Linux.
+#  endif
+#endif
+
+/* ---------------------------------------------------------------------------- */
+/* A macro to force inlining some of our functions */
+/* ---------------------------------------------------------------------------- */
+// Compiler (at least MSVC) is not so smart here-> without that displaying is MUCH slower
+#ifdef _MSC_VER
+#  define STRONG_INLINE __forceinline
+#elif __GNUC__
+#  define STRONG_INLINE inline __attribute__((always_inline))
+#else
+#  define STRONG_INLINE inline
+#endif
+
+#define _USE_MATH_DEFINES
+
+#include <algorithm>
+#include <any>
+#include <array>
+#include <atomic>
+#include <bitset>
+#include <cassert>
+#include <climits>
+#include <cmath>
+#include <codecvt>
+#include <cstdlib>
+#include <cstdio>
+#include <fstream>
+#include <functional>
+#include <iomanip>
+#include <iostream>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <numeric>
+#include <optional>
+#include <queue>
+#include <random>
+#include <regex>
+#include <set>
+#include <sstream>
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+#include <utility>
+#include <variant>
+#include <vector>
+
+//The only available version is 3, as of Boost 1.50
+#include <boost/version.hpp>
+
+#define BOOST_FILESYSTEM_VERSION 3
+#if BOOST_VERSION > 105000
+#  define BOOST_THREAD_VERSION 3
+#endif
+#define BOOST_THREAD_DONT_PROVIDE_THREAD_DESTRUCTOR_CALLS_TERMINATE_IF_JOINABLE 1
+//need to link boost thread dynamically to avoid https://stackoverflow.com/questions/35978572/boost-thread-interupt-does-not-work-when-crossing-a-dll-boundary
+#define BOOST_THREAD_USE_DLL //for example VCAI::finish() may freeze on thread join after interrupt when linking this statically
+#define BOOST_BIND_NO_PLACEHOLDERS
+
+#if BOOST_VERSION >= 106600
+#define BOOST_ASIO_ENABLE_OLD_SERVICES
+#endif
+
+#include <boost/algorithm/hex.hpp>
+#include <boost/algorithm/string.hpp>
+#include <boost/crc.hpp>
+#include <boost/current_function.hpp>
+#include <boost/date_time/posix_time/posix_time_types.hpp>
+#include <boost/date_time/posix_time/time_formatters.hpp>
+#include <boost/filesystem.hpp>
+#include <boost/filesystem/fstream.hpp>
+#include <boost/filesystem/path.hpp>
+#include <boost/format.hpp>
+#include <boost/functional/hash.hpp>
+#include <boost/lexical_cast.hpp>
+#ifdef VCMI_WINDOWS
+#include <boost/locale/generator.hpp>
+#endif
+#include <boost/logic/tribool.hpp>
+#include <boost/multi_array.hpp>
+#include <boost/range/adaptor/filtered.hpp>
+#include <boost/range/adaptor/reversed.hpp>
+#include <boost/range/algorithm.hpp>
+#include <boost/thread/thread_only.hpp>
+#include <boost/thread/shared_mutex.hpp>
+#include <boost/thread/recursive_mutex.hpp>
+#include <boost/thread/once.hpp>
+
+#ifndef M_PI
+#  define M_PI 3.14159265358979323846
+#endif
+
+/* ---------------------------------------------------------------------------- */
+/* Usings */
+/* ---------------------------------------------------------------------------- */
+using namespace std::placeholders;
+namespace range = boost::range;
+
+/* ---------------------------------------------------------------------------- */
+/* Typedefs */
+/* ---------------------------------------------------------------------------- */
+// Integral data types
+typedef uint64_t ui64; //unsigned int 64 bits (8 bytes)
+typedef uint32_t ui32;  //unsigned int 32 bits (4 bytes)
+typedef uint16_t ui16; //unsigned int 16 bits (2 bytes)
+typedef uint8_t ui8; //unsigned int 8 bits (1 byte)
+typedef int64_t si64; //signed int 64 bits (8 bytes)
+typedef int32_t si32; //signed int 32 bits (4 bytes)
+typedef int16_t si16; //signed int 16 bits (2 bytes)
+typedef int8_t si8; //signed int 8 bits (1 byte)
+
+// Lock typedefs
+using TLockGuard = std::lock_guard<std::mutex>;
+using TLockGuardRec = std::lock_guard<std::recursive_mutex>;
+
+/* ---------------------------------------------------------------------------- */
+/* Macros */
+/* ---------------------------------------------------------------------------- */
+// Import + Export macro declarations
+#ifdef VCMI_WINDOWS
+#ifdef VCMI_DLL_STATIC
+#    define DLL_IMPORT
+#    define DLL_EXPORT
+#elif defined(__GNUC__)
+#    define DLL_IMPORT __attribute__((dllimport))
+#    define DLL_EXPORT __attribute__((dllexport))
+#  else
+#    define DLL_IMPORT __declspec(dllimport)
+#    define DLL_EXPORT __declspec(dllexport)
+#  endif
+#  define ELF_VISIBILITY
+#else
+#  ifdef __GNUC__
+#    define DLL_IMPORT	__attribute__ ((visibility("default")))
+#    define DLL_EXPORT __attribute__ ((visibility("default")))
+#    define ELF_VISIBILITY __attribute__ ((visibility("default")))
+#  endif
+#endif
+
+#ifdef VCMI_DLL
+#  define DLL_LINKAGE DLL_EXPORT
+#else
+#  define DLL_LINKAGE DLL_IMPORT
+#endif
+
+#define THROW_FORMAT(message, formatting_elems)  throw std::runtime_error(boost::str(boost::format(message) % formatting_elems))
+
+// old iOS SDKs compatibility
+#ifdef VCMI_IOS
+#include <AvailabilityVersions.h>
+
+#ifndef __IPHONE_13_0
+#define __IPHONE_13_0 130000
+#endif
+#endif // VCMI_IOS
+
+// single-process build makes 2 copies of the main lib by wrapping it in a namespace
+#ifdef VCMI_LIB_NAMESPACE
+#define VCMI_LIB_NAMESPACE_BEGIN namespace VCMI_LIB_NAMESPACE {
+#define VCMI_LIB_NAMESPACE_END }
+#define VCMI_LIB_USING_NAMESPACE using namespace VCMI_LIB_NAMESPACE;
+#define VCMI_LIB_WRAP_NAMESPACE(x) VCMI_LIB_NAMESPACE::x
+#else
+#define VCMI_LIB_NAMESPACE_BEGIN
+#define VCMI_LIB_NAMESPACE_END
+#define VCMI_LIB_USING_NAMESPACE
+#define VCMI_LIB_WRAP_NAMESPACE(x) ::x
+#endif
+
+/* ---------------------------------------------------------------------------- */
+/* VCMI standard library */
+/* ---------------------------------------------------------------------------- */
+#include <vstd/CLoggerBase.h>
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+namespace vstd
+{
+	// combine hashes. Present in boost but not in std
+	template <class T>
+	inline void hash_combine(std::size_t& seed, const T& v)
+	{
+		std::hash<T> hasher;
+		seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);
+	}
+
+	//returns true if container c contains item i
+	template <typename Container, typename Item>
+	bool contains(const Container & c, const Item &i)
+	{
+		return std::find(std::begin(c), std::end(c),i) != std::end(c);
+	}
+
+	//returns true if container c contains item i
+	template <typename Container, typename Pred>
+	bool contains_if(const Container & c, Pred p)
+	{
+		return std::find_if(std::begin(c), std::end(c), p) != std::end(c);
+	}
+
+	//returns true if map c contains item i
+	template <typename V, typename Item, typename Item2>
+	bool contains(const std::map<Item,V> & c, const Item2 &i)
+	{
+		return c.find(i)!=c.end();
+	}
+
+	//returns true if unordered set c contains item i
+	template <typename Item>
+	bool contains(const std::unordered_set<Item> & c, const Item &i)
+	{
+		return c.find(i)!=c.end();
+	}
+
+	template <typename V, typename Item, typename Item2>
+	bool contains(const std::unordered_map<Item,V> & c, const Item2 &i)
+	{
+		return c.find(i)!=c.end();
+	}
+
+	//returns position of first element in vector c equal to s, if there is no such element, -1 is returned
+	template <typename Container, typename T2>
+	int find_pos(const Container & c, const T2 &s)
+	{
+		int i=0;
+		for (auto iter = std::begin(c); iter != std::end(c); iter++, i++)
+			if(*iter == s)
+				return i;
+		return -1;
+	}
+
+	//Func f tells if element matches
+	template <typename Container, typename Func>
+	int find_pos_if(const Container & c, const Func &f)
+	{
+		auto ret = boost::range::find_if(c, f);
+		if(ret != std::end(c))
+			return std::distance(std::begin(c), ret);
+
+		return -1;
+	}
+
+	//returns iterator to the given element if present in container, end() if not
+	template <typename Container, typename Item>
+	typename Container::iterator find(Container & c, const Item &i)
+	{
+		return std::find(c.begin(),c.end(),i);
+	}
+
+	//returns const iterator to the given element if present in container, end() if not
+	template <typename Container, typename Item>
+	typename Container::const_iterator find(const Container & c, const Item &i)
+	{
+		return std::find(c.begin(),c.end(),i);
+	}
+
+	//returns first key that maps to given value if present, returns success via found if provided
+	template <typename Key, typename T>
+	Key findKey(const std::map<Key, T> & map, const T & value, bool * found = nullptr)
+	{
+		for(auto iter = map.cbegin(); iter != map.cend(); iter++)
+		{
+			if(iter->second == value)
+			{
+				if(found)
+					*found = true;
+				return iter->first;
+			}
+		}
+		if(found)
+			*found = false;
+		return Key();
+	}
+
+	//removes element i from container c, returns false if c does not contain i
+	template <typename Container, typename Item>
+	typename Container::size_type operator-=(Container &c, const Item &i)
+	{
+		typename Container::iterator itr = find(c,i);
+		if(itr == c.end())
+			return false;
+		c.erase(itr);
+		return true;
+	}
+
+	//assigns greater of (a, b) to a and returns maximum of (a, b)
+	template <typename t1, typename t2>
+	t1 &amax(t1 &a, const t2 &b)
+	{
+		if(a >= (t1)b)
+			return a;
+		else
+		{
+			a = t1(b);
+			return a;
+		}
+	}
+
+	//assigns smaller of (a, b) to a and returns minimum of (a, b)
+	template <typename t1, typename t2>
+	t1 &amin(t1 &a, const t2 &b)
+	{
+		if(a <= (t1)b)
+			return a;
+		else
+		{
+			a = t1(b);
+			return a;
+		}
+	}
+
+	//makes a to fit the range <b, c>
+	template <typename T>
+	void abetween(T &value, const T &min, const T &max)
+	{
+		value = std::clamp(value, min, max);
+	}
+
+	//checks if a is between b and c
+	template <typename t1, typename t2, typename t3>
+	bool isbetween(const t1 &value, const t2 &min, const t3 &max)
+	{
+		return value > (t1)min && value < (t1)max;
+	}
+
+	//checks if a is within b and c
+	template <typename t1, typename t2, typename t3>
+	bool iswithin(const t1 &value, const t2 &min, const t3 &max)
+	{
+		return value >= (t1)min && value <= (t1)max;
+	}
+
+	template <typename t1, typename t2>
+	struct assigner
+	{
+	public:
+		t1 &op1;
+		t2 op2;
+		assigner(t1 &a1, const t2 & a2)
+			:op1(a1), op2(a2)
+		{}
+		void operator()()
+		{
+			op1 = op2;
+		}
+	};
+
+	//deleted pointer and sets it to nullptr
+	template <typename T>
+	void clear_pointer(T* &ptr)
+	{
+		delete ptr;
+		ptr = nullptr;
+	}
+
+	template <typename Container>
+	typename Container::const_reference circularAt(const Container &r, size_t index)
+	{
+		assert(r.size());
+		index %= r.size();
+		auto itr = std::begin(r);
+		std::advance(itr, index);
+		return *itr;
+	}
+
+	template <typename Container, typename Item>
+	void erase(Container &c, const Item &item)
+	{
+		c.erase(boost::remove(c, item), c.end());
+	}
+
+	template<typename Range, typename Predicate>
+	void erase_if(Range &vec, Predicate pred)
+	{
+		vec.erase(boost::remove_if(vec, pred),vec.end());
+	}
+
+	template<typename Elem, typename Predicate>
+	void erase_if(std::set<Elem> &setContainer, Predicate pred)
+	{
+		auto itr = setContainer.begin();
+		auto endItr = setContainer.end();
+		while(itr != endItr)
+		{
+			auto tmpItr = itr++;
+			if(pred(*tmpItr))
+				setContainer.erase(tmpItr);
+		}
+	}
+
+	template<typename Elem, typename Predicate>
+	void erase_if(std::unordered_set<Elem> &setContainer, Predicate pred)
+	{
+		auto itr = setContainer.begin();
+		auto endItr = setContainer.end();
+		while(itr != endItr)
+		{
+			auto tmpItr = itr++;
+			if(pred(*tmpItr))
+				setContainer.erase(tmpItr);
+		}
+	}
+
+	//works for map and std::map, maybe something else
+	template<typename Key, typename Val, typename Predicate>
+	void erase_if(std::map<Key, Val> &container, Predicate pred)
+	{
+		auto itr = container.begin();
+		auto endItr = container.end();
+		while(itr != endItr)
+		{
+			auto tmpItr = itr++;
+			if(pred(*tmpItr))
+				container.erase(tmpItr);
+		}
+	}
+
+	template<typename InputRange, typename OutputIterator, typename Predicate>
+	OutputIterator copy_if(const InputRange &input, OutputIterator result, Predicate pred)
+	{
+		return std::copy_if(std::cbegin(input), std::end(input), result, pred);
+	}
+
+	template <typename Container>
+	std::insert_iterator<Container> set_inserter(Container &c)
+	{
+		return std::inserter(c, c.end());
+	}
+
+	//Returns iterator to the element for which the value of ValueFunction is minimal
+	template<class ForwardRange, class ValueFunction>
+	auto minElementByFun(const ForwardRange& rng, ValueFunction vf) -> decltype(std::begin(rng))
+	{
+		/* Clang crashes when instantiating this function template and having PCH compilation enabled.
+		 * There is a bug report here: http://llvm.org/bugs/show_bug.cgi?id=18744
+		 * Current bugfix is to don't use a typedef for decltype(*std::begin(rng)) and to use decltype
+		 * directly for both function parameters.
+		 */
+		return boost::min_element(rng, [&] (decltype(*std::begin(rng)) lhs, decltype(*std::begin(rng)) rhs) -> bool
+		{
+			return vf(lhs) < vf(rhs);
+		});
+	}
+
+	//Returns iterator to the element for which the value of ValueFunction is maximal
+	template<class ForwardRange, class ValueFunction>
+	auto maxElementByFun(const ForwardRange& rng, ValueFunction vf) -> decltype(std::begin(rng))
+	{
+		/* Clang crashes when instantiating this function template and having PCH compilation enabled.
+		 * There is a bug report here: http://llvm.org/bugs/show_bug.cgi?id=18744
+		 * Current bugfix is to don't use a typedef for decltype(*std::begin(rng)) and to use decltype
+		 * directly for both function parameters.
+		 */
+		return boost::max_element(rng, [&] (decltype(*std::begin(rng)) lhs, decltype(*std::begin(rng)) rhs) -> bool
+		{
+			return vf(lhs) < vf(rhs);
+		});
+	}
+
+	/// Increments value by specific delta
+	/// similar to std::next but works with other types, e.g. enum class
+	template<typename T>
+	T next(const T &obj, int change)
+	{
+		return static_cast<T>(static_cast<ptrdiff_t>(obj) + change);
+	}
+
+	template <typename Container>
+	typename Container::value_type backOrNull(const Container &c) //returns last element of container or nullptr if it is empty (to be used with containers of pointers)
+	{
+		if(c.size())
+			return c.back();
+		else
+			return typename Container::value_type();
+	}
+
+	template <typename Container>
+	typename Container::value_type frontOrNull(const Container &c) //returns first element of container or nullptr if it is empty (to be used with containers of pointers)
+	{
+		if(c.size())
+			return c.front();
+		else
+			return nullptr;
+	}
+
+	template <typename Container, typename Index>
+	bool isValidIndex(const Container &c, Index i)
+	{
+		return i >= 0  &&  i < c.size();
+	}
+
+	template <typename Container>
+	typename Container::const_reference atOrDefault(const Container &r, size_t index, const typename Container::const_reference &defaultValue)
+	{
+		if(index < r.size())
+			return r[index];
+
+		return defaultValue;
+	}
+
+	template <typename Container, typename Item>
+	bool erase_if_present(Container &c, const Item &item)
+	{
+		auto i = std::find(c.begin(), c.end(), item);
+		if (i != c.end())
+		{
+			c.erase(i);
+			return true;
+		}
+
+		return false;
+	}
+
+	template <typename V, typename Item, typename Item2>
+	bool erase_if_present(std::map<Item,V> & c, const Item2 &item)
+	{
+		auto i = c.find(item);
+		if (i != c.end())
+		{
+			c.erase(i);
+			return true;
+		}
+		return false;
+	}
+
+	template<typename T>
+	void removeDuplicates(std::vector<T> &vec)
+	{
+		std::sort(vec.begin(), vec.end());
+		vec.erase(std::unique(vec.begin(), vec.end()), vec.end());
+	}
+
+	template <typename T>
+	void concatenate(std::vector<T> &dest, const std::vector<T> &src)
+	{
+		dest.reserve(dest.size() + src.size());
+		dest.insert(dest.end(), src.begin(), src.end());
+	}
+
+	template <typename T>
+	std::vector<T> intersection(std::vector<T> &v1, std::vector<T> &v2)
+	{
+		std::vector<T> v3;
+		std::sort(v1.begin(), v1.end());
+		std::sort(v2.begin(), v2.end());
+		std::set_intersection(v1.begin(), v1.end(), v2.begin(), v2.end(), std::back_inserter(v3));
+		return v3;
+	}
+
+	template <typename T>
+	std::set<T> difference(const std::set<T> &s1, const std::set<T> s2)
+	{
+		std::set<T> s3;
+		std::set_difference(s1.begin(), s1.end(), s2.begin(), s2.end(), std::inserter(s3, s3.end()));
+		return s3;
+	}
+
+	template <typename Key, typename V>
+	bool containsMapping(const std::multimap<Key,V> & map, const std::pair<const Key,V> & mapping)
+	{
+		auto range = map.equal_range(mapping.first);
+		for(auto contained = range.first; contained != range.second; contained++)
+		{
+			if(mapping.second == contained->second)
+				return true;
+		}
+		return false;
+	}
+
+	template<class M, class Key, class F>
+	typename M::mapped_type & getOrCompute(M & m, const Key & k, F f)
+	{
+		typedef typename M::mapped_type V;
+
+		std::pair<typename M::iterator, bool> r = m.insert(typename M::value_type(k, V()));
+		V & v = r.first->second;
+
+		if(r.second)
+			f(v);
+
+		return v;
+	}
+
+	//c++20 feature
+	template<typename Arithmetic, typename Floating>
+	Arithmetic lerp(const Arithmetic & a, const Arithmetic & b, const Floating & f)
+	{
+		return a + (b - a) * f;
+	}
+
+	///compile-time version of std::abs for ints for int3, in clang++15 std::abs is constexpr
+	static constexpr int abs(int i) {
+		if(i < 0) return -i;
+		return i;
+	}
+
+	///C++23
+	template< class Enum > constexpr std::underlying_type_t<Enum> to_underlying( Enum e ) noexcept
+	{
+		return static_cast<std::underlying_type_t<Enum>>(e);
+	}
+}
+using vstd::operator-=;
+
+VCMI_LIB_NAMESPACE_END

+ 39 - 39
Mods/vcmi/Data/s/std.verm

@@ -1,40 +1,40 @@
-VERM
-; standard verm file, global engine things should be put here
-
-!?PI;
-; example 1 --- Hello World
-![print ^Hello world!^]
-
-; example 2 --- simple arithmetics
-![defun add [x y] [+ x y]]
-![print [add 2 3]]
-
-; example 3 --- semantic macros
-![defmacro do-n-times [times body]
-	`[progn
-		[setq do-counter 0]
-		[setq do-max ,times]
-		[do [< do-counter do-max]
-			[progn 
-				[setq do-counter [+ do-counter 1]]
-				,body
-			]
-		]
-	]
-]
-![do-n-times 4 [print ^tekst\n^]]
-
-
-; example 4 --- conditional expression
-![if [> 2 1] [print ^Wieksze^] [print ^Mniejsze^]]
-
-; example 5 --- lambda expressions
-![[lambda [x y] [if [> x y] [print ^wieksze^] [print ^mniejsze^]]] 2 3]
-
-; example 6 --- resursion
-![defun factorial [n]
-	[if [= n 0] 1
-		[* n [factorial [- n 1]]]
-	]
-]
+VERM
+; standard verm file, global engine things should be put here
+
+!?PI;
+; example 1 --- Hello World
+![print ^Hello world!^]
+
+; example 2 --- simple arithmetics
+![defun add [x y] [+ x y]]
+![print [add 2 3]]
+
+; example 3 --- semantic macros
+![defmacro do-n-times [times body]
+	`[progn
+		[setq do-counter 0]
+		[setq do-max ,times]
+		[do [< do-counter do-max]
+			[progn 
+				[setq do-counter [+ do-counter 1]]
+				,body
+			]
+		]
+	]
+]
+![do-n-times 4 [print ^tekst\n^]]
+
+
+; example 4 --- conditional expression
+![if [> 2 1] [print ^Wieksze^] [print ^Mniejsze^]]
+
+; example 5 --- lambda expressions
+![[lambda [x y] [if [> x y] [print ^wieksze^] [print ^mniejsze^]]] 2 3]
+
+; example 6 --- resursion
+![defun factorial [n]
+	[if [= n 0] 1
+		[* n [factorial [- n 1]]]
+	]
+]
 ![print [factorial 8]]

+ 13 - 13
Mods/vcmi/Data/s/testy.erm

@@ -1,14 +1,14 @@
-ZVSE
-!?PI;
-	!!VRv2777:S4;
-	!!DO1/0/5/1&v2777<>1:P0;
-
-!?FU1;
-	!!VRv2778:Sx16%2;
-	!!IF&x16>3:M^Hello world number %X16! To duza liczba^;
-	!!IF&v2778==0&x16<=3:M^Hello world number %X16! To mala parzysta liczba^;
-	!!IF&v2778==1&x16<=3:M^Hello world number %X16! To mala nieparzysta liczba^;
-
-!?PI;
-	!!VRz10:S^Composed hello ^;
+ZVSE
+!?PI;
+	!!VRv2777:S4;
+	!!DO1/0/5/1&v2777<>1:P0;
+
+!?FU1;
+	!!VRv2778:Sx16%2;
+	!!IF&x16>3:M^Hello world number %X16! To duza liczba^;
+	!!IF&v2778==0&x16<=3:M^Hello world number %X16! To mala parzysta liczba^;
+	!!IF&v2778==1&x16<=3:M^Hello world number %X16! To mala nieparzysta liczba^;
+
+!?PI;
+	!!VRz10:S^Composed hello ^;
 	!!IF:M^%Z10%%world%%, v2777=%V2777, v2778=%V2778!^;

+ 369 - 369
Mods/vcmi/config/vcmi/german.json

@@ -1,369 +1,369 @@
-{
-	"vcmi.adventureMap.monsterThreat.title"     : "\n\n Bedrohung: ",
-	"vcmi.adventureMap.monsterThreat.levels.0"  : "Mühelos",
-	"vcmi.adventureMap.monsterThreat.levels.1"  : "Sehr schwach",
-	"vcmi.adventureMap.monsterThreat.levels.2"  : "Schwach",
-	"vcmi.adventureMap.monsterThreat.levels.3"  : "Ein bisschen schwächer",
-	"vcmi.adventureMap.monsterThreat.levels.4"  : "Gleichauf",
-	"vcmi.adventureMap.monsterThreat.levels.5"  : "Ein bisschen stärker",
-	"vcmi.adventureMap.monsterThreat.levels.6"  : "Stark",
-	"vcmi.adventureMap.monsterThreat.levels.7"  : "Sehr Stark",
-	"vcmi.adventureMap.monsterThreat.levels.8"  : "Herausfordernd",
-	"vcmi.adventureMap.monsterThreat.levels.9"  : "Überwältigend",
-	"vcmi.adventureMap.monsterThreat.levels.10" : "Tödlich",
-	"vcmi.adventureMap.monsterThreat.levels.11" : "Unmöglich",
-
-	"vcmi.adventureMap.confirmRestartGame"     : "Seid Ihr sicher, dass Ihr das Spiel neu starten wollt?",
-	"vcmi.adventureMap.noTownWithMarket"       : "Kein Marktplatz verfügbar!",
-	"vcmi.adventureMap.noTownWithTavern"       : "Keine Stadt mit Taverne verfügbar!",
-	"vcmi.adventureMap.spellUnknownProblem"    : "Unbekanntes Problem mit diesem Zauberspruch, keine weiteren Informationen verfügbar.",
-	"vcmi.adventureMap.playerAttacked"         : "Spieler wurde attackiert: %s",
-	"vcmi.adventureMap.moveCostDetails"        : "Bewegungspunkte - Kosten: %TURNS Runden + %POINTS Punkte, Verbleibende Punkte: %REMAINING",
-	"vcmi.adventureMap.moveCostDetailsNoTurns" : "Bewegungspunkte - Kosten: %POINTS Punkte, Verbleibende Punkte: %REMAINING",
-
-	"vcmi.capitalColors.0" : "Rot",
-	"vcmi.capitalColors.1" : "Blau",
-	"vcmi.capitalColors.2" : "Braun",
-	"vcmi.capitalColors.3" : "Grün",
-	"vcmi.capitalColors.4" : "Orange",
-	"vcmi.capitalColors.5" : "Violett",
-	"vcmi.capitalColors.6" : "Türkis",
-	"vcmi.capitalColors.7" : "Rosa",
-	
-	"vcmi.heroOverview.startingArmy" : "Starteinheiten",
-	"vcmi.heroOverview.warMachine" : "Kriegsmaschinen",
-	"vcmi.heroOverview.secondarySkills" : "Sekundäre Skills",
-	"vcmi.heroOverview.spells" : "Zaubersprüche",
-	
-	"vcmi.radialWheel.mergeSameUnit" : "Gleiche Kreaturen zusammenführen",
-	"vcmi.radialWheel.splitSingleUnit" : "Wegtrennen einzelner Kreaturen",
-	"vcmi.radialWheel.splitUnitEqually" : "Gleichmäßiges trennen der Kreaturen",
-	"vcmi.radialWheel.moveUnit" : "Verschieben der Kreatur in andere Armee",
-	"vcmi.radialWheel.splitUnit" : "Aufsplitten der Kreatur in anderen Slot",
-
-	"vcmi.mainMenu.serverConnecting" : "Verbinde...",
-	"vcmi.mainMenu.serverAddressEnter" : "Addresse eingeben:",
-	"vcmi.mainMenu.serverConnectionFailed" : "Verbindung fehlgeschlagen",
-	"vcmi.mainMenu.serverClosing" : "Trenne...",
-	"vcmi.mainMenu.hostTCP" : "Hoste TCP/IP Spiel",
-	"vcmi.mainMenu.joinTCP" : "Trete TCP/IP Spiel bei",
-	"vcmi.mainMenu.playerName" : "Spieler",
-	
-	"vcmi.lobby.filepath" : "Dateipfad",
-	"vcmi.lobby.creationDate" : "Erstellungsdatum",
-	"vcmi.lobby.scenarioName" : "Szenario-Name",
-	"vcmi.lobby.mapPreview" : "Kartenvorschau",
-	"vcmi.lobby.noPreview" : "Keine Vorschau",
-	"vcmi.lobby.noUnderground" : "Kein Untergrund",
-
-	"vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst",
-	"vcmi.server.errors.modsToEnable"    : "{Erforderliche Mods um das Spiel zu laden}",
-	"vcmi.server.confirmReconnect"       : "Mit der letzten Sitzung verbinden?",
-
-	"vcmi.settingsMainWindow.generalTab.hover" : "Allgemein",
-	"vcmi.settingsMainWindow.generalTab.help"     : "Wechselt zur Registerkarte Allgemeine Optionen, die Einstellungen zum allgemeinen Verhalten des Spielclients enthält.",
-	"vcmi.settingsMainWindow.battleTab.hover" : "Kampf",
-	"vcmi.settingsMainWindow.battleTab.help"     : "Wechselt zur Registerkarte Kampfoptionen, auf der das Spielverhalten während eines Kampfes konfiguriert werden kann.",
-	"vcmi.settingsMainWindow.adventureTab.hover" : "Abenteuer-Karte",
-	"vcmi.settingsMainWindow.adventureTab.help"  : "Wechselt zur Registerkarte Abenteuerkartenoptionen - die Abenteuerkarte ist der Teil des Spiels, in dem du deine Helden bewegen kannst.",
-
-	"vcmi.systemOptions.videoGroup" : "Video-Einstellungen",
-	"vcmi.systemOptions.audioGroup" : "Audio-Einstellungen",
-	"vcmi.systemOptions.otherGroup" : "Andere Einstellungen", // unused right now
-	"vcmi.systemOptions.townsGroup" : "Stadt-Bildschirm",
-
-	"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.",
-	"vcmi.systemOptions.resolutionMenu.hover"   : "Wähle Auflösung",
-	"vcmi.systemOptions.resolutionMenu.help"    : "Ändere die Spielauflösung.",
-	"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.longTouchButton.hover"   : "Berührungsdauer für langer Touch: %d ms", // Translation note: "ms" = "milliseconds"
-	"vcmi.systemOptions.longTouchButton.help"    : "{Berührungsdauer für langer Touch}\n\nBei Verwendung des Touchscreens erscheinen Popup-Fenster nach Berührung des Bildschirms für die angegebene Dauer (in Millisekunden)",
-	"vcmi.systemOptions.longTouchMenu.hover"     : "Wähle Berührungsdauer für langer Touch",
-	"vcmi.systemOptions.longTouchMenu.help"      : "Ändere die Berührungsdauer für den langen Touch",
-	"vcmi.systemOptions.longTouchMenu.entry"     : "%d Millisekunden",
-	"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.",
-	"vcmi.systemOptions.hapticFeedbackButton.hover"  : "Haptisches Feedback",
-	"vcmi.systemOptions.hapticFeedbackButton.help"   : "{Haptisches Feedback}\n\nHaptisches Feedback bei Touch-Eingaben.",
-	"vcmi.systemOptions.enableUiEnhancementsButton.hover"  : "Interface Verbesserungen",
-	"vcmi.systemOptions.enableUiEnhancementsButton.help"   : "{Interface Verbesserungen}\n\nSchaltet verschiedene Interface Verbesserungen um. Wie z.B. ein größeres Zauberbuch, Rucksack, etc. Deaktivieren, um ein klassischeres Erlebnis zu haben.",
-
-	"vcmi.adventureOptions.infoBarPick.hover" : "Meldungen im Infobereich anzeigen",
-	"vcmi.adventureOptions.infoBarPick.help" : "{Meldungen im Infobereich anzeigen}\n\nWann immer möglich, werden Spielnachrichten von besuchten Kartenobjekten in der Infoleiste angezeigt, anstatt als Popup-Fenster zu erscheinen",
-	"vcmi.adventureOptions.numericQuantities.hover" : "Numerische Kreaturenmengen",
-	"vcmi.adventureOptions.numericQuantities.help" : "{Numerische Kreaturenmengen}\n\n Zeigt die ungefähre Menge der feindlichen Kreaturen im numerischen Format A-B an.",
-	"vcmi.adventureOptions.forceMovementInfo.hover" : "Bewegungskosten immer anzeigen",
-	"vcmi.adventureOptions.forceMovementInfo.help" : "{Bewegungskosten immer anzeigen}\n\n Ersetzt die Standardinformationen in der Statusleiste durch die Daten der Bewegungspunkte, ohne dass die ALT-Taste gedrückt werden muss.",
-	"vcmi.adventureOptions.showGrid.hover" : "Raster anzeigen",
-	"vcmi.adventureOptions.showGrid.help" : "{Raster anzeigen}\n\n Zeigt eine Rasterüberlagerung, die die Grenzen zwischen den Kacheln der Abenteuerkarte anzeigt.",
-	"vcmi.adventureOptions.borderScroll.hover" : "Scrollen am Rand",
-	"vcmi.adventureOptions.borderScroll.help" : "{Scrollen am Rand}\n\nScrollt die Abenteuerkarte, wenn sich der Cursor neben dem Fensterrand befindet. Kann mit gedrückter STRG-Taste deaktiviert werden.",
-	"vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Info-Panel Kreaturenmanagement",
-	"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info-Panel Kreaturenmanagement}\n\nErmöglicht die Neuanordnung von Kreaturen im Info-Panel, anstatt zwischen den Standardkomponenten zu wechseln",
-	"vcmi.adventureOptions.leftButtonDrag.hover" : "Ziehen der Karte mit Links",
-	"vcmi.adventureOptions.leftButtonDrag.help" : "{Ziehen der Karte mit Links}\n\nWenn aktiviert, wird die Maus bei gedrückter linker Taste in die Kartenansicht gezogen",
-	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
-	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
-	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
-	"vcmi.adventureOptions.mapScrollSpeed1.help": "Geschwindigkeit des Kartenbildlaufs auf sehr langsam einstellen",
-	"vcmi.adventureOptions.mapScrollSpeed5.help": "Geschwindigkeit des Kartenbildlaufs auf sehr schnell einstellen",
-	"vcmi.adventureOptions.mapScrollSpeed6.help": "Geschwindigkeit des Kartenbildlaufs auf sofort einstellen",
-
-	"vcmi.battleOptions.queueSizeLabel.hover": "Reihenfolge der Kreaturen anzeigen",
-	"vcmi.battleOptions.queueSizeNoneButton.hover": "AUS",
-	"vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO",
-	"vcmi.battleOptions.queueSizeSmallButton.hover": "KLEIN",
-	"vcmi.battleOptions.queueSizeBigButton.hover": "GROß",
-	"vcmi.battleOptions.queueSizeNoneButton.help": "Vollständige Deaktivierung der Sichtbarkeit der Reihenfolge der Kreaturen im Kampf",
-	"vcmi.battleOptions.queueSizeAutoButton.help": "Stellt die Größe der Zugreihenfolge abhängig von der Spielauflösung ein (klein, wenn mit einer Bildschirmauflösung unter 700 Pixeln gespielt wird, ansonsten groß)",
-	"vcmi.battleOptions.queueSizeSmallButton.help": "Setzt die Zugreihenfolge auf klein",
-	"vcmi.battleOptions.queueSizeBigButton.help": "Setzt die Größe der Zugreihenfolge auf groß (nicht unterstützt, wenn die Spielauflösung weniger als 700 Pixel hoch ist)",
-	"vcmi.battleOptions.animationsSpeed1.hover": "",
-	"vcmi.battleOptions.animationsSpeed5.hover": "",
-	"vcmi.battleOptions.animationsSpeed6.hover": "",
-	"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.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.rangeLimitHighlightOnHover.hover": "Bereichsgrenzen für Schützen anzeigen",
-	"vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Bereichsgrenzen für Schützen anzeigen}\n\nZeigt die Entfernungsgrenzen des Schützen an, wenn man mit dem Mauszeiger über ihn fährt.",
-	"vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Statistikfenster für Helden anzeigen",
-	"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Statistikfenster für Helden anzeigen}\n\nDauerhaftes Einschalten des Statistikfenster für Helden, das die primären Werte und Zauberpunkte anzeigt.",
-	"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).",
-	"vcmi.battleWindow.damageEstimation.meleeKills" : "Angriff auf %CREATURE (%DAMAGE, %KILLS).",
-	"vcmi.battleWindow.damageEstimation.ranged" : "Schuss auf %CREATURE (%SHOTS, %DAMAGE).",
-	"vcmi.battleWindow.damageEstimation.rangedKills" : "Schuss auf %CREATURE (%SHOTS, %DAMAGE, %KILLS).",
-	"vcmi.battleWindow.damageEstimation.shots" : "%d Schüsse verbleibend",
-	"vcmi.battleWindow.damageEstimation.shots.1" : "%d Schüsse verbleibend",
-	"vcmi.battleWindow.damageEstimation.damage" : "%d Schaden",
-	"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ö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.",
-
-	"vcmi.townHall.missingBase"             : "Basis Gebäude %s muss als erstes gebaut werden",
-	"vcmi.townHall.noCreaturesToRecruit"    : "Es gibt keine Kreaturen zu rekrutieren!",
-	"vcmi.townHall.greetingManaVortex"      : "Wenn Ihr Euch den %s nähert, wird Euer Körper mit neuer Energie gefüllt. Ihr habt Eure normalen Zauberpunkte verdoppelt.",
-	"vcmi.townHall.greetingKnowledge"       : "Ihr studiert die Glyphen auf dem %s und erhaltet Einblick in die Funktionsweise verschiedener Magie (+1 Wissen).",
-	"vcmi.townHall.greetingSpellPower"      : "Der %s lehrt Euch neue Wege, Eure magischen Kräfte zu bündeln (+1 Kraft).",
-	"vcmi.townHall.greetingExperience"      : "Ein Besuch bei den %s bringt Euch viele neue Fähigkeiten bei (+1000 Erfahrung).",
-	"vcmi.townHall.greetingAttack"          : "Nach einiger Zeit im %s könnt Ihr effizientere Kampffertigkeiten erlernen (+1 Angriffsfertigkeit).",
-	"vcmi.townHall.greetingDefence"         : "Wenn Ihr Zeit im %s verbringt, bringen Euch die erfahrenen Krieger dort zusätzliche Verteidigungsfähigkeiten bei (+1 Verteidigung).",
-	"vcmi.townHall.hasNotProduced"          : "Die %s hat noch nichts produziert.",
-	"vcmi.townHall.hasProduced"             : "Die %s hat diese Woche %d %s produziert.",
-	"vcmi.townHall.greetingCustomBonus"     : "%s gibt Ihnen +%d %s%s",
-	"vcmi.townHall.greetingCustomUntil"     : " bis zur nächsten Schlacht.",
-	"vcmi.townHall.greetingInTownMagicWell" : "%s hat Eure Zauberpunkte wieder auf das Maximum erhöht.",
-
-	"vcmi.logicalExpressions.anyOf"  : "Eines der folgenden:",
-	"vcmi.logicalExpressions.allOf"  : "Alles der folgenden:",
-	"vcmi.logicalExpressions.noneOf" : "Keines der folgenden:",
-
-	"vcmi.heroWindow.openCommander.hover" : "Öffne Kommandanten-Fenster",
-	"vcmi.heroWindow.openCommander.help"  : "Zeige Informationen über Kommandanten dieses Helden",
-
-	"vcmi.commanderWindow.artifactMessage" : "Möchtet Ihr diesen Artefakt dem Helden zurückgeben?",
-
-	"vcmi.creatureWindow.showBonuses.hover"    : "Wechsle zur Bonus-Ansicht",
-	"vcmi.creatureWindow.showBonuses.help"     : "Zeige alle aktiven Boni des Kommandanten",
-	"vcmi.creatureWindow.showSkills.hover"     : "Wechsle zur Fertigkeits-Ansicht",
-	"vcmi.creatureWindow.showSkills.help"      : "Zeige alle erlernten Fertigkeiten des Kommandanten",
-	"vcmi.creatureWindow.returnArtifact.hover" : "Artefekt zurückgeben",
-	"vcmi.creatureWindow.returnArtifact.help"  : "Nutze diese Schaltfläche, um Stapel-Artefakt in den Rucksack des Helden zurückzugeben",
-
-	"vcmi.questLog.hideComplete.hover" : "Verstecke abgeschlossene Quests",
-	"vcmi.questLog.hideComplete.help"  : "Verstecke alle Quests die bereits abgeschlossen sind",
-
-	"vcmi.randomMapTab.widgets.randomTemplate"      : "(Zufällig)",
-	"vcmi.randomMapTab.widgets.templateLabel"        : "Template",
-	"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Einrichtung...",
-	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Team-Zuordnungen",
-	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Straßentypen",
-
-	// Custom victory conditions for H3 campaigns and HotA maps
-	"vcmi.map.victoryCondition.daysPassed.toOthers" : "Der Feind hat es geschafft, bis zum heutigen Tag zu überleben. Der Sieg gehört ihm!",
-	"vcmi.map.victoryCondition.daysPassed.toSelf" : "Herzlichen Glückwunsch! Ihr habt es geschafft, zu überleben. Der Sieg ist euer!",
-	"vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "Der Feind hat alle Monster besiegt, die das Land heimsuchen, und fordert den Sieg!",
-	"vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Herzlichen Glückwunsch! Ihr habt alle Monster besiegt, die dieses Land plagen, und könnt den Sieg für euch beanspruchen!",
-	"vcmi.map.victoryCondition.collectArtifacts.message" : "Sammelt drei Artefakte",
-	"vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Herzlichen Glückwunsch! Alle eure Feinde wurden besiegt und ihr habt die Engelsallianz! Der Sieg ist euer!",
-	"vcmi.map.victoryCondition.angelicAlliance.message" : "Besiege alle Feinde und gründe eine Engelsallianz",
-
-	// few strings from WoG used by vcmi
-	"vcmi.stackExperience.description" : "» D e t a i l s   z u r   S t a p e l e r f a h r u n g «\n\nKreatur-Typ ................... : %s\nErfahrungsrang ................. : %s (%i)\nErfahrungspunkte ............... : %i\nErfahrungspunkte für den nächsten Rang .. : %i\nMaximale Erfahrung pro Kampf ... : %i%% (%i)\nAnzahl der Kreaturen im Stapel .... : %i\nMaximale Anzahl neuer Rekruten\n ohne Verlust von aktuellem Rang .... : %i\nErfahrungs-Multiplikator ........... : %.2f\nUpgrade-Multiplikator .............. : %.2f\nErfahrung nach Rang 10 ........ : %i\nMaximale Anzahl der neuen Rekruten, die bei\n Rang 10 bei maximaler Erfahrung übrig sind : %i",
-	"vcmi.stackExperience.rank.0" : "Grundlagen",
-	"vcmi.stackExperience.rank.1" : "Neuling",
-	"vcmi.stackExperience.rank.2" : "Ausgebildet",
-	"vcmi.stackExperience.rank.3" : "Kompetent",
-	"vcmi.stackExperience.rank.4" : "Bewährt",
-	"vcmi.stackExperience.rank.5" : "Veteran",
-	"vcmi.stackExperience.rank.6" : "Gekonnt",
-	"vcmi.stackExperience.rank.7" : "Experte",
-	"vcmi.stackExperience.rank.8" : "Elite",
-	"vcmi.stackExperience.rank.9" : "Meister",
-	"vcmi.stackExperience.rank.10" : "Ass",
-	
-	"core.bonus.ADDITIONAL_ATTACK.name": "Doppelschlag",
-	"core.bonus.ADDITIONAL_ATTACK.description": "Greift zweimal an",
-	"core.bonus.ADDITIONAL_RETALIATION.name": "Zusätzliche Vergeltungsmaßnahmen",
-	"core.bonus.ADDITIONAL_RETALIATION.description": "Kann ${val} zusätzliche Male vergelten",
-	"core.bonus.AIR_IMMUNITY.name": "Luftimmunität",
-	"core.bonus.AIR_IMMUNITY.description": "Immun gegen alle Luftschulzauber",
-	"core.bonus.ATTACKS_ALL_ADJACENT.name": "Rundum angreifen",
-	"core.bonus.ATTACKS_ALL_ADJACENT.description": "Greift alle benachbarten Gegner an",
-	"core.bonus.BLOCKS_RETALIATION.name": "Keine Vergeltung",
-	"core.bonus.BLOCKS_RETALIATION.description": "Feind kann nicht vergelten",
-	"core.bonus.BLOCKS_RANGED_RETALIATION.name": "Keine Reichweitenverschiebung",
-	"core.bonus.BLOCKS_RANGED_RETALIATION.description": "Feind kann nicht durch Schießen vergelten",
-	"core.bonus.CATAPULT.name": "Katapult",
-	"core.bonus.CATAPULT.description": "Greift Belagerungsmauern an",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Reduziere Zauberkosten (${val})",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Reduziert die Zauberkosten für den Helden",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Zauberdämpfer (${val})",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Erhöht die Kosten von gegnerischen Zaubern",
-	"core.bonus.CHARGE_IMMUNITY.name": "Immun gegen Aufladung",
-	"core.bonus.CHARGE_IMMUNITY.description": "Immun gegen Aufladung",
-	"core.bonus.DARKNESS.name": "Abdeckung der Dunkelheit",
-	"core.bonus.DARKNESS.description": "Fügt ${val} Dunkelheitsradius hinzu",
-	"core.bonus.DEATH_STARE.name": "Todesstarren (${val}%)",
-	"core.bonus.DEATH_STARE.description": "${val}% Chance, eine einzelne Kreatur zu töten",
-	"core.bonus.DEFENSIVE_STANCE.name": "Verteidigungsbonus",
-	"core.bonus.DEFENSIVE_STANCE.description": "+${val} Verteidigung beim Verteidigen",
-	"core.bonus.DESTRUCTION.name": "Zerstörung",
-	"core.bonus.DESTRUCTION.description": "Hat ${val}% Chance, zusätzliche Einheiten nach dem Angriff zu töten",
-	"core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Todesstoß",
-	"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.EARTH_IMMUNITY.name": "Erdimmunität",
-	"core.bonus.EARTH_IMMUNITY.description": "Immun gegen alle Zauber der Erdschule",
-	"core.bonus.ENCHANTER.name": "Verzauberer",
-	"core.bonus.ENCHANTER.description": "Kann jede Runde eine Masse von ${subtype.spell} zaubern",
-	"core.bonus.ENCHANTED.name": "Verzaubert",
-	"core.bonus.ENCHANTED.description": "Beeinflusst von permanentem ${subtype.spell}",
-	"core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignoriere Verteidigung (${val}%)",
-	"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Ignoriert einen Teil der Verteidigung für den Angriff",
-	"core.bonus.FIRE_IMMUNITY.name": "Feuerimmunität",
-	"core.bonus.FIRE_IMMUNITY.description": "Immun gegen alle Zauber der Schule des Feuers",
-	"core.bonus.FIRE_SHIELD.name": "Feuerschild (${val}%)",
-	"core.bonus.FIRE_SHIELD.description": "Reflektiert einen Teil des Nahkampfschadens",
-	"core.bonus.FIRST_STRIKE.name": "Erstschlag",
-	"core.bonus.FIRST_STRIKE.description": "Diese Kreatur greift zuerst an, anstatt zu vergelten",
-	"core.bonus.FEAR.name": "Furcht",
-	"core.bonus.FEAR.description": "Verursacht Furcht bei einem gegnerischen Stapel",
-	"core.bonus.FEARLESS.name": "Furchtlos",
-	"core.bonus.FEARLESS.description": "immun gegen die Fähigkeit Furcht",
-	"core.bonus.FLYING.name": "Fliegen",
-	"core.bonus.FLYING.description": "Kann fliegen (ignoriert Hindernisse)",
-	"core.bonus.FREE_SHOOTING.name": "Nah schießen",
-	"core.bonus.FREE_SHOOTING.description": "Kann im Nahkampf schießen",
-	"core.bonus.GARGOYLE.name": "Gargoyle",
-	"core.bonus.GARGOYLE.description": "Kann nicht aufgerichtet oder geheilt werden",
-	"core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Schaden vermindern (${val}%)",
-	"core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Reduziert physischen Schaden aus dem Fern- oder Nahkampf",
-	"core.bonus.HATE.name": "Hasst ${subtype.creature}",
-	"core.bonus.HATE.description": "Macht ${val}% mehr Schaden",
-	"core.bonus.HEALER.name": "Heiler",
-	"core.bonus.HEALER.description": "Heilt verbündete Einheiten",
-	"core.bonus.HP_REGENERATION.name": "Regeneration",
-	"core.bonus.HP_REGENERATION.description": "Heilt ${val} Trefferpunkte jede Runde",
-	"core.bonus.JOUSTING.name": "Champion Charge",
-	"core.bonus.JOUSTING.description": "+${val}% Schaden pro zurückgelegtem Feld",
-	"core.bonus.KING.name": "König",
-	"core.bonus.KING.description": "Anfällig für SLAYER Level ${val} oder höher",
-	"core.bonus.LEVEL_SPELL_IMMUNITY.name": "Zauberimmunität 1-${val}",
-	"core.bonus.LEVEL_SPELL_IMMUNITY.description": "Immun gegen Zaubersprüche der Stufen 1-${val}",
-	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Begrenzte Schussweite",
-	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Kann nicht auf Ziele schießen, die weiter als ${val} Felder entfernt sind",
-	"core.bonus.LIFE_DRAIN.name": "Leben entziehen (${val}%)",
-	"core.bonus.LIFE_DRAIN.description": "Drainiert ${val}% des zugefügten Schadens",
-	"core.bonus.MANA_CHANNELING.name": "Magiekanal ${val}%",
-	"core.bonus.MANA_CHANNELING.description": "Gibt Ihrem Helden Mana, das vom Gegner ausgegeben wird",
-	"core.bonus.MANA_DRAIN.name": "Mana-Entzug",
-	"core.bonus.MANA_DRAIN.description": "Entzieht ${val} Mana jede Runde",
-	"core.bonus.MAGIC_MIRROR.name": "Zauberspiegel (${val}%)",
-	"core.bonus.MAGIC_MIRROR.description": "${val}% Chance, einen Angriffszauber auf den Gegner umzulenken",
-	"core.bonus.MAGIC_RESISTANCE.name": "Magie-Widerstand(${val}%)",
-	"core.bonus.MAGIC_RESISTANCE.description": "${val}% Chance, gegnerischem Zauber zu widerstehen",
-	"core.bonus.MIND_IMMUNITY.name": "Geist-Zauber-Immunität",
-	"core.bonus.MIND_IMMUNITY.description": "Immun gegen Zauber vom Typ Geist",
-	"core.bonus.NO_DISTANCE_PENALTY.name": "Keine Entfernungsstrafe",
-	"core.bonus.NO_DISTANCE_PENALTY.description": "Voller Schaden aus beliebiger Entfernung",
-	"core.bonus.NO_MELEE_PENALTY.name": "Keine Nahkampf-Strafe",
-	"core.bonus.NO_MELEE_PENALTY.description": "Kreatur hat keinen Nahkampf-Malus",
-	"core.bonus.NO_MORALE.name": "Neutrale Moral",
-	"core.bonus.NO_MORALE.description": "Kreatur ist immun gegen Moral-Effekte",
-	"core.bonus.NO_WALL_PENALTY.name": "Keine Wand-Strafe",
-	"core.bonus.NO_WALL_PENALTY.description": "Voller Schaden bei Belagerung",
-	"core.bonus.NON_LIVING.name": "Nicht lebend",
-	"core.bonus.NON_LIVING.description": "Immunität gegen viele Effekte",
-	"core.bonus.RANDOM_SPELLCASTER.name": "Zufälliger Zauberwirker",
-	"core.bonus.RANDOM_SPELLCASTER.description": "Kann einen zufälligen Zauberspruch wirken",
-	"core.bonus.RANGED_RETALIATION.name": "Fernkampf-Vergeltung",
-	"core.bonus.RANGED_RETALIATION.description": "Kann einen Fernkampf-Gegenangriff durchführen",
-	"core.bonus.RECEPTIVE.name": "Empfänglich",
-	"core.bonus.RECEPTIVE.description": "Keine Immunität gegen Freundschaftszauber",
-	"core.bonus.REBIRTH.name": "Wiedergeburt (${val}%)",
-	"core.bonus.REBIRTH.description": "${val}% des Stacks wird nach dem Tod auferstehen",
-	"core.bonus.RETURN_AFTER_STRIKE.name": "Angriff und Rückkehr",
-	"core.bonus.RETURN_AFTER_STRIKE.description": "Kehrt nach Nahkampfangriff zurück",
-	"core.bonus.SHOOTER.name": "Fernkämpfer",
-	"core.bonus.SHOOTER.description": "Kreatur kann schießen",
-	"core.bonus.SHOOTS_ALL_ADJACENT.name": "Schießt rundherum",
-	"core.bonus.SHOOTS_ALL_ADJACENT.description": "Die Fernkampfangriffe dieser Kreatur treffen alle Ziele in einem kleinen Bereich",
-	"core.bonus.SOUL_STEAL.name": "Seelenraub",
-	"core.bonus.SOUL_STEAL.description": "Gewinnt ${val} neue Kreaturen für jeden getöteten Gegner",
-	"core.bonus.SPELLCASTER.name": "Zauberer",
-	"core.bonus.SPELLCASTER.description": "Kann ${subtype.spell} zaubern",
-	"core.bonus.SPELL_AFTER_ATTACK.name": "Nach Angriff zaubern",
-	"core.bonus.SPELL_AFTER_ATTACK.description": "${val}%, um ${subtype.spell} nach dem Angriff zu wirken",
-	"core.bonus.SPELL_BEFORE_ATTACK.name": "Zauber vor Angriff",
-	"core.bonus.SPELL_BEFORE_ATTACK.description": "${val}% um ${subtype.spell} vor dem Angriff zu wirken",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "Zauberwiderstand",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description": "Schaden von Zaubern reduziert ${val}%.",
-	"core.bonus.SPELL_IMMUNITY.name": "Zauberimmunität",
-	"core.bonus.SPELL_IMMUNITY.description": "Immun gegen ${subtype.spell}",
-	"core.bonus.SPELL_LIKE_ATTACK.name": "zauberähnlicher Angriff",
-	"core.bonus.SPELL_LIKE_ATTACK.description": "Angriffe mit ${subtype.spell}",
-	"core.bonus.SPELL_RESISTANCE_AURA.name": "Aura des Widerstands",
-	"core.bonus.SPELL_RESISTANCE_AURA.description": "Stapel in der Nähe erhalten ${val}% Widerstand",
-	"core.bonus.SUMMON_GUARDIANS.name": "Wächter beschwören",
-	"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": "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",
-	"core.bonus.TRANSMUTATION.name": "Transmutation",
-	"core.bonus.TRANSMUTATION.description": "${val}% Chance, angegriffene Einheit in einen anderen Typ zu verwandeln",
-	"core.bonus.UNDEAD.name": "Untot",
-	"core.bonus.UNDEAD.description": "Kreatur ist untot",
-	"core.bonus.UNLIMITED_RETALIATIONS.name": "Unbegrenzte Vergeltungsmaßnahmen",
-	"core.bonus.UNLIMITED_RETALIATIONS.description": "Vergeltungen für eine beliebige Anzahl von Angriffen",
-	"core.bonus.WATER_IMMUNITY.name": "Wasser-Immunität",
-	"core.bonus.WATER_IMMUNITY.description": "Immun gegen alle Zauber der Wasserschule",
-	"core.bonus.WIDE_BREATH.name": "Breiter Atem",
-	"core.bonus.WIDE_BREATH.description": "Breiter Atem-Angriff (mehrere Felder)"
-}
+{
+	"vcmi.adventureMap.monsterThreat.title"     : "\n\n Bedrohung: ",
+	"vcmi.adventureMap.monsterThreat.levels.0"  : "Mühelos",
+	"vcmi.adventureMap.monsterThreat.levels.1"  : "Sehr schwach",
+	"vcmi.adventureMap.monsterThreat.levels.2"  : "Schwach",
+	"vcmi.adventureMap.monsterThreat.levels.3"  : "Ein bisschen schwächer",
+	"vcmi.adventureMap.monsterThreat.levels.4"  : "Gleichauf",
+	"vcmi.adventureMap.monsterThreat.levels.5"  : "Ein bisschen stärker",
+	"vcmi.adventureMap.monsterThreat.levels.6"  : "Stark",
+	"vcmi.adventureMap.monsterThreat.levels.7"  : "Sehr Stark",
+	"vcmi.adventureMap.monsterThreat.levels.8"  : "Herausfordernd",
+	"vcmi.adventureMap.monsterThreat.levels.9"  : "Überwältigend",
+	"vcmi.adventureMap.monsterThreat.levels.10" : "Tödlich",
+	"vcmi.adventureMap.monsterThreat.levels.11" : "Unmöglich",
+
+	"vcmi.adventureMap.confirmRestartGame"     : "Seid Ihr sicher, dass Ihr das Spiel neu starten wollt?",
+	"vcmi.adventureMap.noTownWithMarket"       : "Kein Marktplatz verfügbar!",
+	"vcmi.adventureMap.noTownWithTavern"       : "Keine Stadt mit Taverne verfügbar!",
+	"vcmi.adventureMap.spellUnknownProblem"    : "Unbekanntes Problem mit diesem Zauberspruch, keine weiteren Informationen verfügbar.",
+	"vcmi.adventureMap.playerAttacked"         : "Spieler wurde attackiert: %s",
+	"vcmi.adventureMap.moveCostDetails"        : "Bewegungspunkte - Kosten: %TURNS Runden + %POINTS Punkte, Verbleibende Punkte: %REMAINING",
+	"vcmi.adventureMap.moveCostDetailsNoTurns" : "Bewegungspunkte - Kosten: %POINTS Punkte, Verbleibende Punkte: %REMAINING",
+
+	"vcmi.capitalColors.0" : "Rot",
+	"vcmi.capitalColors.1" : "Blau",
+	"vcmi.capitalColors.2" : "Braun",
+	"vcmi.capitalColors.3" : "Grün",
+	"vcmi.capitalColors.4" : "Orange",
+	"vcmi.capitalColors.5" : "Violett",
+	"vcmi.capitalColors.6" : "Türkis",
+	"vcmi.capitalColors.7" : "Rosa",
+	
+	"vcmi.heroOverview.startingArmy" : "Starteinheiten",
+	"vcmi.heroOverview.warMachine" : "Kriegsmaschinen",
+	"vcmi.heroOverview.secondarySkills" : "Sekundäre Skills",
+	"vcmi.heroOverview.spells" : "Zaubersprüche",
+	
+	"vcmi.radialWheel.mergeSameUnit" : "Gleiche Kreaturen zusammenführen",
+	"vcmi.radialWheel.splitSingleUnit" : "Wegtrennen einzelner Kreaturen",
+	"vcmi.radialWheel.splitUnitEqually" : "Gleichmäßiges trennen der Kreaturen",
+	"vcmi.radialWheel.moveUnit" : "Verschieben der Kreatur in andere Armee",
+	"vcmi.radialWheel.splitUnit" : "Aufsplitten der Kreatur in anderen Slot",
+
+	"vcmi.mainMenu.serverConnecting" : "Verbinde...",
+	"vcmi.mainMenu.serverAddressEnter" : "Addresse eingeben:",
+	"vcmi.mainMenu.serverConnectionFailed" : "Verbindung fehlgeschlagen",
+	"vcmi.mainMenu.serverClosing" : "Trenne...",
+	"vcmi.mainMenu.hostTCP" : "Hoste TCP/IP Spiel",
+	"vcmi.mainMenu.joinTCP" : "Trete TCP/IP Spiel bei",
+	"vcmi.mainMenu.playerName" : "Spieler",
+	
+	"vcmi.lobby.filepath" : "Dateipfad",
+	"vcmi.lobby.creationDate" : "Erstellungsdatum",
+	"vcmi.lobby.scenarioName" : "Szenario-Name",
+	"vcmi.lobby.mapPreview" : "Kartenvorschau",
+	"vcmi.lobby.noPreview" : "Keine Vorschau",
+	"vcmi.lobby.noUnderground" : "Kein Untergrund",
+
+	"vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst",
+	"vcmi.server.errors.modsToEnable"    : "{Erforderliche Mods um das Spiel zu laden}",
+	"vcmi.server.confirmReconnect"       : "Mit der letzten Sitzung verbinden?",
+
+	"vcmi.settingsMainWindow.generalTab.hover" : "Allgemein",
+	"vcmi.settingsMainWindow.generalTab.help"     : "Wechselt zur Registerkarte Allgemeine Optionen, die Einstellungen zum allgemeinen Verhalten des Spielclients enthält.",
+	"vcmi.settingsMainWindow.battleTab.hover" : "Kampf",
+	"vcmi.settingsMainWindow.battleTab.help"     : "Wechselt zur Registerkarte Kampfoptionen, auf der das Spielverhalten während eines Kampfes konfiguriert werden kann.",
+	"vcmi.settingsMainWindow.adventureTab.hover" : "Abenteuer-Karte",
+	"vcmi.settingsMainWindow.adventureTab.help"  : "Wechselt zur Registerkarte Abenteuerkartenoptionen - die Abenteuerkarte ist der Teil des Spiels, in dem du deine Helden bewegen kannst.",
+
+	"vcmi.systemOptions.videoGroup" : "Video-Einstellungen",
+	"vcmi.systemOptions.audioGroup" : "Audio-Einstellungen",
+	"vcmi.systemOptions.otherGroup" : "Andere Einstellungen", // unused right now
+	"vcmi.systemOptions.townsGroup" : "Stadt-Bildschirm",
+
+	"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.",
+	"vcmi.systemOptions.resolutionMenu.hover"   : "Wähle Auflösung",
+	"vcmi.systemOptions.resolutionMenu.help"    : "Ändere die Spielauflösung.",
+	"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.longTouchButton.hover"   : "Berührungsdauer für langer Touch: %d ms", // Translation note: "ms" = "milliseconds"
+	"vcmi.systemOptions.longTouchButton.help"    : "{Berührungsdauer für langer Touch}\n\nBei Verwendung des Touchscreens erscheinen Popup-Fenster nach Berührung des Bildschirms für die angegebene Dauer (in Millisekunden)",
+	"vcmi.systemOptions.longTouchMenu.hover"     : "Wähle Berührungsdauer für langer Touch",
+	"vcmi.systemOptions.longTouchMenu.help"      : "Ändere die Berührungsdauer für den langen Touch",
+	"vcmi.systemOptions.longTouchMenu.entry"     : "%d Millisekunden",
+	"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.",
+	"vcmi.systemOptions.hapticFeedbackButton.hover"  : "Haptisches Feedback",
+	"vcmi.systemOptions.hapticFeedbackButton.help"   : "{Haptisches Feedback}\n\nHaptisches Feedback bei Touch-Eingaben.",
+	"vcmi.systemOptions.enableUiEnhancementsButton.hover"  : "Interface Verbesserungen",
+	"vcmi.systemOptions.enableUiEnhancementsButton.help"   : "{Interface Verbesserungen}\n\nSchaltet verschiedene Interface Verbesserungen um. Wie z.B. ein größeres Zauberbuch, Rucksack, etc. Deaktivieren, um ein klassischeres Erlebnis zu haben.",
+
+	"vcmi.adventureOptions.infoBarPick.hover" : "Meldungen im Infobereich anzeigen",
+	"vcmi.adventureOptions.infoBarPick.help" : "{Meldungen im Infobereich anzeigen}\n\nWann immer möglich, werden Spielnachrichten von besuchten Kartenobjekten in der Infoleiste angezeigt, anstatt als Popup-Fenster zu erscheinen",
+	"vcmi.adventureOptions.numericQuantities.hover" : "Numerische Kreaturenmengen",
+	"vcmi.adventureOptions.numericQuantities.help" : "{Numerische Kreaturenmengen}\n\n Zeigt die ungefähre Menge der feindlichen Kreaturen im numerischen Format A-B an.",
+	"vcmi.adventureOptions.forceMovementInfo.hover" : "Bewegungskosten immer anzeigen",
+	"vcmi.adventureOptions.forceMovementInfo.help" : "{Bewegungskosten immer anzeigen}\n\n Ersetzt die Standardinformationen in der Statusleiste durch die Daten der Bewegungspunkte, ohne dass die ALT-Taste gedrückt werden muss.",
+	"vcmi.adventureOptions.showGrid.hover" : "Raster anzeigen",
+	"vcmi.adventureOptions.showGrid.help" : "{Raster anzeigen}\n\n Zeigt eine Rasterüberlagerung, die die Grenzen zwischen den Kacheln der Abenteuerkarte anzeigt.",
+	"vcmi.adventureOptions.borderScroll.hover" : "Scrollen am Rand",
+	"vcmi.adventureOptions.borderScroll.help" : "{Scrollen am Rand}\n\nScrollt die Abenteuerkarte, wenn sich der Cursor neben dem Fensterrand befindet. Kann mit gedrückter STRG-Taste deaktiviert werden.",
+	"vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Info-Panel Kreaturenmanagement",
+	"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info-Panel Kreaturenmanagement}\n\nErmöglicht die Neuanordnung von Kreaturen im Info-Panel, anstatt zwischen den Standardkomponenten zu wechseln",
+	"vcmi.adventureOptions.leftButtonDrag.hover" : "Ziehen der Karte mit Links",
+	"vcmi.adventureOptions.leftButtonDrag.help" : "{Ziehen der Karte mit Links}\n\nWenn aktiviert, wird die Maus bei gedrückter linker Taste in die Kartenansicht gezogen",
+	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed1.help": "Geschwindigkeit des Kartenbildlaufs auf sehr langsam einstellen",
+	"vcmi.adventureOptions.mapScrollSpeed5.help": "Geschwindigkeit des Kartenbildlaufs auf sehr schnell einstellen",
+	"vcmi.adventureOptions.mapScrollSpeed6.help": "Geschwindigkeit des Kartenbildlaufs auf sofort einstellen",
+
+	"vcmi.battleOptions.queueSizeLabel.hover": "Reihenfolge der Kreaturen anzeigen",
+	"vcmi.battleOptions.queueSizeNoneButton.hover": "AUS",
+	"vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO",
+	"vcmi.battleOptions.queueSizeSmallButton.hover": "KLEIN",
+	"vcmi.battleOptions.queueSizeBigButton.hover": "GROß",
+	"vcmi.battleOptions.queueSizeNoneButton.help": "Vollständige Deaktivierung der Sichtbarkeit der Reihenfolge der Kreaturen im Kampf",
+	"vcmi.battleOptions.queueSizeAutoButton.help": "Stellt die Größe der Zugreihenfolge abhängig von der Spielauflösung ein (klein, wenn mit einer Bildschirmauflösung unter 700 Pixeln gespielt wird, ansonsten groß)",
+	"vcmi.battleOptions.queueSizeSmallButton.help": "Setzt die Zugreihenfolge auf klein",
+	"vcmi.battleOptions.queueSizeBigButton.help": "Setzt die Größe der Zugreihenfolge auf groß (nicht unterstützt, wenn die Spielauflösung weniger als 700 Pixel hoch ist)",
+	"vcmi.battleOptions.animationsSpeed1.hover": "",
+	"vcmi.battleOptions.animationsSpeed5.hover": "",
+	"vcmi.battleOptions.animationsSpeed6.hover": "",
+	"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.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.rangeLimitHighlightOnHover.hover": "Bereichsgrenzen für Schützen anzeigen",
+	"vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Bereichsgrenzen für Schützen anzeigen}\n\nZeigt die Entfernungsgrenzen des Schützen an, wenn man mit dem Mauszeiger über ihn fährt.",
+	"vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Statistikfenster für Helden anzeigen",
+	"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Statistikfenster für Helden anzeigen}\n\nDauerhaftes Einschalten des Statistikfenster für Helden, das die primären Werte und Zauberpunkte anzeigt.",
+	"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).",
+	"vcmi.battleWindow.damageEstimation.meleeKills" : "Angriff auf %CREATURE (%DAMAGE, %KILLS).",
+	"vcmi.battleWindow.damageEstimation.ranged" : "Schuss auf %CREATURE (%SHOTS, %DAMAGE).",
+	"vcmi.battleWindow.damageEstimation.rangedKills" : "Schuss auf %CREATURE (%SHOTS, %DAMAGE, %KILLS).",
+	"vcmi.battleWindow.damageEstimation.shots" : "%d Schüsse verbleibend",
+	"vcmi.battleWindow.damageEstimation.shots.1" : "%d Schüsse verbleibend",
+	"vcmi.battleWindow.damageEstimation.damage" : "%d Schaden",
+	"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ö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.",
+
+	"vcmi.townHall.missingBase"             : "Basis Gebäude %s muss als erstes gebaut werden",
+	"vcmi.townHall.noCreaturesToRecruit"    : "Es gibt keine Kreaturen zu rekrutieren!",
+	"vcmi.townHall.greetingManaVortex"      : "Wenn Ihr Euch den %s nähert, wird Euer Körper mit neuer Energie gefüllt. Ihr habt Eure normalen Zauberpunkte verdoppelt.",
+	"vcmi.townHall.greetingKnowledge"       : "Ihr studiert die Glyphen auf dem %s und erhaltet Einblick in die Funktionsweise verschiedener Magie (+1 Wissen).",
+	"vcmi.townHall.greetingSpellPower"      : "Der %s lehrt Euch neue Wege, Eure magischen Kräfte zu bündeln (+1 Kraft).",
+	"vcmi.townHall.greetingExperience"      : "Ein Besuch bei den %s bringt Euch viele neue Fähigkeiten bei (+1000 Erfahrung).",
+	"vcmi.townHall.greetingAttack"          : "Nach einiger Zeit im %s könnt Ihr effizientere Kampffertigkeiten erlernen (+1 Angriffsfertigkeit).",
+	"vcmi.townHall.greetingDefence"         : "Wenn Ihr Zeit im %s verbringt, bringen Euch die erfahrenen Krieger dort zusätzliche Verteidigungsfähigkeiten bei (+1 Verteidigung).",
+	"vcmi.townHall.hasNotProduced"          : "Die %s hat noch nichts produziert.",
+	"vcmi.townHall.hasProduced"             : "Die %s hat diese Woche %d %s produziert.",
+	"vcmi.townHall.greetingCustomBonus"     : "%s gibt Ihnen +%d %s%s",
+	"vcmi.townHall.greetingCustomUntil"     : " bis zur nächsten Schlacht.",
+	"vcmi.townHall.greetingInTownMagicWell" : "%s hat Eure Zauberpunkte wieder auf das Maximum erhöht.",
+
+	"vcmi.logicalExpressions.anyOf"  : "Eines der folgenden:",
+	"vcmi.logicalExpressions.allOf"  : "Alles der folgenden:",
+	"vcmi.logicalExpressions.noneOf" : "Keines der folgenden:",
+
+	"vcmi.heroWindow.openCommander.hover" : "Öffne Kommandanten-Fenster",
+	"vcmi.heroWindow.openCommander.help"  : "Zeige Informationen über Kommandanten dieses Helden",
+
+	"vcmi.commanderWindow.artifactMessage" : "Möchtet Ihr diesen Artefakt dem Helden zurückgeben?",
+
+	"vcmi.creatureWindow.showBonuses.hover"    : "Wechsle zur Bonus-Ansicht",
+	"vcmi.creatureWindow.showBonuses.help"     : "Zeige alle aktiven Boni des Kommandanten",
+	"vcmi.creatureWindow.showSkills.hover"     : "Wechsle zur Fertigkeits-Ansicht",
+	"vcmi.creatureWindow.showSkills.help"      : "Zeige alle erlernten Fertigkeiten des Kommandanten",
+	"vcmi.creatureWindow.returnArtifact.hover" : "Artefekt zurückgeben",
+	"vcmi.creatureWindow.returnArtifact.help"  : "Nutze diese Schaltfläche, um Stapel-Artefakt in den Rucksack des Helden zurückzugeben",
+
+	"vcmi.questLog.hideComplete.hover" : "Verstecke abgeschlossene Quests",
+	"vcmi.questLog.hideComplete.help"  : "Verstecke alle Quests die bereits abgeschlossen sind",
+
+	"vcmi.randomMapTab.widgets.randomTemplate"      : "(Zufällig)",
+	"vcmi.randomMapTab.widgets.templateLabel"        : "Template",
+	"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Einrichtung...",
+	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Team-Zuordnungen",
+	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Straßentypen",
+
+	// Custom victory conditions for H3 campaigns and HotA maps
+	"vcmi.map.victoryCondition.daysPassed.toOthers" : "Der Feind hat es geschafft, bis zum heutigen Tag zu überleben. Der Sieg gehört ihm!",
+	"vcmi.map.victoryCondition.daysPassed.toSelf" : "Herzlichen Glückwunsch! Ihr habt es geschafft, zu überleben. Der Sieg ist euer!",
+	"vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "Der Feind hat alle Monster besiegt, die das Land heimsuchen, und fordert den Sieg!",
+	"vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Herzlichen Glückwunsch! Ihr habt alle Monster besiegt, die dieses Land plagen, und könnt den Sieg für euch beanspruchen!",
+	"vcmi.map.victoryCondition.collectArtifacts.message" : "Sammelt drei Artefakte",
+	"vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Herzlichen Glückwunsch! Alle eure Feinde wurden besiegt und ihr habt die Engelsallianz! Der Sieg ist euer!",
+	"vcmi.map.victoryCondition.angelicAlliance.message" : "Besiege alle Feinde und gründe eine Engelsallianz",
+
+	// few strings from WoG used by vcmi
+	"vcmi.stackExperience.description" : "» D e t a i l s   z u r   S t a p e l e r f a h r u n g «\n\nKreatur-Typ ................... : %s\nErfahrungsrang ................. : %s (%i)\nErfahrungspunkte ............... : %i\nErfahrungspunkte für den nächsten Rang .. : %i\nMaximale Erfahrung pro Kampf ... : %i%% (%i)\nAnzahl der Kreaturen im Stapel .... : %i\nMaximale Anzahl neuer Rekruten\n ohne Verlust von aktuellem Rang .... : %i\nErfahrungs-Multiplikator ........... : %.2f\nUpgrade-Multiplikator .............. : %.2f\nErfahrung nach Rang 10 ........ : %i\nMaximale Anzahl der neuen Rekruten, die bei\n Rang 10 bei maximaler Erfahrung übrig sind : %i",
+	"vcmi.stackExperience.rank.0" : "Grundlagen",
+	"vcmi.stackExperience.rank.1" : "Neuling",
+	"vcmi.stackExperience.rank.2" : "Ausgebildet",
+	"vcmi.stackExperience.rank.3" : "Kompetent",
+	"vcmi.stackExperience.rank.4" : "Bewährt",
+	"vcmi.stackExperience.rank.5" : "Veteran",
+	"vcmi.stackExperience.rank.6" : "Gekonnt",
+	"vcmi.stackExperience.rank.7" : "Experte",
+	"vcmi.stackExperience.rank.8" : "Elite",
+	"vcmi.stackExperience.rank.9" : "Meister",
+	"vcmi.stackExperience.rank.10" : "Ass",
+	
+	"core.bonus.ADDITIONAL_ATTACK.name": "Doppelschlag",
+	"core.bonus.ADDITIONAL_ATTACK.description": "Greift zweimal an",
+	"core.bonus.ADDITIONAL_RETALIATION.name": "Zusätzliche Vergeltungsmaßnahmen",
+	"core.bonus.ADDITIONAL_RETALIATION.description": "Kann ${val} zusätzliche Male vergelten",
+	"core.bonus.AIR_IMMUNITY.name": "Luftimmunität",
+	"core.bonus.AIR_IMMUNITY.description": "Immun gegen alle Luftschulzauber",
+	"core.bonus.ATTACKS_ALL_ADJACENT.name": "Rundum angreifen",
+	"core.bonus.ATTACKS_ALL_ADJACENT.description": "Greift alle benachbarten Gegner an",
+	"core.bonus.BLOCKS_RETALIATION.name": "Keine Vergeltung",
+	"core.bonus.BLOCKS_RETALIATION.description": "Feind kann nicht vergelten",
+	"core.bonus.BLOCKS_RANGED_RETALIATION.name": "Keine Reichweitenverschiebung",
+	"core.bonus.BLOCKS_RANGED_RETALIATION.description": "Feind kann nicht durch Schießen vergelten",
+	"core.bonus.CATAPULT.name": "Katapult",
+	"core.bonus.CATAPULT.description": "Greift Belagerungsmauern an",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Reduziere Zauberkosten (${val})",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Reduziert die Zauberkosten für den Helden",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Zauberdämpfer (${val})",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Erhöht die Kosten von gegnerischen Zaubern",
+	"core.bonus.CHARGE_IMMUNITY.name": "Immun gegen Aufladung",
+	"core.bonus.CHARGE_IMMUNITY.description": "Immun gegen Aufladung",
+	"core.bonus.DARKNESS.name": "Abdeckung der Dunkelheit",
+	"core.bonus.DARKNESS.description": "Fügt ${val} Dunkelheitsradius hinzu",
+	"core.bonus.DEATH_STARE.name": "Todesstarren (${val}%)",
+	"core.bonus.DEATH_STARE.description": "${val}% Chance, eine einzelne Kreatur zu töten",
+	"core.bonus.DEFENSIVE_STANCE.name": "Verteidigungsbonus",
+	"core.bonus.DEFENSIVE_STANCE.description": "+${val} Verteidigung beim Verteidigen",
+	"core.bonus.DESTRUCTION.name": "Zerstörung",
+	"core.bonus.DESTRUCTION.description": "Hat ${val}% Chance, zusätzliche Einheiten nach dem Angriff zu töten",
+	"core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Todesstoß",
+	"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.EARTH_IMMUNITY.name": "Erdimmunität",
+	"core.bonus.EARTH_IMMUNITY.description": "Immun gegen alle Zauber der Erdschule",
+	"core.bonus.ENCHANTER.name": "Verzauberer",
+	"core.bonus.ENCHANTER.description": "Kann jede Runde eine Masse von ${subtype.spell} zaubern",
+	"core.bonus.ENCHANTED.name": "Verzaubert",
+	"core.bonus.ENCHANTED.description": "Beeinflusst von permanentem ${subtype.spell}",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignoriere Verteidigung (${val}%)",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Ignoriert einen Teil der Verteidigung für den Angriff",
+	"core.bonus.FIRE_IMMUNITY.name": "Feuerimmunität",
+	"core.bonus.FIRE_IMMUNITY.description": "Immun gegen alle Zauber der Schule des Feuers",
+	"core.bonus.FIRE_SHIELD.name": "Feuerschild (${val}%)",
+	"core.bonus.FIRE_SHIELD.description": "Reflektiert einen Teil des Nahkampfschadens",
+	"core.bonus.FIRST_STRIKE.name": "Erstschlag",
+	"core.bonus.FIRST_STRIKE.description": "Diese Kreatur greift zuerst an, anstatt zu vergelten",
+	"core.bonus.FEAR.name": "Furcht",
+	"core.bonus.FEAR.description": "Verursacht Furcht bei einem gegnerischen Stapel",
+	"core.bonus.FEARLESS.name": "Furchtlos",
+	"core.bonus.FEARLESS.description": "immun gegen die Fähigkeit Furcht",
+	"core.bonus.FLYING.name": "Fliegen",
+	"core.bonus.FLYING.description": "Kann fliegen (ignoriert Hindernisse)",
+	"core.bonus.FREE_SHOOTING.name": "Nah schießen",
+	"core.bonus.FREE_SHOOTING.description": "Kann im Nahkampf schießen",
+	"core.bonus.GARGOYLE.name": "Gargoyle",
+	"core.bonus.GARGOYLE.description": "Kann nicht aufgerichtet oder geheilt werden",
+	"core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Schaden vermindern (${val}%)",
+	"core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Reduziert physischen Schaden aus dem Fern- oder Nahkampf",
+	"core.bonus.HATE.name": "Hasst ${subtype.creature}",
+	"core.bonus.HATE.description": "Macht ${val}% mehr Schaden",
+	"core.bonus.HEALER.name": "Heiler",
+	"core.bonus.HEALER.description": "Heilt verbündete Einheiten",
+	"core.bonus.HP_REGENERATION.name": "Regeneration",
+	"core.bonus.HP_REGENERATION.description": "Heilt ${val} Trefferpunkte jede Runde",
+	"core.bonus.JOUSTING.name": "Champion Charge",
+	"core.bonus.JOUSTING.description": "+${val}% Schaden pro zurückgelegtem Feld",
+	"core.bonus.KING.name": "König",
+	"core.bonus.KING.description": "Anfällig für SLAYER Level ${val} oder höher",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.name": "Zauberimmunität 1-${val}",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.description": "Immun gegen Zaubersprüche der Stufen 1-${val}",
+	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Begrenzte Schussweite",
+	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Kann nicht auf Ziele schießen, die weiter als ${val} Felder entfernt sind",
+	"core.bonus.LIFE_DRAIN.name": "Leben entziehen (${val}%)",
+	"core.bonus.LIFE_DRAIN.description": "Drainiert ${val}% des zugefügten Schadens",
+	"core.bonus.MANA_CHANNELING.name": "Magiekanal ${val}%",
+	"core.bonus.MANA_CHANNELING.description": "Gibt Ihrem Helden Mana, das vom Gegner ausgegeben wird",
+	"core.bonus.MANA_DRAIN.name": "Mana-Entzug",
+	"core.bonus.MANA_DRAIN.description": "Entzieht ${val} Mana jede Runde",
+	"core.bonus.MAGIC_MIRROR.name": "Zauberspiegel (${val}%)",
+	"core.bonus.MAGIC_MIRROR.description": "${val}% Chance, einen Angriffszauber auf den Gegner umzulenken",
+	"core.bonus.MAGIC_RESISTANCE.name": "Magie-Widerstand(${val}%)",
+	"core.bonus.MAGIC_RESISTANCE.description": "${val}% Chance, gegnerischem Zauber zu widerstehen",
+	"core.bonus.MIND_IMMUNITY.name": "Geist-Zauber-Immunität",
+	"core.bonus.MIND_IMMUNITY.description": "Immun gegen Zauber vom Typ Geist",
+	"core.bonus.NO_DISTANCE_PENALTY.name": "Keine Entfernungsstrafe",
+	"core.bonus.NO_DISTANCE_PENALTY.description": "Voller Schaden aus beliebiger Entfernung",
+	"core.bonus.NO_MELEE_PENALTY.name": "Keine Nahkampf-Strafe",
+	"core.bonus.NO_MELEE_PENALTY.description": "Kreatur hat keinen Nahkampf-Malus",
+	"core.bonus.NO_MORALE.name": "Neutrale Moral",
+	"core.bonus.NO_MORALE.description": "Kreatur ist immun gegen Moral-Effekte",
+	"core.bonus.NO_WALL_PENALTY.name": "Keine Wand-Strafe",
+	"core.bonus.NO_WALL_PENALTY.description": "Voller Schaden bei Belagerung",
+	"core.bonus.NON_LIVING.name": "Nicht lebend",
+	"core.bonus.NON_LIVING.description": "Immunität gegen viele Effekte",
+	"core.bonus.RANDOM_SPELLCASTER.name": "Zufälliger Zauberwirker",
+	"core.bonus.RANDOM_SPELLCASTER.description": "Kann einen zufälligen Zauberspruch wirken",
+	"core.bonus.RANGED_RETALIATION.name": "Fernkampf-Vergeltung",
+	"core.bonus.RANGED_RETALIATION.description": "Kann einen Fernkampf-Gegenangriff durchführen",
+	"core.bonus.RECEPTIVE.name": "Empfänglich",
+	"core.bonus.RECEPTIVE.description": "Keine Immunität gegen Freundschaftszauber",
+	"core.bonus.REBIRTH.name": "Wiedergeburt (${val}%)",
+	"core.bonus.REBIRTH.description": "${val}% des Stacks wird nach dem Tod auferstehen",
+	"core.bonus.RETURN_AFTER_STRIKE.name": "Angriff und Rückkehr",
+	"core.bonus.RETURN_AFTER_STRIKE.description": "Kehrt nach Nahkampfangriff zurück",
+	"core.bonus.SHOOTER.name": "Fernkämpfer",
+	"core.bonus.SHOOTER.description": "Kreatur kann schießen",
+	"core.bonus.SHOOTS_ALL_ADJACENT.name": "Schießt rundherum",
+	"core.bonus.SHOOTS_ALL_ADJACENT.description": "Die Fernkampfangriffe dieser Kreatur treffen alle Ziele in einem kleinen Bereich",
+	"core.bonus.SOUL_STEAL.name": "Seelenraub",
+	"core.bonus.SOUL_STEAL.description": "Gewinnt ${val} neue Kreaturen für jeden getöteten Gegner",
+	"core.bonus.SPELLCASTER.name": "Zauberer",
+	"core.bonus.SPELLCASTER.description": "Kann ${subtype.spell} zaubern",
+	"core.bonus.SPELL_AFTER_ATTACK.name": "Nach Angriff zaubern",
+	"core.bonus.SPELL_AFTER_ATTACK.description": "${val}%, um ${subtype.spell} nach dem Angriff zu wirken",
+	"core.bonus.SPELL_BEFORE_ATTACK.name": "Zauber vor Angriff",
+	"core.bonus.SPELL_BEFORE_ATTACK.description": "${val}% um ${subtype.spell} vor dem Angriff zu wirken",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "Zauberwiderstand",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description": "Schaden von Zaubern reduziert ${val}%.",
+	"core.bonus.SPELL_IMMUNITY.name": "Zauberimmunität",
+	"core.bonus.SPELL_IMMUNITY.description": "Immun gegen ${subtype.spell}",
+	"core.bonus.SPELL_LIKE_ATTACK.name": "zauberähnlicher Angriff",
+	"core.bonus.SPELL_LIKE_ATTACK.description": "Angriffe mit ${subtype.spell}",
+	"core.bonus.SPELL_RESISTANCE_AURA.name": "Aura des Widerstands",
+	"core.bonus.SPELL_RESISTANCE_AURA.description": "Stapel in der Nähe erhalten ${val}% Widerstand",
+	"core.bonus.SUMMON_GUARDIANS.name": "Wächter beschwören",
+	"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": "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",
+	"core.bonus.TRANSMUTATION.name": "Transmutation",
+	"core.bonus.TRANSMUTATION.description": "${val}% Chance, angegriffene Einheit in einen anderen Typ zu verwandeln",
+	"core.bonus.UNDEAD.name": "Untot",
+	"core.bonus.UNDEAD.description": "Kreatur ist untot",
+	"core.bonus.UNLIMITED_RETALIATIONS.name": "Unbegrenzte Vergeltungsmaßnahmen",
+	"core.bonus.UNLIMITED_RETALIATIONS.description": "Vergeltungen für eine beliebige Anzahl von Angriffen",
+	"core.bonus.WATER_IMMUNITY.name": "Wasser-Immunität",
+	"core.bonus.WATER_IMMUNITY.description": "Immun gegen alle Zauber der Wasserschule",
+	"core.bonus.WIDE_BREATH.name": "Breiter Atem",
+	"core.bonus.WIDE_BREATH.description": "Breiter Atem-Angriff (mehrere Felder)"
+}

+ 372 - 372
Mods/vcmi/config/vcmi/rmg/hdmod/blockbuster.JSON

@@ -1,372 +1,372 @@
-{
-	"Blockbuster M" :
-	//(ban fly/DD, 2 player, 15-Jun-03, midnight design)
-	{
-		"minSize" : "m", "maxSize" : "m",
-		"players" : "2",
-		"zones" :
-		{
-			"1" :
-			{
-				"type" : "playerStart",
-				"size" : 15,
-				"owner" : 1,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"bannedTowns" : ["necropolis", "conflux"],
-				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
-				"mines" : { "wood" : 1, "ore" : 1 },
-				"treasure" :
-				[
-					{ "min" : 3500, "max" : 6000, "density" : 4 },
-					{ "min" : 800, "max" : 2000, "density" : 12 }
-				]
-			},
-			"2" :
-			{
-				"type" : "playerStart",
-				"size" : 15,
-				"owner" : 2,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"bannedTowns" : ["necropolis", "conflux"],
-				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"3" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "dirt", "grass", "subterra", "lava" ],
-				"mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 },
-				"treasure" :
-				[
-					{ "min" : 8000, "max" : 9800, "density" : 3 },
-					{ "min" : 3500, "max" : 8000, "density" : 10 },
-					{ "min" : 800, "max" : 1200, "density" : 5 }
-				]
-			},
-			"4" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"minesLikeZone" : 3,
-				"treasureLikeZone" : 3
-			},
-			"5" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"treasure" :
-				[
-					{ "min" : 9000, "max" : 9800, "density" : 2 },
-					{ "min" : 3500, "max" : 8999, "density" : 8 },
-					{ "min" : 800, "max" : 1200, "density" : 1 }
-				]
-			},
-			"6" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"treasureLikeZone" : 5
-			},
-			"7" :
-			{
-				"type" : "treasure",
-				"size" : 25,
-				"monsters" : "strong",
-				"allowedTowns" : [ "necropolis" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "sand", "snow" ],
-				"mines" : { "gold" : 2 },
-				"treasure" :
-				[
-					{ "min" : 18000, "max" : 25000, "density" : 2 },
-					{ "min" : 9000, "max" : 9800, "density" : 4 },
-					{ "min" : 500, "max" : 3000, "density" : 3 }
-				]
-			}
-		},
-		"connections" :
-		[
-			{ "a" : "1", "b" : "2", "guard" : 50000 },
-			{ "a" : "1", "b" : "3", "guard" : 4000 },
-			{ "a" : "1", "b" : "5", "guard" : 8000 },
-			{ "a" : "2", "b" : "4", "guard" : 4000 },
-			{ "a" : "2", "b" : "6", "guard" : 8000 },
-			{ "a" : "5", "b" : "7", "guard" : 16000 },
-			{ "a" : "6", "b" : "7", "guard" : 16000 }
-		]
-	},
-	"Blockbuster L" :
-	{
-		"minSize" : "l", "maxSize" : "l",
-		"players" : "2",
-		"zones" :
-		{
-			"1" :
-			{
-				"type" : "playerStart",
-				"size" : 20,
-				"owner" : 1,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"bannedTowns" : ["necropolis", "conflux"],
-				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
-				"mines" : { "wood" : 1, "ore" : 1 },
-				"treasure" :
-				[
-					{ "min" : 3500, "max" : 5500, "density" : 5 },
-					{ "min" : 1000, "max" : 2000, "density" : 5 },
-					{ "min" : 320, "max" : 1000, "density" : 3 }
-				]
-			},
-			"2" :
-			{
-				"type" : "playerStart",
-				"size" : 20,
-				"owner" : 2,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"bannedTowns" : ["necropolis", "conflux"],
-				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"3" :
-			{
-				"type" : "treasure",
-				"size" : 6,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "dirt", "grass", "subterra", "lava" ],
-				"mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 },
-				"treasure" :
-				[
-					{ "min" : 8000, "max" : 9100, "density" : 4 },
-					{ "min" : 3500, "max" : 8000, "density" : 5 },
-					{ "min" : 800, "max" : 2000, "density" : 7 }
-				]
-			},
-			"4" :
-			{
-				"type" : "treasure",
-				"size" : 6,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"minesLikeZone" : 3,
-				"treasureLikeZone" : 3
-			},
-			"5" :
-			{
-				"type" : "treasure",
-				"size" : 6,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"treasure" :
-				[
-					{ "min" : 20000, "max" : 29000, "density" : 1 },
-					{ "min" : 6000, "max" : 9300, "density" : 8 },
-					{ "min" : 800, "max" : 1200, "density" : 2 }
-				]
-			},
-			"6" :
-			{
-				"type" : "treasure",
-				"size" : 6,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"treasureLikeZone" : 5
-			},
-			"7" :
-			{
-				"type" : "treasure",
-				"size" : 25,
-				"monsters" : "strong",
-				"allowedTowns" : [ "necropolis" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "sand", "snow" ],
-				"mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 },
-				"treasure" :
-				[
-					{ "min" : 18000, "max" : 25000, "density" : 2 },
-					{ "min" : 0, "max" : 45000, "density" : 6 },
-					{ "min" : 8000, "max" : 9300, "density" : 3 }
-				]
-			},
-			"8" :
-			{
-				"type" : "treasure",
-				"size" : 25,
-				"monsters" : "strong",
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 7,
-				"minesLikeZone" : 7,
-				"treasureLikeZone" : 7
-			}
-		},
-		"connections" :
-		[
-			{ "a" : "1", "b" : "2", "guard" : 110000 },
-			{ "a" : "1", "b" : "3", "guard" : 4500 },
-			{ "a" : "1", "b" : "5", "guard" : 15000 },
-			{ "a" : "2", "b" : "4", "guard" : 4500 },
-			{ "a" : "2", "b" : "6", "guard" : 15000 },
-			{ "a" : "5", "b" : "7", "guard" : 24000 },
-			{ "a" : "6", "b" : "8", "guard" : 24000 },
-			{ "a" : "7", "b" : "8", "guard" : 40000 }
-		]
-	},
-	"Blockbuster XL" :
-	{
-		"minSize" : "xl", "maxSize" : "xl",
-		"players" : "2",
-		"zones" :
-		{
-			"1" :
-			{
-				"type" : "playerStart",
-				"size" : 20,
-				"owner" : 1,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"bannedTowns" : ["necropolis", "conflux"],
-				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
-				"mines" : { "wood" : 1, "ore" : 1 },
-				"treasure" :
-				[
-					{ "min" : 3500, "max" : 5500, "density" : 4 },
-					{ "min" : 1000, "max" : 2000, "density" : 3 },
-					{ "min" : 300, "max" : 1000, "density" : 2 }
-				]
-			},
-			"2" :
-			{
-				"type" : "playerStart",
-				"size" : 20,
-				"owner" : 2,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"bannedTowns" : ["necropolis", "conflux"],
-				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"3" :
-			{
-				"type" : "treasure",
-				"size" : 4,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "dirt", "grass", "subterra", "lava" ],
-				"mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 },
-				"treasure" :
-				[
-					{ "min" : 8000, "max" : 9100, "density" : 3 },
-					{ "min" : 3500, "max" : 8000, "density" : 4 },
-					{ "min" : 800, "max" : 2000, "density" : 6 }
-				]
-			},
-			"4" :
-			{
-				"type" : "treasure",
-				"size" : 4,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"minesLikeZone" : 3,
-				"treasureLikeZone" : 3
-			},
-			"5" :
-			{
-				"type" : "treasure",
-				"size" : 4,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"treasure" :
-				[
-					{ "min" : 20000, "max" : 29000, "density" : 1 },
-					{ "min" : 6000, "max" : 9200, "density" : 6 },
-					{ "min" : 800, "max" : 2000, "density" : 2 }
-				]
-			},
-			"6" :
-			{
-				"type" : "treasure",
-				"size" : 4,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"treasureLikeZone" : 5
-			},
-			"7" :
-			{
-				"type" : "treasure",
-				"size" : 20,
-				"monsters" : "strong",
-				"allowedTowns" : [ "necropolis" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "sand", "snow" ],
-				"mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 3 },
-				"treasure" :
-				[
-					{ "min" : 28000, "max" : 29000, "density" : 1 },
-					{ "min" : 0, "max" : 50000, "density" : 5 },
-					{ "min" : 7500, "max" : 9200, "density" : 3 }
-				]
-			},
-			"8" :
-			{
-				"type" : "treasure",
-				"size" : 20,
-				"monsters" : "strong",
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 7,
-				"minesLikeZone" : 7,
-				"treasureLikeZone" : 7
-			}
-		},
-		"connections" :
-		[
-			{ "a" : "1", "b" : "2", "guard" : 140000 },
-			{ "a" : "1", "b" : "3", "guard" : 5000 },
-			{ "a" : "1", "b" : "5", "guard" : 17000 },
-			{ "a" : "2", "b" : "4", "guard" : 5000 },
-			{ "a" : "2", "b" : "6", "guard" : 17000 },
-			{ "a" : "5", "b" : "7", "guard" : 30000 },
-			{ "a" : "6", "b" : "8", "guard" : 30000 },
-			{ "a" : "7", "b" : "8", "guard" : 50000 }
-		]
-	}
-}
+{
+	"Blockbuster M" :
+	//(ban fly/DD, 2 player, 15-Jun-03, midnight design)
+	{
+		"minSize" : "m", "maxSize" : "m",
+		"players" : "2",
+		"zones" :
+		{
+			"1" :
+			{
+				"type" : "playerStart",
+				"size" : 15,
+				"owner" : 1,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"bannedTowns" : ["necropolis", "conflux"],
+				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
+				"mines" : { "wood" : 1, "ore" : 1 },
+				"treasure" :
+				[
+					{ "min" : 3500, "max" : 6000, "density" : 4 },
+					{ "min" : 800, "max" : 2000, "density" : 12 }
+				]
+			},
+			"2" :
+			{
+				"type" : "playerStart",
+				"size" : 15,
+				"owner" : 2,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"bannedTowns" : ["necropolis", "conflux"],
+				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"3" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "dirt", "grass", "subterra", "lava" ],
+				"mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 },
+				"treasure" :
+				[
+					{ "min" : 8000, "max" : 9800, "density" : 3 },
+					{ "min" : 3500, "max" : 8000, "density" : 10 },
+					{ "min" : 800, "max" : 1200, "density" : 5 }
+				]
+			},
+			"4" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"minesLikeZone" : 3,
+				"treasureLikeZone" : 3
+			},
+			"5" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"treasure" :
+				[
+					{ "min" : 9000, "max" : 9800, "density" : 2 },
+					{ "min" : 3500, "max" : 8999, "density" : 8 },
+					{ "min" : 800, "max" : 1200, "density" : 1 }
+				]
+			},
+			"6" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"treasureLikeZone" : 5
+			},
+			"7" :
+			{
+				"type" : "treasure",
+				"size" : 25,
+				"monsters" : "strong",
+				"allowedTowns" : [ "necropolis" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "sand", "snow" ],
+				"mines" : { "gold" : 2 },
+				"treasure" :
+				[
+					{ "min" : 18000, "max" : 25000, "density" : 2 },
+					{ "min" : 9000, "max" : 9800, "density" : 4 },
+					{ "min" : 500, "max" : 3000, "density" : 3 }
+				]
+			}
+		},
+		"connections" :
+		[
+			{ "a" : "1", "b" : "2", "guard" : 50000 },
+			{ "a" : "1", "b" : "3", "guard" : 4000 },
+			{ "a" : "1", "b" : "5", "guard" : 8000 },
+			{ "a" : "2", "b" : "4", "guard" : 4000 },
+			{ "a" : "2", "b" : "6", "guard" : 8000 },
+			{ "a" : "5", "b" : "7", "guard" : 16000 },
+			{ "a" : "6", "b" : "7", "guard" : 16000 }
+		]
+	},
+	"Blockbuster L" :
+	{
+		"minSize" : "l", "maxSize" : "l",
+		"players" : "2",
+		"zones" :
+		{
+			"1" :
+			{
+				"type" : "playerStart",
+				"size" : 20,
+				"owner" : 1,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"bannedTowns" : ["necropolis", "conflux"],
+				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
+				"mines" : { "wood" : 1, "ore" : 1 },
+				"treasure" :
+				[
+					{ "min" : 3500, "max" : 5500, "density" : 5 },
+					{ "min" : 1000, "max" : 2000, "density" : 5 },
+					{ "min" : 320, "max" : 1000, "density" : 3 }
+				]
+			},
+			"2" :
+			{
+				"type" : "playerStart",
+				"size" : 20,
+				"owner" : 2,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"bannedTowns" : ["necropolis", "conflux"],
+				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"3" :
+			{
+				"type" : "treasure",
+				"size" : 6,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "dirt", "grass", "subterra", "lava" ],
+				"mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 },
+				"treasure" :
+				[
+					{ "min" : 8000, "max" : 9100, "density" : 4 },
+					{ "min" : 3500, "max" : 8000, "density" : 5 },
+					{ "min" : 800, "max" : 2000, "density" : 7 }
+				]
+			},
+			"4" :
+			{
+				"type" : "treasure",
+				"size" : 6,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"minesLikeZone" : 3,
+				"treasureLikeZone" : 3
+			},
+			"5" :
+			{
+				"type" : "treasure",
+				"size" : 6,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"treasure" :
+				[
+					{ "min" : 20000, "max" : 29000, "density" : 1 },
+					{ "min" : 6000, "max" : 9300, "density" : 8 },
+					{ "min" : 800, "max" : 1200, "density" : 2 }
+				]
+			},
+			"6" :
+			{
+				"type" : "treasure",
+				"size" : 6,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"treasureLikeZone" : 5
+			},
+			"7" :
+			{
+				"type" : "treasure",
+				"size" : 25,
+				"monsters" : "strong",
+				"allowedTowns" : [ "necropolis" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "sand", "snow" ],
+				"mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 },
+				"treasure" :
+				[
+					{ "min" : 18000, "max" : 25000, "density" : 2 },
+					{ "min" : 0, "max" : 45000, "density" : 6 },
+					{ "min" : 8000, "max" : 9300, "density" : 3 }
+				]
+			},
+			"8" :
+			{
+				"type" : "treasure",
+				"size" : 25,
+				"monsters" : "strong",
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 7,
+				"minesLikeZone" : 7,
+				"treasureLikeZone" : 7
+			}
+		},
+		"connections" :
+		[
+			{ "a" : "1", "b" : "2", "guard" : 110000 },
+			{ "a" : "1", "b" : "3", "guard" : 4500 },
+			{ "a" : "1", "b" : "5", "guard" : 15000 },
+			{ "a" : "2", "b" : "4", "guard" : 4500 },
+			{ "a" : "2", "b" : "6", "guard" : 15000 },
+			{ "a" : "5", "b" : "7", "guard" : 24000 },
+			{ "a" : "6", "b" : "8", "guard" : 24000 },
+			{ "a" : "7", "b" : "8", "guard" : 40000 }
+		]
+	},
+	"Blockbuster XL" :
+	{
+		"minSize" : "xl", "maxSize" : "xl",
+		"players" : "2",
+		"zones" :
+		{
+			"1" :
+			{
+				"type" : "playerStart",
+				"size" : 20,
+				"owner" : 1,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"bannedTowns" : ["necropolis", "conflux"],
+				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
+				"mines" : { "wood" : 1, "ore" : 1 },
+				"treasure" :
+				[
+					{ "min" : 3500, "max" : 5500, "density" : 4 },
+					{ "min" : 1000, "max" : 2000, "density" : 3 },
+					{ "min" : 300, "max" : 1000, "density" : 2 }
+				]
+			},
+			"2" :
+			{
+				"type" : "playerStart",
+				"size" : 20,
+				"owner" : 2,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"bannedTowns" : ["necropolis", "conflux"],
+				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"3" :
+			{
+				"type" : "treasure",
+				"size" : 4,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "dirt", "grass", "subterra", "lava" ],
+				"mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 },
+				"treasure" :
+				[
+					{ "min" : 8000, "max" : 9100, "density" : 3 },
+					{ "min" : 3500, "max" : 8000, "density" : 4 },
+					{ "min" : 800, "max" : 2000, "density" : 6 }
+				]
+			},
+			"4" :
+			{
+				"type" : "treasure",
+				"size" : 4,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"minesLikeZone" : 3,
+				"treasureLikeZone" : 3
+			},
+			"5" :
+			{
+				"type" : "treasure",
+				"size" : 4,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"treasure" :
+				[
+					{ "min" : 20000, "max" : 29000, "density" : 1 },
+					{ "min" : 6000, "max" : 9200, "density" : 6 },
+					{ "min" : 800, "max" : 2000, "density" : 2 }
+				]
+			},
+			"6" :
+			{
+				"type" : "treasure",
+				"size" : 4,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"treasureLikeZone" : 5
+			},
+			"7" :
+			{
+				"type" : "treasure",
+				"size" : 20,
+				"monsters" : "strong",
+				"allowedTowns" : [ "necropolis" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "sand", "snow" ],
+				"mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 3 },
+				"treasure" :
+				[
+					{ "min" : 28000, "max" : 29000, "density" : 1 },
+					{ "min" : 0, "max" : 50000, "density" : 5 },
+					{ "min" : 7500, "max" : 9200, "density" : 3 }
+				]
+			},
+			"8" :
+			{
+				"type" : "treasure",
+				"size" : 20,
+				"monsters" : "strong",
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 7,
+				"minesLikeZone" : 7,
+				"treasureLikeZone" : 7
+			}
+		},
+		"connections" :
+		[
+			{ "a" : "1", "b" : "2", "guard" : 140000 },
+			{ "a" : "1", "b" : "3", "guard" : 5000 },
+			{ "a" : "1", "b" : "5", "guard" : 17000 },
+			{ "a" : "2", "b" : "4", "guard" : 5000 },
+			{ "a" : "2", "b" : "6", "guard" : 17000 },
+			{ "a" : "5", "b" : "7", "guard" : 30000 },
+			{ "a" : "6", "b" : "8", "guard" : 30000 },
+			{ "a" : "7", "b" : "8", "guard" : 50000 }
+		]
+	}
+}

+ 239 - 239
Mods/vcmi/config/vcmi/rmg/hdmod/coldshadowsFantasy.json

@@ -1,239 +1,239 @@
-{
-	"Coldshadow's Fantasy":
-	{
-		"minSize" : "xl+u", "maxSize" : "g+u",
-		"players" : "4-8", "cpu" : "3-6",
-		"zones":
-		{
-			"1":
-			{
-				"type" : "playerStart", "size" : 30, "owner" : 1,
-				"playerTowns" : { "castles" : 1 },
-				"neutralTowns" : { "towns" : 1 },
-				"monsters" : "normal",
-				"mines" : {"wood" : 2, "ore" : 2, "gems" : 1, "crystal" : 1, "sulfur" : 1, "mercury" : 1, "gold" : 1},
-				"treasure" : [
-								{"min" : 7500, "max": 25000, "density": 4},
-								{"min" : 3000, "max": 9000, "density": 6},
-								{"min" : 300, "max": 3000, "density": 8}
-							]
-			},
-			"2":
-			{
-				"type" : "cpuStart", "size" : 30, "owner" : 2,
-				"playerTowns" : { "castles" : 1 },
-				"neutralTowns" : { "towns" : 1 },
-				"monsters" : "weak",
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"3":
-			{
-				"type" : "cpuStart", "size" : 30, "owner" : 3,
-				"playerTowns" : { "castles" : 1 },
-				"neutralTowns" : { "towns" : 1 },
-				"monsters" : "weak",
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"4":
-			{
-				"type" : "cpuStart", "size" : 30, "owner" : 4,
-				"playerTowns" : { "castles" : 1 },
-				"neutralTowns" : { "towns" : 1 },
-				"monsters" : "weak",
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"5":
-			{
-				"type" : "playerStart", "size" : 30, "owner" : 5,
-				"playerTowns" : { "castles" : 1 },
-				"neutralTowns" : { "towns" : 1 },
-				"monsters" : "normal",
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"6":
-			{
-				"type" : "cpuStart", "size" : 30, "owner" : 6,
-				"playerTowns" : { "castles" : 1 },
-				"neutralTowns" : { "towns" : 1 },
-				"monsters" : "weak",
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"7":
-			{
-				"type" : "cpuStart", "size" : 30, "owner" : 7,
-				"playerTowns" : { "castles" : 1 },
-				"neutralTowns" : { "towns" : 1 },
-				"monsters" : "weak",
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"8":
-			{
-				"type" : "cpuStart", "size" : 30, "owner" : 8,
-				"playerTowns" : { "castles" : 1 },
-				"neutralTowns" : { "towns" : 1 },
-				"monsters" : "weak",
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"9":
-			{
-				"type" : "treasure", "size" : 15,
-				"terrainTypes" : ["subterra"], "matchTerrainToTown" : false,
-				"neutralTowns" : { "castles" : 1 },
-				"monsters" : "strong",
-				"mines" : {"gems" : 1, "sulfur" : 1, "mercury" : 1, "crystal" : 1},
-				"treasure" : [
-								{"min" : 45000, "max": 75000, "density": 3},
-								{"min" : 15000, "max": 50000, "density": 3},
-								{"min" : 3080, "max": 12500, "density": 4}
-							]
-			},
-			"10":
-			{
-				"type" : "treasure", "size" : 15,
-				"terrainTypeLikeZone" : 9,
-				"neutralTowns" : { "castles" : 1 },
-				"monsters" : "normal",
-				"minesLikeZone" : 9,
-				"treasureLikeZone" : 9
-			},
-			"11":
-			{
-				"type" : "treasure", "size" : 15,
-				"terrainTypeLikeZone" : 9,
-				"neutralTowns" : { "castles" : 1 },
-				"monsters" : "normal",
-				"minesLikeZone" : 9,
-				"treasureLikeZone" : 9
-			},
-			"12":
-			{
-				"type" : "treasure", "size" : 15,
-				"terrainTypeLikeZone" : 9,
-				"neutralTowns" : { "castles" : 1 },
-				"monsters" : "normal",
-				"minesLikeZone" : 9,
-				"treasureLikeZone" : 9
-			},
-			"13":
-			{
-				"type" : "treasure", "size" : 15,
-				"terrainTypeLikeZone" : 9,
-				"neutralTowns" : { "castles" : 1 },
-				"monsters" : "strong",
-				"minesLikeZone" : 9,
-				"treasureLikeZone" : 9
-			},
-			"14":
-			{
-				"type" : "treasure", "size" : 15,
-				"terrainTypeLikeZone" : 9,
-				"neutralTowns" : { "castles" : 1 },
-				"monsters" : "normal",
-				"minesLikeZone" : 9,
-				"treasureLikeZone" : 9
-			},
-			"15":
-			{
-				"type" : "treasure", "size" : 15,
-				"terrainTypeLikeZone" : 9,
-				"neutralTowns" : { "castles" : 1 },
-				"monsters" : "normal",
-				"minesLikeZone" : 9,
-				"treasureLikeZone" : 9
-			},
-			"16":
-			{
-				"type" : "treasure", "size" : 15,
-				"terrainTypeLikeZone" : 9,
-				"neutralTowns" : { "castles" : 1 },
-				"monsters" : "normal",
-				"minesLikeZone" : 9,
-				"treasureLikeZone" : 9
-			},
-			"17":
-			{
-				"type" : "junction", "size" : 30,
-				"terrainTypeLikeZone" : 9,
-				"allowedTowns" : ["neutral"],
-				"monsters" : "strong",
-				"mines" : {"gold" : 1},
-				"treasure" : [
-								{"min" : 65000, "max": 100000, "density": 3},
-								{"min" : 50000, "max": 100000, "density": 3},
-								{"min" : 10000, "max": 15000, "density": 3}
-							]
-			},
-			"18":
-			{
-				"type" : "junction", "size" : 30,
-				"terrainTypeLikeZone" : 9,
-				"allowedTowns" : ["neutral"],
-				"monsters" : "strong",
-				"minesLikeZone" : 17,
-				"treasureLikeZone" : 17
-			},
-			"19":
-			{
-				"type" : "junction", "size" : 30,
-				"terrainTypeLikeZone" : 9,
-				"allowedTowns" : ["neutral"],
-				"monsters" : "strong",
-				"minesLikeZone" : 17,
-				"treasureLikeZone" : 17
-			},
-			"20":
-			{
-				"type" : "junction", "size" : 30,
-				"terrainTypeLikeZone" : 9,
-				"allowedTowns" : ["neutral"],
-				"monsters" : "strong",
-				"minesLikeZone" : 17,
-				"treasureLikeZone" : 17
-			},
-			"21":
-			{
-				"type" : "treasure", "size" : 20,
-				"terrainTypeLikeZone" : 9,
-				"neutralTowns" : { "castles" : 1 },
-				"monsters" : "strong",
-				"treasure" : [
-								{"min" : 100000, "max": 130000, "density": 3},
-								{"min" : 100000, "max": 150000, "density": 3},
-								{"min" : 20000, "max": 60000, "density": 3}
-							]
-			}
-		},
-		"connections" :
-		[
-			{ "a" : "1", "b" : "9", "guard" : 36000 },
-			{ "a" : "2", "b" : "10", "guard" : 12000 },
-			{ "a" : "3", "b" : "11", "guard" : 12000 },
-			{ "a" : "4", "b" : "12", "guard" : 12000 },
-			{ "a" : "5", "b" : "13", "guard" : 36000 },
-			{ "a" : "6", "b" : "14", "guard" : 12000 },
-			{ "a" : "7", "b" : "15", "guard" : 12000 },
-			{ "a" : "8", "b" : "16", "guard" : 12000 },
-			{ "a" : "9", "b" : "17", "guard" : 75000 },
-			{ "a" : "10", "b" : "17", "guard" : 25000 },
-			{ "a" : "11", "b" : "18", "guard" : 25000 },
-			{ "a" : "12", "b" : "18", "guard" : 25000 },
-			{ "a" : "13", "b" : "19", "guard" : 75000 },
-			{ "a" : "14", "b" : "19", "guard" : 25000 },
-			{ "a" : "15", "b" : "20", "guard" : 25000 },
-			{ "a" : "16", "b" : "20", "guard" : 25000 },
-			{ "a" : "17", "b" : "18", "guard" : 50000 },
-			{ "a" : "19", "b" : "20", "guard" : 50000 },
-			{ "a" : "17", "b" : "21", "guard" : 60000 },
-			{ "a" : "18", "b" : "21", "guard" : 60000 },
-			{ "a" : "19", "b" : "21", "guard" : 60000 },
-			{ "a" : "20", "b" : "21", "guard" : 60000 }
-		]
-	}
-}
+{
+	"Coldshadow's Fantasy":
+	{
+		"minSize" : "xl+u", "maxSize" : "g+u",
+		"players" : "4-8", "cpu" : "3-6",
+		"zones":
+		{
+			"1":
+			{
+				"type" : "playerStart", "size" : 30, "owner" : 1,
+				"playerTowns" : { "castles" : 1 },
+				"neutralTowns" : { "towns" : 1 },
+				"monsters" : "normal",
+				"mines" : {"wood" : 2, "ore" : 2, "gems" : 1, "crystal" : 1, "sulfur" : 1, "mercury" : 1, "gold" : 1},
+				"treasure" : [
+								{"min" : 7500, "max": 25000, "density": 4},
+								{"min" : 3000, "max": 9000, "density": 6},
+								{"min" : 300, "max": 3000, "density": 8}
+							]
+			},
+			"2":
+			{
+				"type" : "cpuStart", "size" : 30, "owner" : 2,
+				"playerTowns" : { "castles" : 1 },
+				"neutralTowns" : { "towns" : 1 },
+				"monsters" : "weak",
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"3":
+			{
+				"type" : "cpuStart", "size" : 30, "owner" : 3,
+				"playerTowns" : { "castles" : 1 },
+				"neutralTowns" : { "towns" : 1 },
+				"monsters" : "weak",
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"4":
+			{
+				"type" : "cpuStart", "size" : 30, "owner" : 4,
+				"playerTowns" : { "castles" : 1 },
+				"neutralTowns" : { "towns" : 1 },
+				"monsters" : "weak",
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"5":
+			{
+				"type" : "playerStart", "size" : 30, "owner" : 5,
+				"playerTowns" : { "castles" : 1 },
+				"neutralTowns" : { "towns" : 1 },
+				"monsters" : "normal",
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"6":
+			{
+				"type" : "cpuStart", "size" : 30, "owner" : 6,
+				"playerTowns" : { "castles" : 1 },
+				"neutralTowns" : { "towns" : 1 },
+				"monsters" : "weak",
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"7":
+			{
+				"type" : "cpuStart", "size" : 30, "owner" : 7,
+				"playerTowns" : { "castles" : 1 },
+				"neutralTowns" : { "towns" : 1 },
+				"monsters" : "weak",
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"8":
+			{
+				"type" : "cpuStart", "size" : 30, "owner" : 8,
+				"playerTowns" : { "castles" : 1 },
+				"neutralTowns" : { "towns" : 1 },
+				"monsters" : "weak",
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"9":
+			{
+				"type" : "treasure", "size" : 15,
+				"terrainTypes" : ["subterra"], "matchTerrainToTown" : false,
+				"neutralTowns" : { "castles" : 1 },
+				"monsters" : "strong",
+				"mines" : {"gems" : 1, "sulfur" : 1, "mercury" : 1, "crystal" : 1},
+				"treasure" : [
+								{"min" : 45000, "max": 75000, "density": 3},
+								{"min" : 15000, "max": 50000, "density": 3},
+								{"min" : 3080, "max": 12500, "density": 4}
+							]
+			},
+			"10":
+			{
+				"type" : "treasure", "size" : 15,
+				"terrainTypeLikeZone" : 9,
+				"neutralTowns" : { "castles" : 1 },
+				"monsters" : "normal",
+				"minesLikeZone" : 9,
+				"treasureLikeZone" : 9
+			},
+			"11":
+			{
+				"type" : "treasure", "size" : 15,
+				"terrainTypeLikeZone" : 9,
+				"neutralTowns" : { "castles" : 1 },
+				"monsters" : "normal",
+				"minesLikeZone" : 9,
+				"treasureLikeZone" : 9
+			},
+			"12":
+			{
+				"type" : "treasure", "size" : 15,
+				"terrainTypeLikeZone" : 9,
+				"neutralTowns" : { "castles" : 1 },
+				"monsters" : "normal",
+				"minesLikeZone" : 9,
+				"treasureLikeZone" : 9
+			},
+			"13":
+			{
+				"type" : "treasure", "size" : 15,
+				"terrainTypeLikeZone" : 9,
+				"neutralTowns" : { "castles" : 1 },
+				"monsters" : "strong",
+				"minesLikeZone" : 9,
+				"treasureLikeZone" : 9
+			},
+			"14":
+			{
+				"type" : "treasure", "size" : 15,
+				"terrainTypeLikeZone" : 9,
+				"neutralTowns" : { "castles" : 1 },
+				"monsters" : "normal",
+				"minesLikeZone" : 9,
+				"treasureLikeZone" : 9
+			},
+			"15":
+			{
+				"type" : "treasure", "size" : 15,
+				"terrainTypeLikeZone" : 9,
+				"neutralTowns" : { "castles" : 1 },
+				"monsters" : "normal",
+				"minesLikeZone" : 9,
+				"treasureLikeZone" : 9
+			},
+			"16":
+			{
+				"type" : "treasure", "size" : 15,
+				"terrainTypeLikeZone" : 9,
+				"neutralTowns" : { "castles" : 1 },
+				"monsters" : "normal",
+				"minesLikeZone" : 9,
+				"treasureLikeZone" : 9
+			},
+			"17":
+			{
+				"type" : "junction", "size" : 30,
+				"terrainTypeLikeZone" : 9,
+				"allowedTowns" : ["neutral"],
+				"monsters" : "strong",
+				"mines" : {"gold" : 1},
+				"treasure" : [
+								{"min" : 65000, "max": 100000, "density": 3},
+								{"min" : 50000, "max": 100000, "density": 3},
+								{"min" : 10000, "max": 15000, "density": 3}
+							]
+			},
+			"18":
+			{
+				"type" : "junction", "size" : 30,
+				"terrainTypeLikeZone" : 9,
+				"allowedTowns" : ["neutral"],
+				"monsters" : "strong",
+				"minesLikeZone" : 17,
+				"treasureLikeZone" : 17
+			},
+			"19":
+			{
+				"type" : "junction", "size" : 30,
+				"terrainTypeLikeZone" : 9,
+				"allowedTowns" : ["neutral"],
+				"monsters" : "strong",
+				"minesLikeZone" : 17,
+				"treasureLikeZone" : 17
+			},
+			"20":
+			{
+				"type" : "junction", "size" : 30,
+				"terrainTypeLikeZone" : 9,
+				"allowedTowns" : ["neutral"],
+				"monsters" : "strong",
+				"minesLikeZone" : 17,
+				"treasureLikeZone" : 17
+			},
+			"21":
+			{
+				"type" : "treasure", "size" : 20,
+				"terrainTypeLikeZone" : 9,
+				"neutralTowns" : { "castles" : 1 },
+				"monsters" : "strong",
+				"treasure" : [
+								{"min" : 100000, "max": 130000, "density": 3},
+								{"min" : 100000, "max": 150000, "density": 3},
+								{"min" : 20000, "max": 60000, "density": 3}
+							]
+			}
+		},
+		"connections" :
+		[
+			{ "a" : "1", "b" : "9", "guard" : 36000 },
+			{ "a" : "2", "b" : "10", "guard" : 12000 },
+			{ "a" : "3", "b" : "11", "guard" : 12000 },
+			{ "a" : "4", "b" : "12", "guard" : 12000 },
+			{ "a" : "5", "b" : "13", "guard" : 36000 },
+			{ "a" : "6", "b" : "14", "guard" : 12000 },
+			{ "a" : "7", "b" : "15", "guard" : 12000 },
+			{ "a" : "8", "b" : "16", "guard" : 12000 },
+			{ "a" : "9", "b" : "17", "guard" : 75000 },
+			{ "a" : "10", "b" : "17", "guard" : 25000 },
+			{ "a" : "11", "b" : "18", "guard" : 25000 },
+			{ "a" : "12", "b" : "18", "guard" : 25000 },
+			{ "a" : "13", "b" : "19", "guard" : 75000 },
+			{ "a" : "14", "b" : "19", "guard" : 25000 },
+			{ "a" : "15", "b" : "20", "guard" : 25000 },
+			{ "a" : "16", "b" : "20", "guard" : 25000 },
+			{ "a" : "17", "b" : "18", "guard" : 50000 },
+			{ "a" : "19", "b" : "20", "guard" : 50000 },
+			{ "a" : "17", "b" : "21", "guard" : 60000 },
+			{ "a" : "18", "b" : "21", "guard" : 60000 },
+			{ "a" : "19", "b" : "21", "guard" : 60000 },
+			{ "a" : "20", "b" : "21", "guard" : 60000 }
+		]
+	}
+}

+ 362 - 362
Mods/vcmi/config/vcmi/rmg/hdmod/extreme.JSON

@@ -1,362 +1,362 @@
-{
-	"Extreme L" :
-	//(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design)
-	{
-		"minSize" : "l", "maxSize" : "l",
-		"players" : "2",
-		"zones" :
-		{
-			"1" :
-			{
-				"type" : "playerStart",
-				"size" : 30,
-				"owner" : 1,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"bannedTowns" : ["castle", "necropolis", "conflux"],
-				"mines" : { "wood" : 1, "ore" : 1, "gold" : 2 },
-				"treasure" :
-				[
-					{ "min" : 3400, "max" : 3500, "density" : 3 },
-					{ "min" : 1000, "max" : 2000, "density" : 10 },
-					{ "min" : 300, "max" : 1000, "density" : 2 }
-				]
-			},
-			"2" :
-			{
-				"type" : "playerStart",
-				"size" : 30,
-				"owner" : 2,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"bannedTowns" : ["castle", "necropolis", "conflux"],
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"3" :
-			{
-				"type" : "treasure",
-				"size" : 25,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "dirt", "grass", "subterra", "lava" ],
-				"mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 },
-				"treasure" :
-				[
-					{ "min" : 10000, "max" : 15000, "density" : 1 },
-					{ "min" : 3500, "max" : 9600, "density" : 8 },
-					{ "min" : 300, "max" : 1000, "density" : 4 }
-				]
-			},
-			"4" :
-			{
-				"type" : "treasure",
-				"size" : 25,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"minesLikeZone" : 3,
-				"treasureLikeZone" : 3
-			},
-			"5" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"mines" : { "wood" : 1 },
-				"treasure" :
-				[
-					{ "min" : 40000, "max" : 42000, "density" : 1 },
-					{ "min" : 25000, "max" : 27000, "density" : 2 },
-					{ "min" : 6000, "max" : 15000, "density" : 3 }
-				]
-			},
-			"6" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"minesLikeZone" : 5,
-				"treasureLikeZone" : 5
-			},
-			"7" :
-			{
-				"type" : "treasure",
-				"size" : 14,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"mines" : { "ore" : 1 },
-				"treasure" :
-				[
-					{ "min" : 30000, "max" : 60000, "density" : 1 },
-					{ "min" : 20000, "max" : 29000, "density" : 2 },
-					{ "min" : 3500, "max" : 20000, "density" : 3 }
-				]
-			},
-			"8" :
-			{
-				"type" : "treasure",
-				"size" : 14,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"minesLikeZone" : 7,
-				"treasureLikeZone" : 7
-			},
-			"9" :
-			{
-				"type" : "treasure",
-				"size" : 20,
-				"monsters" : "strong",
-				"allowedTowns" : [ "necropolis" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "sand" ],
-				"treasure" :
-				[
-					{ "min" : 115000, "max" : 120000, "density" : 1 },
-					{ "min" : 50000, "max" : 70000, "density" : 9 }
-				]
-			},
-			"10" :
-			{
-				"type" : "treasure",
-				"size" : 20,
-				"monsters" : "strong",
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 9,
-				"treasureLikeZone" : 9
-			},
-			"11" :
-			{
-				"type" : "treasure",
-				"size" : 20,
-				"monsters" : "strong",
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 9,
-				"treasureLikeZone" : 9
-			},
-			"12" :
-			{
-				"type" : "treasure",
-				"size" : 20,
-				"monsters" : "strong",
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 9,
-				"treasureLikeZone" : 9
-			}
-		},
-		"connections" :
-		[
-			{ "a" : "1", "b" : "3", "guard" : 6000 },
-			{ "a" : "1", "b" : "3", "guard" : 5500 },
-			{ "a" : "2", "b" : "4", "guard" : 6000 },
-			{ "a" : "2", "b" : "4", "guard" : 5500 },
-			{ "a" : "3", "b" : "5", "guard" : 15000 },
-			{ "a" : "3", "b" : "7", "guard" : 20000 },
-			{ "a" : "4", "b" : "6", "guard" : 15000 },
-			{ "a" : "4", "b" : "8", "guard" : 20000 },
-			{ "a" : "3", "b" : "11", "guard" : 50000 },
-			{ "a" : "4", "b" : "12", "guard" : 50000 },
-			{ "a" : "5", "b" : "9", "guard" : 40000 },
-			{ "a" : "7", "b" : "11", "guard" : 40000 },
-			{ "a" : "6", "b" : "10", "guard" : 40000 },
-			{ "a" : "8", "b" : "12", "guard" : 40000 },
-			{ "a" : "1", "b" : "9", "guard" : 50000 },
-			{ "a" : "2", "b" : "10", "guard" : 50000 },
-			{ "a" : "9", "b" : "12", "guard" : 100000 },
-			{ "a" : "10", "b" : "11", "guard" : 100000 }
-		]
-	},
-	"Extreme XL" :
-	//(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design)
-	{
-		"minSize" : "xl", "maxSize" : "xh",
-		"players" : "2",
-		"zones" :
-		{
-			"1" :
-			{
-				"type" : "playerStart",
-				"size" : 20,
-				"owner" : 1,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"bannedTowns" : ["castle", "necropolis", "conflux"],
-				"mines" : { "wood" : 1, "ore" : 1, "gold" : 2 },
-				"treasure" :
-				[
-					{ "min" : 3400, "max" : 3500, "density" : 3 },
-					{ "min" : 1000, "max" : 2000, "density" : 6 },
-					{ "min" : 300, "max" : 1000, "density" : 2 }
-				]
-			},
-			"2" :
-			{
-				"type" : "playerStart",
-				"size" : 20,
-				"owner" : 2,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"bannedTowns" : ["castle", "necropolis", "conflux"],
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"3" :
-			{
-				"type" : "treasure",
-				"size" : 15,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "dirt", "grass", "subterra", "lava" ],
-				"mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 },
-				"treasure" :
-				[
-					{ "min" : 10000, "max" : 15000, "density" : 1 },
-					{ "min" : 3500, "max" : 9500, "density" : 5 },
-					{ "min" : 300, "max" : 1000, "density" : 2 }
-				]
-			},
-			"4" :
-			{
-				"type" : "treasure",
-				"size" : 15,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"minesLikeZone" : 3,
-				"treasureLikeZone" : 3
-			},
-			"5" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"mines" : { "wood" : 1 },
-				"treasure" :
-				[
-					{ "min" : 40000, "max" : 42000, "density" : 1 },
-					{ "min" : 25000, "max" : 27000, "density" : 1 },
-					{ "min" : 6000, "max" : 15000, "density" : 2 }
-				]
-			},
-			"6" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"minesLikeZone" : 5,
-				"treasureLikeZone" : 5
-			},
-			"7" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"mines" : { "ore" : 1 },
-				"treasure" :
-				[
-					{ "min" : 30000, "max" : 60000, "density" : 1 },
-					{ "min" : 20000, "max" : 29000, "density" : 1 },
-					{ "min" : 3500, "max" : 20000, "density" : 2 }
-				]
-			},
-			"8" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"minesLikeZone" : 7,
-				"treasureLikeZone" : 7
-			},
-			"9" :
-			{
-				"type" : "treasure",
-				"size" : 17,
-				"monsters" : "strong",
-				"allowedTowns" : [ "necropolis" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "sand" ],
-				"treasure" :
-				[
-					{ "min" : 115000, "max" : 120000, "density" : 1 },
-					{ "min" : 50000, "max" : 80000, "density" : 6 }
-				]
-			},
-			"10" :
-			{
-				"type" : "treasure",
-				"size" : 17,
-				"monsters" : "strong",
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 9,
-				"treasureLikeZone" : 9
-			},
-			"11" :
-			{
-				"type" : "treasure",
-				"size" : 17,
-				"monsters" : "strong",
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 9,
-				"treasureLikeZone" : 9
-			},
-			"12" :
-			{
-				"type" : "treasure",
-				"size" : 17,
-				"monsters" : "strong",
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 9,
-				"treasureLikeZone" : 9
-			}
-		},
-		"connections" :
-		[
-			{ "a" : "1", "b" : "3", "guard" : 6500 },
-			{ "a" : "1", "b" : "3", "guard" : 6000 },
-			{ "a" : "2", "b" : "4", "guard" : 6500 },
-			{ "a" : "2", "b" : "4", "guard" : 6000 },
-			{ "a" : "3", "b" : "5", "guard" : 18000 },
-			{ "a" : "3", "b" : "7", "guard" : 22000 },
-			{ "a" : "4", "b" : "6", "guard" : 18000 },
-			{ "a" : "4", "b" : "8", "guard" : 22000 },
-			{ "a" : "3", "b" : "11", "guard" : 60000 },
-			{ "a" : "4", "b" : "12", "guard" : 60000 },
-			{ "a" : "5", "b" : "9", "guard" : 50000 },
-			{ "a" : "7", "b" : "11", "guard" : 50000 },
-			{ "a" : "6", "b" : "10", "guard" : 50000 },
-			{ "a" : "8", "b" : "12", "guard" : 50000 },
-			{ "a" : "1", "b" : "9", "guard" : 60000 },
-			{ "a" : "2", "b" : "10", "guard" : 60000 },
-			{ "a" : "9", "b" : "12", "guard" : 140000 },
-			{ "a" : "10", "b" : "11", "guard" : 140000 }
-		]
-	}
-}
+{
+	"Extreme L" :
+	//(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design)
+	{
+		"minSize" : "l", "maxSize" : "l",
+		"players" : "2",
+		"zones" :
+		{
+			"1" :
+			{
+				"type" : "playerStart",
+				"size" : 30,
+				"owner" : 1,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"bannedTowns" : ["castle", "necropolis", "conflux"],
+				"mines" : { "wood" : 1, "ore" : 1, "gold" : 2 },
+				"treasure" :
+				[
+					{ "min" : 3400, "max" : 3500, "density" : 3 },
+					{ "min" : 1000, "max" : 2000, "density" : 10 },
+					{ "min" : 300, "max" : 1000, "density" : 2 }
+				]
+			},
+			"2" :
+			{
+				"type" : "playerStart",
+				"size" : 30,
+				"owner" : 2,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"bannedTowns" : ["castle", "necropolis", "conflux"],
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"3" :
+			{
+				"type" : "treasure",
+				"size" : 25,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "dirt", "grass", "subterra", "lava" ],
+				"mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 },
+				"treasure" :
+				[
+					{ "min" : 10000, "max" : 15000, "density" : 1 },
+					{ "min" : 3500, "max" : 9600, "density" : 8 },
+					{ "min" : 300, "max" : 1000, "density" : 4 }
+				]
+			},
+			"4" :
+			{
+				"type" : "treasure",
+				"size" : 25,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"minesLikeZone" : 3,
+				"treasureLikeZone" : 3
+			},
+			"5" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"mines" : { "wood" : 1 },
+				"treasure" :
+				[
+					{ "min" : 40000, "max" : 42000, "density" : 1 },
+					{ "min" : 25000, "max" : 27000, "density" : 2 },
+					{ "min" : 6000, "max" : 15000, "density" : 3 }
+				]
+			},
+			"6" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"minesLikeZone" : 5,
+				"treasureLikeZone" : 5
+			},
+			"7" :
+			{
+				"type" : "treasure",
+				"size" : 14,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"mines" : { "ore" : 1 },
+				"treasure" :
+				[
+					{ "min" : 30000, "max" : 60000, "density" : 1 },
+					{ "min" : 20000, "max" : 29000, "density" : 2 },
+					{ "min" : 3500, "max" : 20000, "density" : 3 }
+				]
+			},
+			"8" :
+			{
+				"type" : "treasure",
+				"size" : 14,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"minesLikeZone" : 7,
+				"treasureLikeZone" : 7
+			},
+			"9" :
+			{
+				"type" : "treasure",
+				"size" : 20,
+				"monsters" : "strong",
+				"allowedTowns" : [ "necropolis" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "sand" ],
+				"treasure" :
+				[
+					{ "min" : 115000, "max" : 120000, "density" : 1 },
+					{ "min" : 50000, "max" : 70000, "density" : 9 }
+				]
+			},
+			"10" :
+			{
+				"type" : "treasure",
+				"size" : 20,
+				"monsters" : "strong",
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 9,
+				"treasureLikeZone" : 9
+			},
+			"11" :
+			{
+				"type" : "treasure",
+				"size" : 20,
+				"monsters" : "strong",
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 9,
+				"treasureLikeZone" : 9
+			},
+			"12" :
+			{
+				"type" : "treasure",
+				"size" : 20,
+				"monsters" : "strong",
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 9,
+				"treasureLikeZone" : 9
+			}
+		},
+		"connections" :
+		[
+			{ "a" : "1", "b" : "3", "guard" : 6000 },
+			{ "a" : "1", "b" : "3", "guard" : 5500 },
+			{ "a" : "2", "b" : "4", "guard" : 6000 },
+			{ "a" : "2", "b" : "4", "guard" : 5500 },
+			{ "a" : "3", "b" : "5", "guard" : 15000 },
+			{ "a" : "3", "b" : "7", "guard" : 20000 },
+			{ "a" : "4", "b" : "6", "guard" : 15000 },
+			{ "a" : "4", "b" : "8", "guard" : 20000 },
+			{ "a" : "3", "b" : "11", "guard" : 50000 },
+			{ "a" : "4", "b" : "12", "guard" : 50000 },
+			{ "a" : "5", "b" : "9", "guard" : 40000 },
+			{ "a" : "7", "b" : "11", "guard" : 40000 },
+			{ "a" : "6", "b" : "10", "guard" : 40000 },
+			{ "a" : "8", "b" : "12", "guard" : 40000 },
+			{ "a" : "1", "b" : "9", "guard" : 50000 },
+			{ "a" : "2", "b" : "10", "guard" : 50000 },
+			{ "a" : "9", "b" : "12", "guard" : 100000 },
+			{ "a" : "10", "b" : "11", "guard" : 100000 }
+		]
+	},
+	"Extreme XL" :
+	//(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design)
+	{
+		"minSize" : "xl", "maxSize" : "xh",
+		"players" : "2",
+		"zones" :
+		{
+			"1" :
+			{
+				"type" : "playerStart",
+				"size" : 20,
+				"owner" : 1,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"bannedTowns" : ["castle", "necropolis", "conflux"],
+				"mines" : { "wood" : 1, "ore" : 1, "gold" : 2 },
+				"treasure" :
+				[
+					{ "min" : 3400, "max" : 3500, "density" : 3 },
+					{ "min" : 1000, "max" : 2000, "density" : 6 },
+					{ "min" : 300, "max" : 1000, "density" : 2 }
+				]
+			},
+			"2" :
+			{
+				"type" : "playerStart",
+				"size" : 20,
+				"owner" : 2,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"bannedTowns" : ["castle", "necropolis", "conflux"],
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"3" :
+			{
+				"type" : "treasure",
+				"size" : 15,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "dirt", "grass", "subterra", "lava" ],
+				"mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 },
+				"treasure" :
+				[
+					{ "min" : 10000, "max" : 15000, "density" : 1 },
+					{ "min" : 3500, "max" : 9500, "density" : 5 },
+					{ "min" : 300, "max" : 1000, "density" : 2 }
+				]
+			},
+			"4" :
+			{
+				"type" : "treasure",
+				"size" : 15,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"minesLikeZone" : 3,
+				"treasureLikeZone" : 3
+			},
+			"5" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"mines" : { "wood" : 1 },
+				"treasure" :
+				[
+					{ "min" : 40000, "max" : 42000, "density" : 1 },
+					{ "min" : 25000, "max" : 27000, "density" : 1 },
+					{ "min" : 6000, "max" : 15000, "density" : 2 }
+				]
+			},
+			"6" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"minesLikeZone" : 5,
+				"treasureLikeZone" : 5
+			},
+			"7" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"mines" : { "ore" : 1 },
+				"treasure" :
+				[
+					{ "min" : 30000, "max" : 60000, "density" : 1 },
+					{ "min" : 20000, "max" : 29000, "density" : 1 },
+					{ "min" : 3500, "max" : 20000, "density" : 2 }
+				]
+			},
+			"8" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"minesLikeZone" : 7,
+				"treasureLikeZone" : 7
+			},
+			"9" :
+			{
+				"type" : "treasure",
+				"size" : 17,
+				"monsters" : "strong",
+				"allowedTowns" : [ "necropolis" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "sand" ],
+				"treasure" :
+				[
+					{ "min" : 115000, "max" : 120000, "density" : 1 },
+					{ "min" : 50000, "max" : 80000, "density" : 6 }
+				]
+			},
+			"10" :
+			{
+				"type" : "treasure",
+				"size" : 17,
+				"monsters" : "strong",
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 9,
+				"treasureLikeZone" : 9
+			},
+			"11" :
+			{
+				"type" : "treasure",
+				"size" : 17,
+				"monsters" : "strong",
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 9,
+				"treasureLikeZone" : 9
+			},
+			"12" :
+			{
+				"type" : "treasure",
+				"size" : 17,
+				"monsters" : "strong",
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 9,
+				"treasureLikeZone" : 9
+			}
+		},
+		"connections" :
+		[
+			{ "a" : "1", "b" : "3", "guard" : 6500 },
+			{ "a" : "1", "b" : "3", "guard" : 6000 },
+			{ "a" : "2", "b" : "4", "guard" : 6500 },
+			{ "a" : "2", "b" : "4", "guard" : 6000 },
+			{ "a" : "3", "b" : "5", "guard" : 18000 },
+			{ "a" : "3", "b" : "7", "guard" : 22000 },
+			{ "a" : "4", "b" : "6", "guard" : 18000 },
+			{ "a" : "4", "b" : "8", "guard" : 22000 },
+			{ "a" : "3", "b" : "11", "guard" : 60000 },
+			{ "a" : "4", "b" : "12", "guard" : 60000 },
+			{ "a" : "5", "b" : "9", "guard" : 50000 },
+			{ "a" : "7", "b" : "11", "guard" : 50000 },
+			{ "a" : "6", "b" : "10", "guard" : 50000 },
+			{ "a" : "8", "b" : "12", "guard" : 50000 },
+			{ "a" : "1", "b" : "9", "guard" : 60000 },
+			{ "a" : "2", "b" : "10", "guard" : 60000 },
+			{ "a" : "9", "b" : "12", "guard" : 140000 },
+			{ "a" : "10", "b" : "11", "guard" : 140000 }
+		]
+	}
+}

+ 310 - 310
Mods/vcmi/config/vcmi/rmg/hdmod/extreme2.JSON

@@ -1,310 +1,310 @@
-{
-	"Extreme II L":
-	//(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design)"
-	{
-		"minSize" : "l", "maxSize" : "l",
-		"players" : "2",
-		"zones" :
-		{
-			"1" :
-			{
-				"type" : "playerStart",
-				"size" : 120,
-				"owner" : 1,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"bannedTowns" : ["castle", "necropolis", "conflux"],
-				"mines" : { "wood" : 1, "ore" : 1, "gold" : 3 },
-				"treasure" :
-				[
-					{ "min" : 16000, "max" : 90000, "density" : 1 },
-					{ "min" : 300, "max" : 16000, "density" : 2 },
-					{ "min" : 370, "max" : 2000, "density" : 5 }
-				]
-			},
-			"2" :
-			{
-				"type" : "playerStart",
-				"size" : 120,
-				"owner" : 2,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"bannedTowns" : ["castle", "necropolis", "conflux"],
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"3" :
-			{
-				"type" : "treasure",
-				"size" : 22,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "grass", "subterra" ],
-				"mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 },
-				"treasure" :
-				[
-					{ "min" : 5000, "max" : 9000, "density" : 4 },
-					{ "min" : 300, "max" : 1000, "density" : 6 }
-				]
-			},
-			"4" :
-			{
-				"type" : "treasure",
-				"size" : 22,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "subterra", "lava" ],
-				"minesLikeZone" : 3,
-				"treasureLikeZone" : 3
-			},
-			"5" :
-			{
-				"type" : "treasure",
-				"size" : 18,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "sand" ],
-				"treasure" :
-				[
-					{ "min" : 70000, "max" : 90000, "density" : 6 },
-					{ "min" : 20000, "max" : 20000, "density" : 2 },
-					{ "min" : 300, "max" : 400, "density" : 1 }
-				]
-			},
-			"6" :
-			{
-				"type" : "treasure",
-				"size" : 18,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 5,
-				"treasureLikeZone" : 5
-			},
-			"7" :
-			{
-				"type" : "treasure",
-				"size" : 18,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 5,
-				"treasureLikeZone" : 5
-			},
-			"8" :
-			{
-				"type" : "treasure",
-				"size" : 18,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 5,
-				"treasureLikeZone" : 5
-			},
-			"9" :
-			{
-				"type" : "treasure",
-				"size" : 15,
-				"monsters" : "strong",
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 5,
-				"treasure" :
-				[
-					{ "min" : 90000, "max" : 120000, "density" : 6 },
-					{ "min" : 20000, "max" : 20000, "density" : 2 },
-					{ "min" : 300, "max" : 400, "density" : 1 }
-				]
-			},
-			"10" :
-			{
-				"type" : "treasure",
-				"size" : 15,
-				"monsters" : "strong",
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 5,
-				"treasureLikeZone" : 9
-			}
-		},
-		"connections" :
-		[
-			{ "a" : "1", "b" : "3", "guard" : 6500 },
-			{ "a" : "1", "b" : "3", "guard" : 6000 },
-			{ "a" : "1", "b" : "3", "guard" : 5500 },
-			{ "a" : "2", "b" : "4", "guard" : 6500 },
-			{ "a" : "2", "b" : "4", "guard" : 6000 },
-			{ "a" : "2", "b" : "4", "guard" : 5500 },
-			{ "a" : "3", "b" : "5", "guard" : 65000 },
-			{ "a" : "3", "b" : "7", "guard" : 65000 },
-			{ "a" : "4", "b" : "6", "guard" : 65000 },
-			{ "a" : "4", "b" : "8", "guard" : 65000 },
-			{ "a" : "5", "b" : "9", "guard" : 135000 },
-			{ "a" : "6", "b" : "9", "guard" : 135000 },
-			{ "a" : "7", "b" : "10", "guard" : 135000 },
-			{ "a" : "8", "b" : "10", "guard" : 135000 },
-			{ "a" : "3", "b" : "5", "guard" : 60000 },
-			{ "a" : "3", "b" : "7", "guard" : 60000 },
-			{ "a" : "4", "b" : "6", "guard" : 60000 },
-			{ "a" : "4", "b" : "8", "guard" : 60000 }
-		]
-	},
-	"Extreme II XL":
-	//(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design)
-	{
-		"minSize" : "xl", "maxSize" : "xh",
-		"players" : "2",
-		"zones" :
-		{
-			"1" :
-			{
-				"type" : "playerStart",
-				"size" : 90,
-				"owner" : 1,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"bannedTowns" : ["castle", "necropolis", "conflux"],
-				"mines" : { "wood" : 1, "ore" : 1, "gold" : 3 },
-				"treasure" :
-				[
-					{ "min" : 16000, "max" : 120000, "density" : 1 },
-					{ "min" : 300, "max" : 16000, "density" : 2 },
-					{ "min" : 370, "max" : 2000, "density" : 5 }
-				]
-			},
-			"2" :
-			{
-				"type" : "playerStart",
-				"size" : 90,
-				"owner" : 2,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"bannedTowns" : ["castle", "necropolis", "conflux"],
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"3" :
-			{
-				"type" : "treasure",
-				"size" : 20,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "grass", "subterra" ],
-				"mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 },
-				"treasure" :
-				[
-					{ "min" : 5000, "max" : 9000, "density" : 4 },
-					{ "min" : 300, "max" : 1000, "density" : 4 }
-				]
-			},
-			"4" :
-			{
-				"type" : "treasure",
-				"size" : 20,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "subterra", "lava" ],
-				"minesLikeZone" : 3,
-				"treasureLikeZone" : 3
-			},
-			"5" :
-			{
-				"type" : "treasure",
-				"size" : 15,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "sand" ],
-				"treasure" :
-				[
-					{ "min" : 85000, "max" : 100000, "density" : 4 },
-					{ "min" : 20000, "max" : 20000, "density" : 2 },
-					{ "min" : 300, "max" : 400, "density" : 1 }
-				]
-			},
-			"6" :
-			{
-				"type" : "treasure",
-				"size" : 15,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 5,
-				"treasureLikeZone" : 5
-			},
-			"7" :
-			{
-				"type" : "treasure",
-				"size" : 15,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 5,
-				"treasureLikeZone" : 5
-			},
-			"8" :
-			{
-				"type" : "treasure",
-				"size" : 15,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 5,
-				"treasureLikeZone" : 5
-			},
-			"9" :
-			{
-				"type" : "treasure",
-				"size" : 25,
-				"monsters" : "strong",
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 5,
-				"treasure" :
-				[
-					{ "min" : 115000, "max" : 120000, "density" : 4 },
-					{ "min" : 20000, "max" : 20000, "density" : 2 },
-					{ "min" : 300, "max" : 400, "density" : 1 }
-				]
-			},
-			"10" :
-			{
-				"type" : "treasure",
-				"size" : 25,
-				"monsters" : "strong",
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 5,
-				"treasureLikeZone" : 9
-			}
-		},
-		"connections" :
-		[
-			{ "a" : "1", "b" : "3", "guard" : 6500 },
-			{ "a" : "1", "b" : "3", "guard" : 6000 },
-			{ "a" : "1", "b" : "3", "guard" : 5500 },
-			{ "a" : "2", "b" : "4", "guard" : 6500 },
-			{ "a" : "2", "b" : "4", "guard" : 6000 },
-			{ "a" : "2", "b" : "4", "guard" : 5500 },
-			{ "a" : "3", "b" : "5", "guard" : 80000 },
-			{ "a" : "3", "b" : "7", "guard" : 80000 },
-			{ "a" : "4", "b" : "6", "guard" : 80000 },
-			{ "a" : "4", "b" : "8", "guard" : 80000 },
-			{ "a" : "5", "b" : "9", "guard" : 160000 },
-			{ "a" : "6", "b" : "9", "guard" : 160000 },
-			{ "a" : "7", "b" : "10", "guard" : 160000 },
-			{ "a" : "8", "b" : "10", "guard" : 160000 },
-			{ "a" : "3", "b" : "5", "guard" : 70000 },
-			{ "a" : "3", "b" : "7", "guard" : 70000 },
-			{ "a" : "4", "b" : "6", "guard" : 70000 },
-			{ "a" : "4", "b" : "8", "guard" : 70000 }
-		]
-	}
-}
+{
+	"Extreme II L":
+	//(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design)"
+	{
+		"minSize" : "l", "maxSize" : "l",
+		"players" : "2",
+		"zones" :
+		{
+			"1" :
+			{
+				"type" : "playerStart",
+				"size" : 120,
+				"owner" : 1,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"bannedTowns" : ["castle", "necropolis", "conflux"],
+				"mines" : { "wood" : 1, "ore" : 1, "gold" : 3 },
+				"treasure" :
+				[
+					{ "min" : 16000, "max" : 90000, "density" : 1 },
+					{ "min" : 300, "max" : 16000, "density" : 2 },
+					{ "min" : 370, "max" : 2000, "density" : 5 }
+				]
+			},
+			"2" :
+			{
+				"type" : "playerStart",
+				"size" : 120,
+				"owner" : 2,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"bannedTowns" : ["castle", "necropolis", "conflux"],
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"3" :
+			{
+				"type" : "treasure",
+				"size" : 22,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "grass", "subterra" ],
+				"mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 },
+				"treasure" :
+				[
+					{ "min" : 5000, "max" : 9000, "density" : 4 },
+					{ "min" : 300, "max" : 1000, "density" : 6 }
+				]
+			},
+			"4" :
+			{
+				"type" : "treasure",
+				"size" : 22,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "subterra", "lava" ],
+				"minesLikeZone" : 3,
+				"treasureLikeZone" : 3
+			},
+			"5" :
+			{
+				"type" : "treasure",
+				"size" : 18,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "sand" ],
+				"treasure" :
+				[
+					{ "min" : 70000, "max" : 90000, "density" : 6 },
+					{ "min" : 20000, "max" : 20000, "density" : 2 },
+					{ "min" : 300, "max" : 400, "density" : 1 }
+				]
+			},
+			"6" :
+			{
+				"type" : "treasure",
+				"size" : 18,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 5,
+				"treasureLikeZone" : 5
+			},
+			"7" :
+			{
+				"type" : "treasure",
+				"size" : 18,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 5,
+				"treasureLikeZone" : 5
+			},
+			"8" :
+			{
+				"type" : "treasure",
+				"size" : 18,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 5,
+				"treasureLikeZone" : 5
+			},
+			"9" :
+			{
+				"type" : "treasure",
+				"size" : 15,
+				"monsters" : "strong",
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 5,
+				"treasure" :
+				[
+					{ "min" : 90000, "max" : 120000, "density" : 6 },
+					{ "min" : 20000, "max" : 20000, "density" : 2 },
+					{ "min" : 300, "max" : 400, "density" : 1 }
+				]
+			},
+			"10" :
+			{
+				"type" : "treasure",
+				"size" : 15,
+				"monsters" : "strong",
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 5,
+				"treasureLikeZone" : 9
+			}
+		},
+		"connections" :
+		[
+			{ "a" : "1", "b" : "3", "guard" : 6500 },
+			{ "a" : "1", "b" : "3", "guard" : 6000 },
+			{ "a" : "1", "b" : "3", "guard" : 5500 },
+			{ "a" : "2", "b" : "4", "guard" : 6500 },
+			{ "a" : "2", "b" : "4", "guard" : 6000 },
+			{ "a" : "2", "b" : "4", "guard" : 5500 },
+			{ "a" : "3", "b" : "5", "guard" : 65000 },
+			{ "a" : "3", "b" : "7", "guard" : 65000 },
+			{ "a" : "4", "b" : "6", "guard" : 65000 },
+			{ "a" : "4", "b" : "8", "guard" : 65000 },
+			{ "a" : "5", "b" : "9", "guard" : 135000 },
+			{ "a" : "6", "b" : "9", "guard" : 135000 },
+			{ "a" : "7", "b" : "10", "guard" : 135000 },
+			{ "a" : "8", "b" : "10", "guard" : 135000 },
+			{ "a" : "3", "b" : "5", "guard" : 60000 },
+			{ "a" : "3", "b" : "7", "guard" : 60000 },
+			{ "a" : "4", "b" : "6", "guard" : 60000 },
+			{ "a" : "4", "b" : "8", "guard" : 60000 }
+		]
+	},
+	"Extreme II XL":
+	//(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design)
+	{
+		"minSize" : "xl", "maxSize" : "xh",
+		"players" : "2",
+		"zones" :
+		{
+			"1" :
+			{
+				"type" : "playerStart",
+				"size" : 90,
+				"owner" : 1,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"bannedTowns" : ["castle", "necropolis", "conflux"],
+				"mines" : { "wood" : 1, "ore" : 1, "gold" : 3 },
+				"treasure" :
+				[
+					{ "min" : 16000, "max" : 120000, "density" : 1 },
+					{ "min" : 300, "max" : 16000, "density" : 2 },
+					{ "min" : 370, "max" : 2000, "density" : 5 }
+				]
+			},
+			"2" :
+			{
+				"type" : "playerStart",
+				"size" : 90,
+				"owner" : 2,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"bannedTowns" : ["castle", "necropolis", "conflux"],
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"3" :
+			{
+				"type" : "treasure",
+				"size" : 20,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "grass", "subterra" ],
+				"mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 },
+				"treasure" :
+				[
+					{ "min" : 5000, "max" : 9000, "density" : 4 },
+					{ "min" : 300, "max" : 1000, "density" : 4 }
+				]
+			},
+			"4" :
+			{
+				"type" : "treasure",
+				"size" : 20,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "subterra", "lava" ],
+				"minesLikeZone" : 3,
+				"treasureLikeZone" : 3
+			},
+			"5" :
+			{
+				"type" : "treasure",
+				"size" : 15,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "sand" ],
+				"treasure" :
+				[
+					{ "min" : 85000, "max" : 100000, "density" : 4 },
+					{ "min" : 20000, "max" : 20000, "density" : 2 },
+					{ "min" : 300, "max" : 400, "density" : 1 }
+				]
+			},
+			"6" :
+			{
+				"type" : "treasure",
+				"size" : 15,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 5,
+				"treasureLikeZone" : 5
+			},
+			"7" :
+			{
+				"type" : "treasure",
+				"size" : 15,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 5,
+				"treasureLikeZone" : 5
+			},
+			"8" :
+			{
+				"type" : "treasure",
+				"size" : 15,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 5,
+				"treasureLikeZone" : 5
+			},
+			"9" :
+			{
+				"type" : "treasure",
+				"size" : 25,
+				"monsters" : "strong",
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 5,
+				"treasure" :
+				[
+					{ "min" : 115000, "max" : 120000, "density" : 4 },
+					{ "min" : 20000, "max" : 20000, "density" : 2 },
+					{ "min" : 300, "max" : 400, "density" : 1 }
+				]
+			},
+			"10" :
+			{
+				"type" : "treasure",
+				"size" : 25,
+				"monsters" : "strong",
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 5,
+				"treasureLikeZone" : 9
+			}
+		},
+		"connections" :
+		[
+			{ "a" : "1", "b" : "3", "guard" : 6500 },
+			{ "a" : "1", "b" : "3", "guard" : 6000 },
+			{ "a" : "1", "b" : "3", "guard" : 5500 },
+			{ "a" : "2", "b" : "4", "guard" : 6500 },
+			{ "a" : "2", "b" : "4", "guard" : 6000 },
+			{ "a" : "2", "b" : "4", "guard" : 5500 },
+			{ "a" : "3", "b" : "5", "guard" : 80000 },
+			{ "a" : "3", "b" : "7", "guard" : 80000 },
+			{ "a" : "4", "b" : "6", "guard" : 80000 },
+			{ "a" : "4", "b" : "8", "guard" : 80000 },
+			{ "a" : "5", "b" : "9", "guard" : 160000 },
+			{ "a" : "6", "b" : "9", "guard" : 160000 },
+			{ "a" : "7", "b" : "10", "guard" : 160000 },
+			{ "a" : "8", "b" : "10", "guard" : 160000 },
+			{ "a" : "3", "b" : "5", "guard" : 70000 },
+			{ "a" : "3", "b" : "7", "guard" : 70000 },
+			{ "a" : "4", "b" : "6", "guard" : 70000 },
+			{ "a" : "4", "b" : "8", "guard" : 70000 }
+		]
+	}
+}

+ 83 - 83
Mods/vcmi/config/vcmi/rmg/hdmod/poorJebus.JSON

@@ -1,83 +1,83 @@
-{
-	"Poor Jebus" :
-	//(made by Bjorn190, modified by Maretti and Angelito)
-	{
-		"minSize" : "m", "maxSize" : "xl+u",
-		"players" : "2-4",
-		"zones" :
-		{
-			"1" :
-			{
-				"type" : "playerStart",
-				"size" : 30,
-				"owner" : 1,
-				"monsters" : "weak",
-				"playerTowns" : { "castles" : 1 },
-				"neutralTowns" : { "towns" : 2 },
-				"mines" : { "wood" : 4, "mercury" : 1, "ore" : 4, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 },
-				"treasure" :
-				[
-					{ "min" : 12000, "max" : 22000, "density" : 1 },
-					{ "min" : 5000, "max" : 16000, "density" : 6 },
-					{ "min" : 400, "max" : 3000, "density" : 4 }
-				]
-			},
-			"2" :
-			{
-				"type" : "playerStart",
-				"size" : 30,
-				"owner" : 2,
-				"monsters" : "weak",
-				"playerTowns" : { "castles" : 1 },
-				"neutralTowns" : { "towns" : 2 },
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"3" :
-			{
-				"type" : "playerStart",
-				"size" : 30,
-				"owner" : 3,
-				"monsters" : "weak",
-				"playerTowns" : { "castles" : 1 },
-				"neutralTowns" : { "towns" : 2 },
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"4" :
-			{
-				"type" : "playerStart",
-				"size" : 30,
-				"owner" : 4,
-				"monsters" : "weak",
-				"playerTowns" : { "castles" : 1 },
-				"neutralTowns" : { "towns" : 2 },
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"5" :
-			{
-				"type" : "treasure",
-				"size" : 40,
-				"monsters" : "strong",
-				"neutralTowns" : { "castles" : 2 },
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "sand" ],
-				"mines" : { "gold" : 4 },
-				"treasure" :
-				[
-					{ "min" : 35000, "max" : 55000, "density" : 3 },
-					{ "min" : 25000, "max" : 35000, "density" : 10 },
-					{ "min" : 10000, "max" : 25000, "density" : 10 }
-				]
-			}
-		},
-		"connections" :
-		[
-			{ "a" : "1", "b" : "5", "guard" : 45000 },
-			{ "a" : "2", "b" : "5", "guard" : 45000 },
-			{ "a" : "3", "b" : "5", "guard" : 45000 },
-			{ "a" : "4", "b" : "5", "guard" : 45000 }
-		]
-	}
-}
+{
+	"Poor Jebus" :
+	//(made by Bjorn190, modified by Maretti and Angelito)
+	{
+		"minSize" : "m", "maxSize" : "xl+u",
+		"players" : "2-4",
+		"zones" :
+		{
+			"1" :
+			{
+				"type" : "playerStart",
+				"size" : 30,
+				"owner" : 1,
+				"monsters" : "weak",
+				"playerTowns" : { "castles" : 1 },
+				"neutralTowns" : { "towns" : 2 },
+				"mines" : { "wood" : 4, "mercury" : 1, "ore" : 4, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 },
+				"treasure" :
+				[
+					{ "min" : 12000, "max" : 22000, "density" : 1 },
+					{ "min" : 5000, "max" : 16000, "density" : 6 },
+					{ "min" : 400, "max" : 3000, "density" : 4 }
+				]
+			},
+			"2" :
+			{
+				"type" : "playerStart",
+				"size" : 30,
+				"owner" : 2,
+				"monsters" : "weak",
+				"playerTowns" : { "castles" : 1 },
+				"neutralTowns" : { "towns" : 2 },
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"3" :
+			{
+				"type" : "playerStart",
+				"size" : 30,
+				"owner" : 3,
+				"monsters" : "weak",
+				"playerTowns" : { "castles" : 1 },
+				"neutralTowns" : { "towns" : 2 },
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"4" :
+			{
+				"type" : "playerStart",
+				"size" : 30,
+				"owner" : 4,
+				"monsters" : "weak",
+				"playerTowns" : { "castles" : 1 },
+				"neutralTowns" : { "towns" : 2 },
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"5" :
+			{
+				"type" : "treasure",
+				"size" : 40,
+				"monsters" : "strong",
+				"neutralTowns" : { "castles" : 2 },
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "sand" ],
+				"mines" : { "gold" : 4 },
+				"treasure" :
+				[
+					{ "min" : 35000, "max" : 55000, "density" : 3 },
+					{ "min" : 25000, "max" : 35000, "density" : 10 },
+					{ "min" : 10000, "max" : 25000, "density" : 10 }
+				]
+			}
+		},
+		"connections" :
+		[
+			{ "a" : "1", "b" : "5", "guard" : 45000 },
+			{ "a" : "2", "b" : "5", "guard" : 45000 },
+			{ "a" : "3", "b" : "5", "guard" : 45000 },
+			{ "a" : "4", "b" : "5", "guard" : 45000 }
+		]
+	}
+}

+ 140 - 140
Mods/vcmi/config/vcmi/rmg/hdmod/reckless.JSON

@@ -1,140 +1,140 @@
-{
-	"Reckless" :
-	//(2 player, 6-Jan-03, midnight design)
-	{
-		"minSize" : "l", "maxSize" : "xl+u",
-		"players" : "2",
-		"zones" :
-		{
-			"1" :
-			{
-				"type" : "playerStart",
-				"size" : 30,
-				"owner" : 1,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"bannedTowns" : ["necropolis", "conflux"],
-				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
-				"mines" : { "wood" : 1, "ore" : 1 },
-				"treasure" :
-				[
-					{ "min" : 4500, "max" : 9800, "density" : 1 },
-					{ "min" : 3500, "max" : 4500, "density" : 5 },
-					{ "min" : 1500, "max" : 2000, "density" : 7 }
-				]
-			},
-			"2" :
-			{
-				"type" : "playerStart",
-				"size" : 30,
-				"owner" : 2,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"bannedTowns" : ["necropolis", "conflux"],
-				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"3" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
-				"mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1 },
-				"treasure" :
-				[
-					{ "min" : 20000, "max" : 25000, "density" : 1 },
-					{ "min" : 3500, "max" : 9800, "density" : 8 },
-					{ "min" : 800, "max" : 1500, "density" : 5 }
-				]
-			},
-			"4" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"mines" : { "sulfur" : 1, "crystal" : 1, "gems" : 1 },
-				"treasureLikeZone" : 3
-			},
-			"5" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "strong",
-				"terrainTypeLikeZone" : 3,
-				"mines" : { "mercury" : 1, "crystal" : 1, "gems" : 1 },
-				"treasureLikeZone" : 3
-			},
-			"6" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"minesLikeZone" : 3,
-				"treasureLikeZone" : 3
-			},
-			"7" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"minesLikeZone" : 4,
-				"treasureLikeZone" : 3
-			},
-			"8" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "strong",
-				"terrainTypeLikeZone" : 3,
-				"minesLikeZone" : 5,
-				"treasureLikeZone" : 3
-			},
-			"9" :
-			{
-				"type" : "treasure",
-				"size" : 50,
-				"monsters" : "strong",
-				"neutralTowns" : { "castles" : 4 },
-				"bannedTowns" : ["necropolis", "conflux"],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "sand" ],
-				"mines" : { "wood" : 2, "ore" : 2, "gold" : 3 },
-				"treasure" :
-				[
-					{ "min" : 40000, "max" : 60000, "density" : 1 },
-					{ "min" : 500, "max" : 29000, "density" : 12 },
-					{ "min" : 800, "max" : 2000, "density" : 8 }
-				]
-			}
-		},
-		"connections" :
-		[
-			{ "a" : "1", "b" : "3", "guard" : 5000 },
-			{ "a" : "1", "b" : "4", "guard" : 5000 },
-			{ "a" : "1", "b" : "5", "guard" : 5000 },
-			{ "a" : "2", "b" : "6", "guard" : 5000 },
-			{ "a" : "2", "b" : "7", "guard" : 5000 },
-			{ "a" : "2", "b" : "8", "guard" : 5000 },
-			{ "a" : "3", "b" : "9", "guard" : 11000 },
-			{ "a" : "4", "b" : "9", "guard" : 11000 },
-			{ "a" : "5", "b" : "9", "guard" : 11000 },
-			{ "a" : "6", "b" : "9", "guard" : 11000 },
-			{ "a" : "7", "b" : "9", "guard" : 11000 },
-			{ "a" : "8", "b" : "9", "guard" : 11000 }
-		]
-	}
-}
+{
+	"Reckless" :
+	//(2 player, 6-Jan-03, midnight design)
+	{
+		"minSize" : "l", "maxSize" : "xl+u",
+		"players" : "2",
+		"zones" :
+		{
+			"1" :
+			{
+				"type" : "playerStart",
+				"size" : 30,
+				"owner" : 1,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"bannedTowns" : ["necropolis", "conflux"],
+				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
+				"mines" : { "wood" : 1, "ore" : 1 },
+				"treasure" :
+				[
+					{ "min" : 4500, "max" : 9800, "density" : 1 },
+					{ "min" : 3500, "max" : 4500, "density" : 5 },
+					{ "min" : 1500, "max" : 2000, "density" : 7 }
+				]
+			},
+			"2" :
+			{
+				"type" : "playerStart",
+				"size" : 30,
+				"owner" : 2,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"bannedTowns" : ["necropolis", "conflux"],
+				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"3" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
+				"mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1 },
+				"treasure" :
+				[
+					{ "min" : 20000, "max" : 25000, "density" : 1 },
+					{ "min" : 3500, "max" : 9800, "density" : 8 },
+					{ "min" : 800, "max" : 1500, "density" : 5 }
+				]
+			},
+			"4" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"mines" : { "sulfur" : 1, "crystal" : 1, "gems" : 1 },
+				"treasureLikeZone" : 3
+			},
+			"5" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "strong",
+				"terrainTypeLikeZone" : 3,
+				"mines" : { "mercury" : 1, "crystal" : 1, "gems" : 1 },
+				"treasureLikeZone" : 3
+			},
+			"6" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"minesLikeZone" : 3,
+				"treasureLikeZone" : 3
+			},
+			"7" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"minesLikeZone" : 4,
+				"treasureLikeZone" : 3
+			},
+			"8" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "strong",
+				"terrainTypeLikeZone" : 3,
+				"minesLikeZone" : 5,
+				"treasureLikeZone" : 3
+			},
+			"9" :
+			{
+				"type" : "treasure",
+				"size" : 50,
+				"monsters" : "strong",
+				"neutralTowns" : { "castles" : 4 },
+				"bannedTowns" : ["necropolis", "conflux"],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "sand" ],
+				"mines" : { "wood" : 2, "ore" : 2, "gold" : 3 },
+				"treasure" :
+				[
+					{ "min" : 40000, "max" : 60000, "density" : 1 },
+					{ "min" : 500, "max" : 29000, "density" : 12 },
+					{ "min" : 800, "max" : 2000, "density" : 8 }
+				]
+			}
+		},
+		"connections" :
+		[
+			{ "a" : "1", "b" : "3", "guard" : 5000 },
+			{ "a" : "1", "b" : "4", "guard" : 5000 },
+			{ "a" : "1", "b" : "5", "guard" : 5000 },
+			{ "a" : "2", "b" : "6", "guard" : 5000 },
+			{ "a" : "2", "b" : "7", "guard" : 5000 },
+			{ "a" : "2", "b" : "8", "guard" : 5000 },
+			{ "a" : "3", "b" : "9", "guard" : 11000 },
+			{ "a" : "4", "b" : "9", "guard" : 11000 },
+			{ "a" : "5", "b" : "9", "guard" : 11000 },
+			{ "a" : "6", "b" : "9", "guard" : 11000 },
+			{ "a" : "7", "b" : "9", "guard" : 11000 },
+			{ "a" : "8", "b" : "9", "guard" : 11000 }
+		]
+	}
+}

+ 139 - 139
Mods/vcmi/config/vcmi/rmg/hdmod/roadrunner.JSON

@@ -1,139 +1,139 @@
-{
-	"Roadrunner" :
-	//(ban fly/DD, 2 player, 31-May-03, midnight design)
-	{
-		"minSize" : "xl", "maxSize" : "xl",
-		"players" : "2",
-		"zones" :
-		{
-			"1" :
-			{
-				"type" : "playerStart",
-				"size" : 15,
-				"owner" : 1,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"neutralTowns" : { "towns" : 1 },
-				"bannedTowns" : ["necropolis", "conflux"],
-				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
-				"mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 },
-				"treasure" :
-				[
-					{ "min" : 3500, "max" : 6000, "density" : 4 },
-					{ "min" : 300, "max" : 2000, "density" : 5 }
-				]
-			},
-			"2" :
-			{
-				"type" : "playerStart",
-				"size" : 15,
-				"owner" : 2,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"neutralTowns" : { "towns" : 1 },
-				"bannedTowns" : ["necropolis", "conflux"],
-				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"3" :
-			{
-				"type" : "treasure",
-				"size" : 25,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 2 },
-				"allowedTowns" : [ "necropolis" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "dirt", "grass", "subterra", "lava" ],
-				"mines" : { "gold" : 1 },
-				"treasure" :
-				[
-					{ "min" : 6000, "max" : 9500, "density" : 2 },
-					{ "min" : 3500, "max" : 6000, "density" : 5 },
-					{ "min" : 1000, "max" : 2000, "density" : 3 }
-				]
-			},
-			"4" :
-			{
-				"type" : "treasure",
-				"size" : 25,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 2 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"minesLikeZone" : 3,
-				"treasureLikeZone" : 3
-			},
-			"5" :
-			{
-				"type" : "treasure",
-				"size" : 40,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 2 },
-				"terrainTypeLikeZone" : 3,
-				"mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 },
-				"treasure" :
-				[
-					{ "min" : 45000, "max" : 50000, "density" : 1 },
-					{ "min" : 15000, "max" : 22000, "density" : 1 },
-					{ "min" : 8000, "max" : 9200, "density" : 12 }
-				]
-			},
-			"6" :
-			{
-				"type" : "treasure",
-				"size" : 40,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 2 },
-				"terrainTypeLikeZone" : 3,
-				"minesLikeZone" : 5,
-				"treasureLikeZone" : 5
-			},
-			"7" :
-			{
-				"type" : "treasure",
-				"size" : 20,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 2 },
-				"bannedTowns" : ["necropolis"],
-				"matchTerrainToTown" : false,
-				"treasure" :
-				[
-					{ "min" : 100000, "max" : 120000, "density" : 1 },
-					{ "min" : 25000, "max" : 29000, "density" : 1 },
-					{ "min" : 8000, "max" : 9200, "density" : 12 }
-				]
-			},
-			"8" :
-			{
-				"type" : "treasure",
-				"size" : 20,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 2 },
-				"bannedTowns" : ["necropolis"],
-				"matchTerrainToTown" : false,
-				"treasureLikeZone" : 7
-			}
-		},
-		"connections" :
-		[
-			{ "a" : "1", "b" : "3", "guard" : 0 },
-			{ "a" : "1", "b" : "3", "guard" : 0 },
-			{ "a" : "2", "b" : "4", "guard" : 0 },
-			{ "a" : "2", "b" : "4", "guard" : 0 },
-			{ "a" : "3", "b" : "5", "guard" : 0 },
-			{ "a" : "3", "b" : "5", "guard" : 0 },
-			{ "a" : "3", "b" : "5", "guard" : 0 },
-			{ "a" : "4", "b" : "6", "guard" : 0 },
-			{ "a" : "4", "b" : "6", "guard" : 0 },
-			{ "a" : "4", "b" : "6", "guard" : 0 },
-			{ "a" : "5", "b" : "7", "guard" : 0 },
-			{ "a" : "5", "b" : "7", "guard" : 0 },
-			{ "a" : "6", "b" : "8", "guard" : 0 },
-			{ "a" : "6", "b" : "8", "guard" : 0 },
-			{ "a" : "7", "b" : "8", "guard" : 0 },
-			{ "a" : "7", "b" : "8", "guard" : 0 },
-			{ "a" : "7", "b" : "8", "guard" : 0 }
-		]
-	}
-}
+{
+	"Roadrunner" :
+	//(ban fly/DD, 2 player, 31-May-03, midnight design)
+	{
+		"minSize" : "xl", "maxSize" : "xl",
+		"players" : "2",
+		"zones" :
+		{
+			"1" :
+			{
+				"type" : "playerStart",
+				"size" : 15,
+				"owner" : 1,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"neutralTowns" : { "towns" : 1 },
+				"bannedTowns" : ["necropolis", "conflux"],
+				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
+				"mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 },
+				"treasure" :
+				[
+					{ "min" : 3500, "max" : 6000, "density" : 4 },
+					{ "min" : 300, "max" : 2000, "density" : 5 }
+				]
+			},
+			"2" :
+			{
+				"type" : "playerStart",
+				"size" : 15,
+				"owner" : 2,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"neutralTowns" : { "towns" : 1 },
+				"bannedTowns" : ["necropolis", "conflux"],
+				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"3" :
+			{
+				"type" : "treasure",
+				"size" : 25,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 2 },
+				"allowedTowns" : [ "necropolis" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "dirt", "grass", "subterra", "lava" ],
+				"mines" : { "gold" : 1 },
+				"treasure" :
+				[
+					{ "min" : 6000, "max" : 9500, "density" : 2 },
+					{ "min" : 3500, "max" : 6000, "density" : 5 },
+					{ "min" : 1000, "max" : 2000, "density" : 3 }
+				]
+			},
+			"4" :
+			{
+				"type" : "treasure",
+				"size" : 25,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 2 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"minesLikeZone" : 3,
+				"treasureLikeZone" : 3
+			},
+			"5" :
+			{
+				"type" : "treasure",
+				"size" : 40,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 2 },
+				"terrainTypeLikeZone" : 3,
+				"mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 },
+				"treasure" :
+				[
+					{ "min" : 45000, "max" : 50000, "density" : 1 },
+					{ "min" : 15000, "max" : 22000, "density" : 1 },
+					{ "min" : 8000, "max" : 9200, "density" : 12 }
+				]
+			},
+			"6" :
+			{
+				"type" : "treasure",
+				"size" : 40,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 2 },
+				"terrainTypeLikeZone" : 3,
+				"minesLikeZone" : 5,
+				"treasureLikeZone" : 5
+			},
+			"7" :
+			{
+				"type" : "treasure",
+				"size" : 20,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 2 },
+				"bannedTowns" : ["necropolis"],
+				"matchTerrainToTown" : false,
+				"treasure" :
+				[
+					{ "min" : 100000, "max" : 120000, "density" : 1 },
+					{ "min" : 25000, "max" : 29000, "density" : 1 },
+					{ "min" : 8000, "max" : 9200, "density" : 12 }
+				]
+			},
+			"8" :
+			{
+				"type" : "treasure",
+				"size" : 20,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 2 },
+				"bannedTowns" : ["necropolis"],
+				"matchTerrainToTown" : false,
+				"treasureLikeZone" : 7
+			}
+		},
+		"connections" :
+		[
+			{ "a" : "1", "b" : "3", "guard" : 0 },
+			{ "a" : "1", "b" : "3", "guard" : 0 },
+			{ "a" : "2", "b" : "4", "guard" : 0 },
+			{ "a" : "2", "b" : "4", "guard" : 0 },
+			{ "a" : "3", "b" : "5", "guard" : 0 },
+			{ "a" : "3", "b" : "5", "guard" : 0 },
+			{ "a" : "3", "b" : "5", "guard" : 0 },
+			{ "a" : "4", "b" : "6", "guard" : 0 },
+			{ "a" : "4", "b" : "6", "guard" : 0 },
+			{ "a" : "4", "b" : "6", "guard" : 0 },
+			{ "a" : "5", "b" : "7", "guard" : 0 },
+			{ "a" : "5", "b" : "7", "guard" : 0 },
+			{ "a" : "6", "b" : "8", "guard" : 0 },
+			{ "a" : "6", "b" : "8", "guard" : 0 },
+			{ "a" : "7", "b" : "8", "guard" : 0 },
+			{ "a" : "7", "b" : "8", "guard" : 0 },
+			{ "a" : "7", "b" : "8", "guard" : 0 }
+		]
+	}
+}

+ 111 - 111
Mods/vcmi/config/vcmi/rmg/hdmod/superslam.JSON

@@ -1,111 +1,111 @@
-{
-	"SuperSlam" :
-	//(2 player, Large or XL  no under)  For powermonger players, meet by SLAMMING thru super zones. Your chances are not over until the fat lady sings! Should ban spec log along with normal random rules
-	{
-		"minSize" : "l", "maxSize" : "xl",
-		"players" : "2",
-		"zones" :
-		{
-			"1" :
-			{
-				"type" : "playerStart",
-				"size" : 20,
-				"owner" : 1,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"bannedTowns" : ["castle", "necropolis", "conflux"],
-				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
-				"mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 },
-				"treasure" :
-				[
-					{ "min" : 400, "max" : 2000, "density" : 6 },
-					{ "min" : 3500, "max" : 5000, "density" : 5 },
-					{ "min" : 2100, "max" : 3000, "density" : 2 }
-				]
-			},
-			"2" :
-			{
-				"type" : "playerStart",
-				"size" : 20,
-				"owner" : 2,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"bannedTowns" : ["castle", "necropolis", "conflux"],
-				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"3" :
-			{
-				"type" : "treasure",
-				"size" : 25,
-				"monsters" : "normal",
-				"neutralTowns" : { "towns" : 2 },
-				"allowedTowns" : [ "necropolis" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "dirt", "grass", "subterra", "lava" ],
-				"mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1, "gems" : 1, "gold" : 1 },
-				"treasure" :
-				[
-					{ "min" : 6000, "max" : 9500, "density" : 2 },
-					{ "min" : 3500, "max" : 6000, "density" : 5 },
-					{ "min" : 1000, "max" : 2000, "density" : 3 }
-				]
-			},
-			"4" :
-			{
-				"type" : "treasure",
-				"size" : 25,
-				"monsters" : "normal",
-				"neutralTowns" : { "towns" : 2 },
-				"allowedTowns" : [ "necropolis" ],
-				"terrainTypeLikeZone" : 3,
-				"minesLikeZone" : 3,
-				"treasureLikeZone" : 3
-			},
-			"5" :
-			{
-				"type" : "treasure",
-				"size" : 50,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 2 },
-				"allowedTowns" : [ "rampart", "tower", "inferno", "dungeon", "stronghold" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "sand" ],
-				"mines" : { "mercury" : 1, "crystal" : 1 },
-				"treasure" :
-				[
-					{ "min" : 25000, "max" : 29000, "density" : 5 },
-					{ "min" : 10000, "max" : 22000, "density" : 3 },
-					{ "min" : 1000, "max" : 1700, "density" : 1 }
-				]
-			},
-			"6" :
-			{
-				"type" : "treasure",
-				"size" : 50,
-				"monsters" : "strong",
-				"neutralTowns" : { "towns" : 2 },
-				"allowedTowns" : [ "rampart", "tower", "inferno", "dungeon", "stronghold" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "snow" ],
-				"minesLikeZone" : 5,
-				"treasure" :
-				[
-					{ "min" : 10000, "max" : 22000, "density" : 3 },
-					{ "min" : 25000, "max" : 29000, "density" : 5 },
-					{ "min" : 1000, "max" : 1700, "density" : 1 }
-				]
-			}
-		},
-		"connections" :
-		[
-			{ "a" : "1", "b" : "3", "guard" : 10 },
-			{ "a" : "2", "b" : "4", "guard" : 10 },
-			{ "a" : "3", "b" : "5", "guard" : 0 },
-			{ "a" : "4", "b" : "6", "guard" : 0 },
-			{ "a" : "5", "b" : "6", "guard" : 120000 },
-			{ "a" : "3", "b" : "4", "guard" : 120000 }
-		]
-	}
-}
+{
+	"SuperSlam" :
+	//(2 player, Large or XL  no under)  For powermonger players, meet by SLAMMING thru super zones. Your chances are not over until the fat lady sings! Should ban spec log along with normal random rules
+	{
+		"minSize" : "l", "maxSize" : "xl",
+		"players" : "2",
+		"zones" :
+		{
+			"1" :
+			{
+				"type" : "playerStart",
+				"size" : 20,
+				"owner" : 1,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"bannedTowns" : ["castle", "necropolis", "conflux"],
+				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
+				"mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 },
+				"treasure" :
+				[
+					{ "min" : 400, "max" : 2000, "density" : 6 },
+					{ "min" : 3500, "max" : 5000, "density" : 5 },
+					{ "min" : 2100, "max" : 3000, "density" : 2 }
+				]
+			},
+			"2" :
+			{
+				"type" : "playerStart",
+				"size" : 20,
+				"owner" : 2,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"bannedTowns" : ["castle", "necropolis", "conflux"],
+				"terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ],
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"3" :
+			{
+				"type" : "treasure",
+				"size" : 25,
+				"monsters" : "normal",
+				"neutralTowns" : { "towns" : 2 },
+				"allowedTowns" : [ "necropolis" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "dirt", "grass", "subterra", "lava" ],
+				"mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1, "gems" : 1, "gold" : 1 },
+				"treasure" :
+				[
+					{ "min" : 6000, "max" : 9500, "density" : 2 },
+					{ "min" : 3500, "max" : 6000, "density" : 5 },
+					{ "min" : 1000, "max" : 2000, "density" : 3 }
+				]
+			},
+			"4" :
+			{
+				"type" : "treasure",
+				"size" : 25,
+				"monsters" : "normal",
+				"neutralTowns" : { "towns" : 2 },
+				"allowedTowns" : [ "necropolis" ],
+				"terrainTypeLikeZone" : 3,
+				"minesLikeZone" : 3,
+				"treasureLikeZone" : 3
+			},
+			"5" :
+			{
+				"type" : "treasure",
+				"size" : 50,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 2 },
+				"allowedTowns" : [ "rampart", "tower", "inferno", "dungeon", "stronghold" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "sand" ],
+				"mines" : { "mercury" : 1, "crystal" : 1 },
+				"treasure" :
+				[
+					{ "min" : 25000, "max" : 29000, "density" : 5 },
+					{ "min" : 10000, "max" : 22000, "density" : 3 },
+					{ "min" : 1000, "max" : 1700, "density" : 1 }
+				]
+			},
+			"6" :
+			{
+				"type" : "treasure",
+				"size" : 50,
+				"monsters" : "strong",
+				"neutralTowns" : { "towns" : 2 },
+				"allowedTowns" : [ "rampart", "tower", "inferno", "dungeon", "stronghold" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "snow" ],
+				"minesLikeZone" : 5,
+				"treasure" :
+				[
+					{ "min" : 10000, "max" : 22000, "density" : 3 },
+					{ "min" : 25000, "max" : 29000, "density" : 5 },
+					{ "min" : 1000, "max" : 1700, "density" : 1 }
+				]
+			}
+		},
+		"connections" :
+		[
+			{ "a" : "1", "b" : "3", "guard" : 10 },
+			{ "a" : "2", "b" : "4", "guard" : 10 },
+			{ "a" : "3", "b" : "5", "guard" : 0 },
+			{ "a" : "4", "b" : "6", "guard" : 0 },
+			{ "a" : "5", "b" : "6", "guard" : 120000 },
+			{ "a" : "3", "b" : "4", "guard" : 120000 }
+		]
+	}
+}

+ 146 - 146
Mods/vcmi/config/vcmi/rmg/heroes3/readyOrNot.JSON

@@ -1,146 +1,146 @@
-{
-	"Ready or Not" :
-	{
-		"minSize" : "m", "maxSize" : "xl+u",
-		"players" : "2-3",
-		"zones" :
-		{
-			"1" :
-			{
-				"type" : "playerStart",
-				"size" : 10,
-				"owner" : 1,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"townsAreSameType" : true,
-				"allowedTowns" : [ "fortress" ],
-				"mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 },
-				"treasure" :
-				[
-					{ "min" : 500, "max" : 3000, "density" : 9 },
-					{ "min" : 500, "max" : 3000, "density" : 6 },
-					{ "min" : 3000, "max" : 6000, "density" : 1 }
-				]
-			},
-			"2" :
-			{
-				"type" : "playerStart",
-				"size" : 10,
-				"owner" : 2,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"townsAreSameType" : true,
-				"allowedTowns" : [ "inferno" ],
-				"mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 },
-				"treasureLikeZone" : 1
-			},
-			"3" :
-			{
-				"type" : "playerStart",
-				"size" : 10,
-				"owner" : 3,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"townsAreSameType" : true,
-				"allowedTowns" : [ "rampart" ],
-				"mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 },
-				"treasureLikeZone" : 1
-			},
-			"4" :
-			{
-				"type" : "treasure",
-				"size" : 15,
-				"monsters" : "normal",
-				"neutralTowns" : { "castles" : 1 },
-				"allowedTowns" : [ "rampart" ],
-				"allowedMonsters" : [ "inferno", "fortress", "neutral" ],
-				"mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 },
-				"treasure" :
-				[
-					{ "min" : 3000, "max" : 6000, "density" : 6 },
-					{ "min" : 10000, "max" : 15000, "density" : 1 },
-					{ "min" : 500, "max" : 3000, "density" : 9 }
-				]
-			},
-			"5" :
-			{
-				"type" : "treasure",
-				"size" : 5,
-				"monsters" : "normal",
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "dirt", "sand", "snow", "swamp", "rough", "subterra" ],
-				"mines" : { "wood" : 1, "ore" : 1 },
-				"treasure" :
-				[
-					{ "min" : 3000, "max" : 6000, "density" : 1 },
-					{ "min" : 500, "max" : 3000, "density" : 6 },
-					{ "min" : 500, "max" : 3000, "density" : 9 }
-				]
-			},
-			"6" :
-			{
-				"type" : "treasure",
-				"size" : 15,
-				"monsters" : "normal",
-				"neutralTowns" : { "castles" : 1 },
-				"allowedTowns" : [ "fortress" ],
-				"allowedMonsters" : [ "rampart", "inferno", "neutral" ],
-				"minesLikeZone" : 4,
-				"treasureLikeZone" : 4
-			},
-			"7" :
-			{
-				"type" : "treasure",
-				"size" : 15,
-				"monsters" : "normal",
-				"neutralTowns" : { "castles" : 1 },
-				"allowedTowns" : [ "inferno" ],
-				"allowedMonsters" : [ "rampart", "fortress", "neutral" ],
-				"minesLikeZone" : 4,
-				"treasureLikeZone" : 4
-			},
-			"8" :
-			{
-				"type" : "treasure",
-				"size" : 8,
-				"monsters" : "strong",
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "dirt" ],
-				"treasure" :
-				[
-					{ "min" : 20000, "max" : 30000, "density" : 1 },
-					{ "min" : 15000, "max" : 20000, "density" : 6 },
-					{ "min" : 10000, "max" : 15000, "density" : 9 }
-				]
-			},
-			"9" :
-			{
-				"type" : "treasure",
-				"size" : 8,
-				"monsters" : "strong",
-				"terrainTypeLikeZone" : 8,
-				"treasureLikeZone" : 8
-			},
-			"10" :
-			{
-				"type" : "treasure",
-				"size" : 8,
-				"monsters" : "strong",
-				"terrainTypeLikeZone" : 8,
-				"treasureLikeZone" : 8
-			}
-		},
-		"connections" :
-		[
-			{ "a" : "1", "b" : "5", "guard" : 6000 },
-			{ "a" : "2", "b" : "5", "guard" : 6000 },
-			{ "a" : "3", "b" : "5", "guard" : 6000 },
-			{ "a" : "4", "b" : "5", "guard" : 0 },
-			{ "a" : "6", "b" : "5", "guard" : 0 },
-			{ "a" : "7", "b" : "5", "guard" : 0 },
-			{ "a" : "8", "b" : "1", "guard" : 12500 }, // Border guard replaced by monster
-			{ "a" : "9", "b" : "2", "guard" : 12500 }, // Border guard replaced by monster
-			{ "a" : "10", "b" : "3", "guard" : 12500 } // Border guard replaced by monster
-		]
-	}
-}
+{
+	"Ready or Not" :
+	{
+		"minSize" : "m", "maxSize" : "xl+u",
+		"players" : "2-3",
+		"zones" :
+		{
+			"1" :
+			{
+				"type" : "playerStart",
+				"size" : 10,
+				"owner" : 1,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"townsAreSameType" : true,
+				"allowedTowns" : [ "fortress" ],
+				"mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 },
+				"treasure" :
+				[
+					{ "min" : 500, "max" : 3000, "density" : 9 },
+					{ "min" : 500, "max" : 3000, "density" : 6 },
+					{ "min" : 3000, "max" : 6000, "density" : 1 }
+				]
+			},
+			"2" :
+			{
+				"type" : "playerStart",
+				"size" : 10,
+				"owner" : 2,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"townsAreSameType" : true,
+				"allowedTowns" : [ "inferno" ],
+				"mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 },
+				"treasureLikeZone" : 1
+			},
+			"3" :
+			{
+				"type" : "playerStart",
+				"size" : 10,
+				"owner" : 3,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"townsAreSameType" : true,
+				"allowedTowns" : [ "rampart" ],
+				"mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 },
+				"treasureLikeZone" : 1
+			},
+			"4" :
+			{
+				"type" : "treasure",
+				"size" : 15,
+				"monsters" : "normal",
+				"neutralTowns" : { "castles" : 1 },
+				"allowedTowns" : [ "rampart" ],
+				"allowedMonsters" : [ "inferno", "fortress", "neutral" ],
+				"mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 },
+				"treasure" :
+				[
+					{ "min" : 3000, "max" : 6000, "density" : 6 },
+					{ "min" : 10000, "max" : 15000, "density" : 1 },
+					{ "min" : 500, "max" : 3000, "density" : 9 }
+				]
+			},
+			"5" :
+			{
+				"type" : "treasure",
+				"size" : 5,
+				"monsters" : "normal",
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "dirt", "sand", "snow", "swamp", "rough", "subterra" ],
+				"mines" : { "wood" : 1, "ore" : 1 },
+				"treasure" :
+				[
+					{ "min" : 3000, "max" : 6000, "density" : 1 },
+					{ "min" : 500, "max" : 3000, "density" : 6 },
+					{ "min" : 500, "max" : 3000, "density" : 9 }
+				]
+			},
+			"6" :
+			{
+				"type" : "treasure",
+				"size" : 15,
+				"monsters" : "normal",
+				"neutralTowns" : { "castles" : 1 },
+				"allowedTowns" : [ "fortress" ],
+				"allowedMonsters" : [ "rampart", "inferno", "neutral" ],
+				"minesLikeZone" : 4,
+				"treasureLikeZone" : 4
+			},
+			"7" :
+			{
+				"type" : "treasure",
+				"size" : 15,
+				"monsters" : "normal",
+				"neutralTowns" : { "castles" : 1 },
+				"allowedTowns" : [ "inferno" ],
+				"allowedMonsters" : [ "rampart", "fortress", "neutral" ],
+				"minesLikeZone" : 4,
+				"treasureLikeZone" : 4
+			},
+			"8" :
+			{
+				"type" : "treasure",
+				"size" : 8,
+				"monsters" : "strong",
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "dirt" ],
+				"treasure" :
+				[
+					{ "min" : 20000, "max" : 30000, "density" : 1 },
+					{ "min" : 15000, "max" : 20000, "density" : 6 },
+					{ "min" : 10000, "max" : 15000, "density" : 9 }
+				]
+			},
+			"9" :
+			{
+				"type" : "treasure",
+				"size" : 8,
+				"monsters" : "strong",
+				"terrainTypeLikeZone" : 8,
+				"treasureLikeZone" : 8
+			},
+			"10" :
+			{
+				"type" : "treasure",
+				"size" : 8,
+				"monsters" : "strong",
+				"terrainTypeLikeZone" : 8,
+				"treasureLikeZone" : 8
+			}
+		},
+		"connections" :
+		[
+			{ "a" : "1", "b" : "5", "guard" : 6000 },
+			{ "a" : "2", "b" : "5", "guard" : 6000 },
+			{ "a" : "3", "b" : "5", "guard" : 6000 },
+			{ "a" : "4", "b" : "5", "guard" : 0 },
+			{ "a" : "6", "b" : "5", "guard" : 0 },
+			{ "a" : "7", "b" : "5", "guard" : 0 },
+			{ "a" : "8", "b" : "1", "guard" : 12500 }, // Border guard replaced by monster
+			{ "a" : "9", "b" : "2", "guard" : 12500 }, // Border guard replaced by monster
+			{ "a" : "10", "b" : "3", "guard" : 12500 } // Border guard replaced by monster
+		]
+	}
+}

+ 232 - 232
Mods/vcmi/config/vcmi/rmg/heroes3/worldsAtWar.JSON

@@ -1,232 +1,232 @@
-{
-	"Worlds at War" :
-	{
-		"minSize" : "m+u", "maxSize" : "xl+u",
-		"players" : "2", "cpu" : "3",
-		"zones" :
-		{
-			"1" :
-			{
-				"type" : "playerStart",
-				"size" : 15,
-				"owner" : 1,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"mines" : { "wood" : 1, "ore" : 1 },
-				"treasure" :
-				[
-					{ "min" : 500, "max" : 3000, "density" : 8 },
-					{ "min" : 3000, "max" : 6000, "density" : 6 },
-					{ "min" : 10000, "max" : 15000, "density" : 1 }
-				]
-			},
-			"2" :
-			{
-				"type" : "playerStart",
-				"size" : 15,
-				"owner" : 2,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"3" :
-			{
-				"type" : "cpuStart",
-				"size" : 15,
-				"owner" : 3,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"4" :
-			{
-				"type" : "cpuStart",
-				"size" : 15,
-				"owner" : 4,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 1
-			},
-			"5" :
-			{
-				"type" : "cpuStart",
-				"size" : 15,
-				"owner" : 5,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 },
-				"treasureLikeZone" : 1
-			},
-			"6" :
-			{
-				"type" : "treasure",
-				"size" : 30,
-				"monsters" : "normal",
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "swamp" ],
-				"mines" : { "sulfur" : 1, "gems" : 1 },
-				"treasure" :
-				[
-					{ "min" : 500, "max" : 8000, "density" : 8 },
-					{ "min" : 3000, "max" : 15000, "density" : 6 },
-					{ "min" : 10000, "max" : 15000, "density" : 1 }
-				]
-			},
-			"7" :
-			{
-				"type" : "treasure",
-				"size" : 20,
-				"monsters" : "normal",
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "dirt" ],
-				"mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 },
-				"treasure" :
-				[
-					{ "min" : 500, "max" : 3000, "density" : 6 },
-					{ "min" : 3000, "max" : 6000, "density" : 6 },
-					{ "min" : 15000, "max" : 20000, "density" : 3 }
-				]
-			},
-			"8" :
-			{
-				"type" : "treasure",
-				"size" : 30,
-				"monsters" : "normal",
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "rough" ],
-				"minesLikeZone" : 6,
-				"treasureLikeZone" : 6
-			},
-			"9" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "normal",
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "grass" ],
-				"treasure" :
-				[
-					{ "min" : 500, "max" : 3000, "density" : 3 },
-					{ "min" : 10000, "max" : 20000, "density" : 2 },
-					{ "min" : 20000, "max" : 30000, "density" : 1 }
-				]
-			},
-			"10" :
-			{
-				"type" : "treasure",
-				"size" : 15,
-				"monsters" : "normal",
-				"neutralTowns" : { "castles" : 1 },
-				"minesLikeZone" : 1,
-				"treasure" :
-				[
-					{ "min" : 10000, "max" : 20000, "density" : 1 },
-					{ "min" : 500, "max" : 3000, "density" : 8 },
-					{ "min" : 3000, "max" : 6000, "density" : 2 }
-				]
-			},
-			"11" :
-			{
-				"type" : "treasure",
-				"size" : 15,
-				"monsters" : "normal",
-				"neutralTowns" : { "castles" : 1 },
-				"minesLikeZone" : 1,
-				"treasureLikeZone" : 10
-			},
-			"12" :
-			{
-				"type" : "treasure",
-				"size" : 20,
-				"monsters" : "normal",
-				"neutralTowns" : { "castles" : 1 },
-				"mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 },
-				"treasure" :
-				[
-					{ "min" : 500, "max" : 3000, "density" : 8 },
-					{ "min" : 3000, "max" : 6000, "density" : 4 },
-					{ "min" : 10000, "max" : 20000, "density" : 3 }
-				]
-			},
-			"13" :
-			{
-				"type" : "treasure",
-				"size" : 35,
-				"monsters" : "strong",
-				"neutralTowns" : { "castles" : 1 },
-				"mines" : { "gold" : 1 },
-				"treasure" :
-				[
-					{ "min" : 5000, "max" : 20000, "density" : 6 },
-					{ "min" : 10000, "max" : 20000, "density" : 6 },
-					{ "min" : 500, "max" : 3000, "density" : 10 }
-				]
-			},
-			"14" :
-			{
-				"type" : "treasure",
-				"size" : 20,
-				"monsters" : "normal",
-				"neutralTowns" : { "castles" : 1 },
-				"minesLikeZone" : 12,
-				"treasureLikeZone" : 12
-			},
-			"15" :
-			{
-				"type" : "treasure",
-				"size" : 20,
-				"monsters" : "normal",
-				"neutralTowns" : { "castles" : 1 },
-				"minesLikeZone" : 12,
-				"treasureLikeZone" : 12
-			},
-			"16" :
-			{
-				"type" : "treasure",
-				"size" : 20,
-				"monsters" : "normal",
-				"neutralTowns" : { "castles" : 1 },
-				"minesLikeZone" : 12,
-				"treasureLikeZone" : 12
-			},
-			"17" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "normal",
-				"terrainTypeLikeZone" : 9,
-				"minesLikeZone" : 13,
-				"treasure" :
-				[
-					{ "min" : 500, "max" : 3000, "density" : 6 },
-					{ "min" : 20000, "max" : 30000, "density" : 1 },
-					{ "min" : 10000, "max" : 20000, "density" : 4 }
-				]
-			}
-		},
-		"connections" :
-		[
-			{ "a" : "1", "b" : "6", "guard" : 3000 },
-			{ "a" : "2", "b" : "8", "guard" : 3000 },
-			{ "a" : "3", "b" : "12", "guard" : 4500 },
-			{ "a" : "4", "b" : "14", "guard" : 4500 },
-			{ "a" : "5", "b" : "9", "guard" : 6000 },
-			{ "a" : "5", "b" : "15", "guard" : 3000 },
-			{ "a" : "5", "b" : "16", "guard" : 3000 },
-			{ "a" : "6", "b" : "7", "guard" : 12500 }, // Border guard replaced by 12.5k guard
-			{ "a" : "6", "b" : "8", "guard" : 6000 },
-			{ "a" : "6", "b" : "10", "guard" : 4500 },
-			{ "a" : "7", "b" : "8", "guard" : 12500 }, // Border guard replaced by 12.5k guard
-			{ "a" : "8", "b" : "11", "guard" : 4500 },
-			{ "a" : "9", "b" : "13", "guard" : 6000 },
-			{ "a" : "12", "b" : "13", "guard" : 4500 },
-			{ "a" : "13", "b" : "14", "guard" : 4500 },
-			{ "a" : "13", "b" : "17", "guard" : 12500 }, // Border guard replaced by 12.5k guard
-			{ "a" : "6", "b" : "13", "guard" : 6000 },
-			{ "a" : "8", "b" : "13", "guard" : 6000 }
-		]
-	}
-}
+{
+	"Worlds at War" :
+	{
+		"minSize" : "m+u", "maxSize" : "xl+u",
+		"players" : "2", "cpu" : "3",
+		"zones" :
+		{
+			"1" :
+			{
+				"type" : "playerStart",
+				"size" : 15,
+				"owner" : 1,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"mines" : { "wood" : 1, "ore" : 1 },
+				"treasure" :
+				[
+					{ "min" : 500, "max" : 3000, "density" : 8 },
+					{ "min" : 3000, "max" : 6000, "density" : 6 },
+					{ "min" : 10000, "max" : 15000, "density" : 1 }
+				]
+			},
+			"2" :
+			{
+				"type" : "playerStart",
+				"size" : 15,
+				"owner" : 2,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"3" :
+			{
+				"type" : "cpuStart",
+				"size" : 15,
+				"owner" : 3,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"4" :
+			{
+				"type" : "cpuStart",
+				"size" : 15,
+				"owner" : 4,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 1
+			},
+			"5" :
+			{
+				"type" : "cpuStart",
+				"size" : 15,
+				"owner" : 5,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 },
+				"treasureLikeZone" : 1
+			},
+			"6" :
+			{
+				"type" : "treasure",
+				"size" : 30,
+				"monsters" : "normal",
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "swamp" ],
+				"mines" : { "sulfur" : 1, "gems" : 1 },
+				"treasure" :
+				[
+					{ "min" : 500, "max" : 8000, "density" : 8 },
+					{ "min" : 3000, "max" : 15000, "density" : 6 },
+					{ "min" : 10000, "max" : 15000, "density" : 1 }
+				]
+			},
+			"7" :
+			{
+				"type" : "treasure",
+				"size" : 20,
+				"monsters" : "normal",
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "dirt" ],
+				"mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 },
+				"treasure" :
+				[
+					{ "min" : 500, "max" : 3000, "density" : 6 },
+					{ "min" : 3000, "max" : 6000, "density" : 6 },
+					{ "min" : 15000, "max" : 20000, "density" : 3 }
+				]
+			},
+			"8" :
+			{
+				"type" : "treasure",
+				"size" : 30,
+				"monsters" : "normal",
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "rough" ],
+				"minesLikeZone" : 6,
+				"treasureLikeZone" : 6
+			},
+			"9" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "normal",
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "grass" ],
+				"treasure" :
+				[
+					{ "min" : 500, "max" : 3000, "density" : 3 },
+					{ "min" : 10000, "max" : 20000, "density" : 2 },
+					{ "min" : 20000, "max" : 30000, "density" : 1 }
+				]
+			},
+			"10" :
+			{
+				"type" : "treasure",
+				"size" : 15,
+				"monsters" : "normal",
+				"neutralTowns" : { "castles" : 1 },
+				"minesLikeZone" : 1,
+				"treasure" :
+				[
+					{ "min" : 10000, "max" : 20000, "density" : 1 },
+					{ "min" : 500, "max" : 3000, "density" : 8 },
+					{ "min" : 3000, "max" : 6000, "density" : 2 }
+				]
+			},
+			"11" :
+			{
+				"type" : "treasure",
+				"size" : 15,
+				"monsters" : "normal",
+				"neutralTowns" : { "castles" : 1 },
+				"minesLikeZone" : 1,
+				"treasureLikeZone" : 10
+			},
+			"12" :
+			{
+				"type" : "treasure",
+				"size" : 20,
+				"monsters" : "normal",
+				"neutralTowns" : { "castles" : 1 },
+				"mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 },
+				"treasure" :
+				[
+					{ "min" : 500, "max" : 3000, "density" : 8 },
+					{ "min" : 3000, "max" : 6000, "density" : 4 },
+					{ "min" : 10000, "max" : 20000, "density" : 3 }
+				]
+			},
+			"13" :
+			{
+				"type" : "treasure",
+				"size" : 35,
+				"monsters" : "strong",
+				"neutralTowns" : { "castles" : 1 },
+				"mines" : { "gold" : 1 },
+				"treasure" :
+				[
+					{ "min" : 5000, "max" : 20000, "density" : 6 },
+					{ "min" : 10000, "max" : 20000, "density" : 6 },
+					{ "min" : 500, "max" : 3000, "density" : 10 }
+				]
+			},
+			"14" :
+			{
+				"type" : "treasure",
+				"size" : 20,
+				"monsters" : "normal",
+				"neutralTowns" : { "castles" : 1 },
+				"minesLikeZone" : 12,
+				"treasureLikeZone" : 12
+			},
+			"15" :
+			{
+				"type" : "treasure",
+				"size" : 20,
+				"monsters" : "normal",
+				"neutralTowns" : { "castles" : 1 },
+				"minesLikeZone" : 12,
+				"treasureLikeZone" : 12
+			},
+			"16" :
+			{
+				"type" : "treasure",
+				"size" : 20,
+				"monsters" : "normal",
+				"neutralTowns" : { "castles" : 1 },
+				"minesLikeZone" : 12,
+				"treasureLikeZone" : 12
+			},
+			"17" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "normal",
+				"terrainTypeLikeZone" : 9,
+				"minesLikeZone" : 13,
+				"treasure" :
+				[
+					{ "min" : 500, "max" : 3000, "density" : 6 },
+					{ "min" : 20000, "max" : 30000, "density" : 1 },
+					{ "min" : 10000, "max" : 20000, "density" : 4 }
+				]
+			}
+		},
+		"connections" :
+		[
+			{ "a" : "1", "b" : "6", "guard" : 3000 },
+			{ "a" : "2", "b" : "8", "guard" : 3000 },
+			{ "a" : "3", "b" : "12", "guard" : 4500 },
+			{ "a" : "4", "b" : "14", "guard" : 4500 },
+			{ "a" : "5", "b" : "9", "guard" : 6000 },
+			{ "a" : "5", "b" : "15", "guard" : 3000 },
+			{ "a" : "5", "b" : "16", "guard" : 3000 },
+			{ "a" : "6", "b" : "7", "guard" : 12500 }, // Border guard replaced by 12.5k guard
+			{ "a" : "6", "b" : "8", "guard" : 6000 },
+			{ "a" : "6", "b" : "10", "guard" : 4500 },
+			{ "a" : "7", "b" : "8", "guard" : 12500 }, // Border guard replaced by 12.5k guard
+			{ "a" : "8", "b" : "11", "guard" : 4500 },
+			{ "a" : "9", "b" : "13", "guard" : 6000 },
+			{ "a" : "12", "b" : "13", "guard" : 4500 },
+			{ "a" : "13", "b" : "14", "guard" : 4500 },
+			{ "a" : "13", "b" : "17", "guard" : 12500 }, // Border guard replaced by 12.5k guard
+			{ "a" : "6", "b" : "13", "guard" : 6000 },
+			{ "a" : "8", "b" : "13", "guard" : 6000 }
+		]
+	}
+}

+ 361 - 361
Mods/vcmi/config/vcmi/rmg/heroes3unused/gauntlet.JSON

@@ -1,361 +1,361 @@
-{
-	"Gauntlet" :
-	{
-		"minSize" : "m+u", "maxSize" : "xl+u",
-		"players" : "1", "cpu" : "5",
-		"zones" :
-		{
-			"1" :
-			{
-				"type" : "playerStart",
-				"size" : 6,
-				"owner" : 1,
-				"monsters" : "weak",
-				"playerTowns" : { "castles" : 1 },
-				"allowedTowns" : [ "necropolis" ],
-				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "snow" ],
-				"treasure" :
-				[
-					{ "min" : 5000, "max" : 8000, "density" : 1 },
-					{ "min" : 3000, "max" : 6000, "density" : 2 },
-					{ "min" : 500, "max" : 3000, "density" : 10 }
-				]
-			},
-			"2" :
-			{
-				"type" : "treasure",
-				"size" : 6,
-				"monsters" : "weak",
-				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
-				"terrainTypeLikeZone" : 1,
-				"mines" : { "ore" : 1 },
-				"treasure" :
-				[
-					{ "min" : 3000, "max" : 6000, "density" : 3 },
-					{ "min" : 500, "max" : 3000, "density" : 10 },
-					{ "min" : 0, "max" : 0, "density" : 1 }
-				]
-			},
-			"3" :
-			{
-				"type" : "treasure",
-				"size" : 8,
-				"monsters" : "weak",
-				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
-				"terrainTypeLikeZone" : 1,
-				"treasure" :
-				[
-					{ "min" : 10000, "max" : 14000, "density" : 1 },
-					{ "min" : 4000, "max" : 8000, "density" : 3 },
-					{ "min" : 500, "max" : 3000, "density" : 9 }
-				]
-			},
-			"4" :
-			{
-				"type" : "treasure",
-				"size" : 6,
-				"monsters" : "weak",
-				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
-				"terrainTypeLikeZone" : 1,
-				"mines" : { "wood" : 1 },
-				"treasureLikeZone" : 2
-			},
-			"5" :
-			{
-				"type" : "junction",
-				"size" : 6,
-				"monsters" : "weak",
-				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "dirt", "sand", "rough" ],
-				"treasure" :
-				[
-					{ "min" : 100, "max" : 2000, "density" : 8 },
-					{ "min" : 0, "max" : 0, "density" : 1 },
-					{ "min" : 0, "max" : 0, "density" : 1 }
-				]
-			},
-			"6" :
-			{
-				"type" : "treasure",
-				"size" : 8,
-				"monsters" : "normal",
-				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
-				"terrainTypeLikeZone" : 5,
-				"mines" : { "mercury" : 1, "sulfur" : 1, "gems" : 1 },
-				"treasure" :
-				[
-					{ "min" : 3000, "max" : 6000, "density" : 4 },
-					{ "min" : 500, "max" : 3000, "density" : 8 },
-					{ "min" : 0, "max" : 0, "density" : 1 }
-				]
-			},
-			"7" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "normal",
-				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "swamp" ],
-				"treasure" :
-				[
-					{ "min" : 12000, "max" : 18000, "density" : 1 },
-					{ "min" : 5000, "max" : 10000, "density" : 5 },
-					{ "min" : 1000, "max" : 4000, "density" : 7 }
-				]
-			},
-			"8" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "normal",
-				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
-				"terrainTypeLikeZone" : 7,
-				"treasure" :
-				[
-					{ "min" : 5000, "max" : 10000, "density" : 6 },
-					{ "min" : 1000, "max" : 4000, "density" : 7 },
-					{ "min" : 0, "max" : 0, "density" : 1 }
-				]
-			},
-			"9" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "normal",
-				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "dirt", "grass" ],
-				"mines" : { "crystal" : 1 },
-				"treasure" :
-				[
-					{ "min" : 15000, "max" : 20000, "density" : 1 },
-					{ "min" : 4000, "max" : 8000, "density" : 3 },
-					{ "min" : 500, "max" : 3000, "density" : 9 }
-				]
-			},
-			"10" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "normal",
-				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
-				"matchTerrainToTown" : false,
-				"terrainTypes" : [ "dirt", "grass", "rough" ],
-				"mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 },
-				"treasure" :
-				[
-					{ "min" : 4000, "max" : 8000, "density" : 4 },
-					{ "min" : 500, "max" : 3000, "density" : 9 },
-					{ "min" : 0, "max" : 0, "density" : 1 }
-				]
-			},
-			"11" :
-			{
-				"type" : "cpuStart",
-				"size" : 10,
-				"owner" : 2,
-				"monsters" : "weak",
-				"playerTowns" : { "castles" : 1 },
-				"allowedTowns" : [ "castle", "rampart" ],
-				"mines" : { "wood" : 1, "ore" : 1 },
-				"treasure" :
-				[
-					{ "min" : 5000, "max" : 10000, "density" : 1 },
-					{ "min" : 4000, "max" : 8000, "density" : 4 },
-					{ "min" : 1000, "max" : 4000, "density" : 8 }
-				]
-			},
-			"12" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "normal",
-				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
-				"terrainTypeLikeZone" : 10,
-				"treasureLikeZone" : 3
-			},
-			"13" :
-			{
-				"type" : "junction",
-				"size" : 6,
-				"monsters" : "weak",
-				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
-				"terrainTypeLikeZone" : 10,
-				"treasureLikeZone" : 5
-			},
-			"14" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "normal",
-				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
-				"terrainTypeLikeZone" : 10,
-				"minesLikeZone" : 6,
-				"treasure" :
-				[
-					{ "min" : 4000, "max" : 8000, "density" : 5 },
-					{ "min" : 500, "max" : 3000, "density" : 8 },
-					{ "min" : 0, "max" : 0, "density" : 1 }
-				]
-			},
-			"15" :
-			{
-				"type" : "cpuStart",
-				"size" : 10,
-				"owner" : 4,
-				"monsters" : "weak",
-				"playerTowns" : { "castles" : 1 },
-				"allowedTowns" : [ "castle", "rampart", "stronghold" ],
-				"mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 },
-				"treasureLikeZone" : 11
-			},
-			"16" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "weak",
-				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
-				"terrainTypeLikeZone" : 10,
-				"treasure" :
-				[
-					{ "min" : 10000, "max" : 14000, "density" : 1 },
-					{ "min" : 4000, "max" : 8000, "density" : 5 },
-					{ "min" : 500, "max" : 3000, "density" : 7 }
-				]
-			},
-			"17" :
-			{
-				"type" : "cpuStart",
-				"size" : 10,
-				"owner" : 6,
-				"monsters" : "weak",
-				"playerTowns" : { "castles" : 1 },
-				"allowedTowns" : [ "castle", "rampart", "stronghold" ],
-				"mines" : { "wood" : 1, "ore" : 1, "gems" : 1 },
-				"treasureLikeZone" : 14
-			},
-			"18" :
-			{
-				"type" : "treasure",
-				"size" : 10,
-				"monsters" : "normal",
-				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
-				"terrainTypeLikeZone" : 10,
-				"mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1 },
-				"treasure" :
-				[
-					{ "min" : 5000, "max" : 10000, "density" : 4 },
-					{ "min" : 500, "max" : 3000, "density" : 9 },
-					{ "min" : 0, "max" : 0, "density" : 1 }
-				]
-			},
-			"19" :
-			{
-				"type" : "junction",
-				"size" : 6,
-				"monsters" : "weak",
-				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
-				"terrainTypeLikeZone" : 10,
-				"treasureLikeZone" : 5
-			},
-			"20" :
-			{
-				"type" : "treasure",
-				"size" : 8,
-				"monsters" : "normal",
-				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
-				"terrainTypeLikeZone" : 10,
-				"mines" : { "sulfur" : 1, "crystal" : 1, "gems" : 1 },
-				"treasureLikeZone" : 18
-			},
-			"21" :
-			{
-				"type" : "cpuStart",
-				"size" : 8,
-				"owner" : 5,
-				"monsters" : "weak",
-				"playerTowns" : { "castles" : 1 },
-				"allowedTowns" : [ "castle", "rampart", "stronghold" ],
-				"minesLikeZone" : 10,
-				"treasure" :
-				[
-					{ "min" : 5000, "max" : 10000, "density" : 3 },
-					{ "min" : 500, "max" : 3000, "density" : 10 },
-					{ "min" : 0, "max" : 0, "density" : 1 }
-				]
-			},
-			"22" :
-			{
-				"type" : "junction",
-				"size" : 6,
-				"monsters" : "weak",
-				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
-				"terrainTypeLikeZone" : 10,
-				"treasureLikeZone" : 5
-			},
-			"23" :
-			{
-				"type" : "treasure",
-				"size" : 8,
-				"monsters" : "weak",
-				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
-				"terrainTypeLikeZone" : 10,
-				"minesLikeZone" : 18,
-				"treasure" :
-				[
-					{ "min" : 8000, "max" : 12000, "density" : 2 },
-					{ "min" : 6000, "max" : 10000, "density" : 4 },
-					{ "min" : 1000, "max" : 5000, "density" : 8 }
-				]
-			},
-			"24" :
-			{
-				"type" : "cpuStart",
-				"size" : 8,
-				"owner" : 3,
-				"monsters" : "normal",
-				"playerTowns" : { "castles" : 1 },
-				"allowedTowns" : [ "castle", "rampart", "stronghold" ],
-				"minesLikeZone" : 17,
-				"treasure" :
-				[
-					{ "min" : 5000, "max" : 10000, "density" : 1 },
-					{ "min" : 4000, "max" : 8000, "density" : 3 },
-					{ "min" : 1000, "max" : 4000, "density" : 8 }
-				]
-			}
-		},
-		"connections" :
-		[
-			{ "a" : "1", "b" : "2", "guard" : 0 },
-			{ "a" : "1", "b" : "4", "guard" : 0 },
-			{ "a" : "1", "b" : "5", "guard" : 12500 }, // Border guard replaced with monster
-			{ "a" : "1", "b" : "15", "guard" : 0 },
-			{ "a" : "2", "b" : "3", "guard" : 0 },
-			{ "a" : "5", "b" : "6", "guard" : 0 },
-			{ "a" : "5", "b" : "21", "guard" : 12500 }, // Border guard replaced with monster
-			{ "a" : "6", "b" : "7", "guard" : 0 },
-			{ "a" : "7", "b" : "8", "guard" : 0 },
-			{ "a" : "8", "b" : "9", "guard" : 0 },
-			{ "a" : "9", "b" : "10", "guard" : 0 },
-			{ "a" : "10", "b" : "11", "guard" : 0 },
-			{ "a" : "10", "b" : "12", "guard" : 0 },
-			{ "a" : "12", "b" : "13", "guard" : 0 },
-			{ "a" : "13", "b" : "14", "guard" : 0 },
-			{ "a" : "14", "b" : "15", "guard" : 0 },
-			{ "a" : "15", "b" : "16", "guard" : 0 },
-			{ "a" : "16", "b" : "17", "guard" : 0 },
-			{ "a" : "17", "b" : "18", "guard" : 0 },
-			{ "a" : "18", "b" : "19", "guard" : 0 },
-			{ "a" : "18", "b" : "22", "guard" : 0 },
-			{ "a" : "19", "b" : "20", "guard" : 0 },
-			{ "a" : "20", "b" : "21", "guard" : 0 },
-			{ "a" : "22", "b" : "23", "guard" : 0 },
-			{ "a" : "23", "b" : "24", "guard" : 0 }
-		]
-	}
-}
+{
+	"Gauntlet" :
+	{
+		"minSize" : "m+u", "maxSize" : "xl+u",
+		"players" : "1", "cpu" : "5",
+		"zones" :
+		{
+			"1" :
+			{
+				"type" : "playerStart",
+				"size" : 6,
+				"owner" : 1,
+				"monsters" : "weak",
+				"playerTowns" : { "castles" : 1 },
+				"allowedTowns" : [ "necropolis" ],
+				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "snow" ],
+				"treasure" :
+				[
+					{ "min" : 5000, "max" : 8000, "density" : 1 },
+					{ "min" : 3000, "max" : 6000, "density" : 2 },
+					{ "min" : 500, "max" : 3000, "density" : 10 }
+				]
+			},
+			"2" :
+			{
+				"type" : "treasure",
+				"size" : 6,
+				"monsters" : "weak",
+				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
+				"terrainTypeLikeZone" : 1,
+				"mines" : { "ore" : 1 },
+				"treasure" :
+				[
+					{ "min" : 3000, "max" : 6000, "density" : 3 },
+					{ "min" : 500, "max" : 3000, "density" : 10 },
+					{ "min" : 0, "max" : 0, "density" : 1 }
+				]
+			},
+			"3" :
+			{
+				"type" : "treasure",
+				"size" : 8,
+				"monsters" : "weak",
+				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
+				"terrainTypeLikeZone" : 1,
+				"treasure" :
+				[
+					{ "min" : 10000, "max" : 14000, "density" : 1 },
+					{ "min" : 4000, "max" : 8000, "density" : 3 },
+					{ "min" : 500, "max" : 3000, "density" : 9 }
+				]
+			},
+			"4" :
+			{
+				"type" : "treasure",
+				"size" : 6,
+				"monsters" : "weak",
+				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
+				"terrainTypeLikeZone" : 1,
+				"mines" : { "wood" : 1 },
+				"treasureLikeZone" : 2
+			},
+			"5" :
+			{
+				"type" : "junction",
+				"size" : 6,
+				"monsters" : "weak",
+				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "dirt", "sand", "rough" ],
+				"treasure" :
+				[
+					{ "min" : 100, "max" : 2000, "density" : 8 },
+					{ "min" : 0, "max" : 0, "density" : 1 },
+					{ "min" : 0, "max" : 0, "density" : 1 }
+				]
+			},
+			"6" :
+			{
+				"type" : "treasure",
+				"size" : 8,
+				"monsters" : "normal",
+				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
+				"terrainTypeLikeZone" : 5,
+				"mines" : { "mercury" : 1, "sulfur" : 1, "gems" : 1 },
+				"treasure" :
+				[
+					{ "min" : 3000, "max" : 6000, "density" : 4 },
+					{ "min" : 500, "max" : 3000, "density" : 8 },
+					{ "min" : 0, "max" : 0, "density" : 1 }
+				]
+			},
+			"7" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "normal",
+				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "swamp" ],
+				"treasure" :
+				[
+					{ "min" : 12000, "max" : 18000, "density" : 1 },
+					{ "min" : 5000, "max" : 10000, "density" : 5 },
+					{ "min" : 1000, "max" : 4000, "density" : 7 }
+				]
+			},
+			"8" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "normal",
+				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
+				"terrainTypeLikeZone" : 7,
+				"treasure" :
+				[
+					{ "min" : 5000, "max" : 10000, "density" : 6 },
+					{ "min" : 1000, "max" : 4000, "density" : 7 },
+					{ "min" : 0, "max" : 0, "density" : 1 }
+				]
+			},
+			"9" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "normal",
+				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "dirt", "grass" ],
+				"mines" : { "crystal" : 1 },
+				"treasure" :
+				[
+					{ "min" : 15000, "max" : 20000, "density" : 1 },
+					{ "min" : 4000, "max" : 8000, "density" : 3 },
+					{ "min" : 500, "max" : 3000, "density" : 9 }
+				]
+			},
+			"10" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "normal",
+				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
+				"matchTerrainToTown" : false,
+				"terrainTypes" : [ "dirt", "grass", "rough" ],
+				"mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 },
+				"treasure" :
+				[
+					{ "min" : 4000, "max" : 8000, "density" : 4 },
+					{ "min" : 500, "max" : 3000, "density" : 9 },
+					{ "min" : 0, "max" : 0, "density" : 1 }
+				]
+			},
+			"11" :
+			{
+				"type" : "cpuStart",
+				"size" : 10,
+				"owner" : 2,
+				"monsters" : "weak",
+				"playerTowns" : { "castles" : 1 },
+				"allowedTowns" : [ "castle", "rampart" ],
+				"mines" : { "wood" : 1, "ore" : 1 },
+				"treasure" :
+				[
+					{ "min" : 5000, "max" : 10000, "density" : 1 },
+					{ "min" : 4000, "max" : 8000, "density" : 4 },
+					{ "min" : 1000, "max" : 4000, "density" : 8 }
+				]
+			},
+			"12" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "normal",
+				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
+				"terrainTypeLikeZone" : 10,
+				"treasureLikeZone" : 3
+			},
+			"13" :
+			{
+				"type" : "junction",
+				"size" : 6,
+				"monsters" : "weak",
+				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
+				"terrainTypeLikeZone" : 10,
+				"treasureLikeZone" : 5
+			},
+			"14" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "normal",
+				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
+				"terrainTypeLikeZone" : 10,
+				"minesLikeZone" : 6,
+				"treasure" :
+				[
+					{ "min" : 4000, "max" : 8000, "density" : 5 },
+					{ "min" : 500, "max" : 3000, "density" : 8 },
+					{ "min" : 0, "max" : 0, "density" : 1 }
+				]
+			},
+			"15" :
+			{
+				"type" : "cpuStart",
+				"size" : 10,
+				"owner" : 4,
+				"monsters" : "weak",
+				"playerTowns" : { "castles" : 1 },
+				"allowedTowns" : [ "castle", "rampart", "stronghold" ],
+				"mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 },
+				"treasureLikeZone" : 11
+			},
+			"16" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "weak",
+				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
+				"terrainTypeLikeZone" : 10,
+				"treasure" :
+				[
+					{ "min" : 10000, "max" : 14000, "density" : 1 },
+					{ "min" : 4000, "max" : 8000, "density" : 5 },
+					{ "min" : 500, "max" : 3000, "density" : 7 }
+				]
+			},
+			"17" :
+			{
+				"type" : "cpuStart",
+				"size" : 10,
+				"owner" : 6,
+				"monsters" : "weak",
+				"playerTowns" : { "castles" : 1 },
+				"allowedTowns" : [ "castle", "rampart", "stronghold" ],
+				"mines" : { "wood" : 1, "ore" : 1, "gems" : 1 },
+				"treasureLikeZone" : 14
+			},
+			"18" :
+			{
+				"type" : "treasure",
+				"size" : 10,
+				"monsters" : "normal",
+				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
+				"terrainTypeLikeZone" : 10,
+				"mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1 },
+				"treasure" :
+				[
+					{ "min" : 5000, "max" : 10000, "density" : 4 },
+					{ "min" : 500, "max" : 3000, "density" : 9 },
+					{ "min" : 0, "max" : 0, "density" : 1 }
+				]
+			},
+			"19" :
+			{
+				"type" : "junction",
+				"size" : 6,
+				"monsters" : "weak",
+				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
+				"terrainTypeLikeZone" : 10,
+				"treasureLikeZone" : 5
+			},
+			"20" :
+			{
+				"type" : "treasure",
+				"size" : 8,
+				"monsters" : "normal",
+				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
+				"terrainTypeLikeZone" : 10,
+				"mines" : { "sulfur" : 1, "crystal" : 1, "gems" : 1 },
+				"treasureLikeZone" : 18
+			},
+			"21" :
+			{
+				"type" : "cpuStart",
+				"size" : 8,
+				"owner" : 5,
+				"monsters" : "weak",
+				"playerTowns" : { "castles" : 1 },
+				"allowedTowns" : [ "castle", "rampart", "stronghold" ],
+				"minesLikeZone" : 10,
+				"treasure" :
+				[
+					{ "min" : 5000, "max" : 10000, "density" : 3 },
+					{ "min" : 500, "max" : 3000, "density" : 10 },
+					{ "min" : 0, "max" : 0, "density" : 1 }
+				]
+			},
+			"22" :
+			{
+				"type" : "junction",
+				"size" : 6,
+				"monsters" : "weak",
+				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
+				"terrainTypeLikeZone" : 10,
+				"treasureLikeZone" : 5
+			},
+			"23" :
+			{
+				"type" : "treasure",
+				"size" : 8,
+				"monsters" : "weak",
+				"allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ],
+				"terrainTypeLikeZone" : 10,
+				"minesLikeZone" : 18,
+				"treasure" :
+				[
+					{ "min" : 8000, "max" : 12000, "density" : 2 },
+					{ "min" : 6000, "max" : 10000, "density" : 4 },
+					{ "min" : 1000, "max" : 5000, "density" : 8 }
+				]
+			},
+			"24" :
+			{
+				"type" : "cpuStart",
+				"size" : 8,
+				"owner" : 3,
+				"monsters" : "normal",
+				"playerTowns" : { "castles" : 1 },
+				"allowedTowns" : [ "castle", "rampart", "stronghold" ],
+				"minesLikeZone" : 17,
+				"treasure" :
+				[
+					{ "min" : 5000, "max" : 10000, "density" : 1 },
+					{ "min" : 4000, "max" : 8000, "density" : 3 },
+					{ "min" : 1000, "max" : 4000, "density" : 8 }
+				]
+			}
+		},
+		"connections" :
+		[
+			{ "a" : "1", "b" : "2", "guard" : 0 },
+			{ "a" : "1", "b" : "4", "guard" : 0 },
+			{ "a" : "1", "b" : "5", "guard" : 12500 }, // Border guard replaced with monster
+			{ "a" : "1", "b" : "15", "guard" : 0 },
+			{ "a" : "2", "b" : "3", "guard" : 0 },
+			{ "a" : "5", "b" : "6", "guard" : 0 },
+			{ "a" : "5", "b" : "21", "guard" : 12500 }, // Border guard replaced with monster
+			{ "a" : "6", "b" : "7", "guard" : 0 },
+			{ "a" : "7", "b" : "8", "guard" : 0 },
+			{ "a" : "8", "b" : "9", "guard" : 0 },
+			{ "a" : "9", "b" : "10", "guard" : 0 },
+			{ "a" : "10", "b" : "11", "guard" : 0 },
+			{ "a" : "10", "b" : "12", "guard" : 0 },
+			{ "a" : "12", "b" : "13", "guard" : 0 },
+			{ "a" : "13", "b" : "14", "guard" : 0 },
+			{ "a" : "14", "b" : "15", "guard" : 0 },
+			{ "a" : "15", "b" : "16", "guard" : 0 },
+			{ "a" : "16", "b" : "17", "guard" : 0 },
+			{ "a" : "17", "b" : "18", "guard" : 0 },
+			{ "a" : "18", "b" : "19", "guard" : 0 },
+			{ "a" : "18", "b" : "22", "guard" : 0 },
+			{ "a" : "19", "b" : "20", "guard" : 0 },
+			{ "a" : "20", "b" : "21", "guard" : 0 },
+			{ "a" : "22", "b" : "23", "guard" : 0 },
+			{ "a" : "23", "b" : "24", "guard" : 0 }
+		]
+	}
+}

+ 24 - 24
android/vcmi-app/src/main/java/eu/vcmi/vcmi/Const.java

@@ -1,24 +1,24 @@
-package eu.vcmi.vcmi;
-
-import android.content.Context;
-import android.os.Build;
-import android.os.Environment;
-
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStreamWriter;
-
-/**
- * @author F
- */
-public class Const
-{
-    public static final String JNI_METHOD_SUPPRESS = "unused"; // jni methods are marked as unused, because IDE doesn't understand jni calls
-    // used to disable lint errors about try-with-resources being unsupported on api <19 (it is supported, because retrolambda backports it)
-    public static final int SUPPRESS_TRY_WITH_RESOURCES_WARNING = Build.VERSION_CODES.KITKAT;
-
-    public static final String VCMI_DATA_ROOT_FOLDER_NAME = "vcmi-data";
-}
+package eu.vcmi.vcmi;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Environment;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+
+/**
+ * @author F
+ */
+public class Const
+{
+    public static final String JNI_METHOD_SUPPRESS = "unused"; // jni methods are marked as unused, because IDE doesn't understand jni calls
+    // used to disable lint errors about try-with-resources being unsupported on api <19 (it is supported, because retrolambda backports it)
+    public static final int SUPPRESS_TRY_WITH_RESOURCES_WARNING = Build.VERSION_CODES.KITKAT;
+
+    public static final String VCMI_DATA_ROOT_FOLDER_NAME = "vcmi-data";
+}

+ 115 - 115
client/CGameInfo.cpp

@@ -1,115 +1,115 @@
-/*
- * CGameInfo.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 "CGameInfo.h"
-
-#include "../lib/VCMI_Lib.h"
-
-const CGameInfo * CGI;
-CClientState * CCS = nullptr;
-CServerHandler * CSH;
-
-
-CGameInfo::CGameInfo()
-{
-	generaltexth = nullptr;
-	mh = nullptr;
-	townh = nullptr;
-	globalServices = nullptr;
-}
-
-void CGameInfo::setFromLib()
-{
-	globalServices = VLC;
-	modh = VLC->modh;
-	generaltexth = VLC->generaltexth;
-	creh = VLC->creh;
-	townh = VLC->townh;
-	heroh = VLC->heroh;
-	objh = VLC->objh;
-	spellh = VLC->spellh;
-	skillh = VLC->skillh;
-	objtypeh = VLC->objtypeh;
-	terrainTypeHandler = VLC->terrainTypeHandler;
-	battleFieldHandler = VLC->battlefieldsHandler;
-	obstacleHandler = VLC->obstacleHandler;
-}
-
-const ArtifactService * CGameInfo::artifacts() const
-{
-	return globalServices->artifacts();
-}
-
-const BattleFieldService * CGameInfo::battlefields() const
-{
-	return globalServices->battlefields();
-}
-
-const CreatureService * CGameInfo::creatures() const
-{
-	return globalServices->creatures();
-}
-
-const FactionService * CGameInfo::factions() const
-{
-	return globalServices->factions();
-}
-
-const HeroClassService * CGameInfo::heroClasses() const
-{
-	return globalServices->heroClasses();
-}
-
-const HeroTypeService * CGameInfo::heroTypes() const
-{
-	return globalServices->heroTypes();
-}
-
-#if SCRIPTING_ENABLED
-const scripting::Service * CGameInfo::scripts()  const
-{
-	return globalServices->scripts();
-}
-#endif
-
-const spells::Service * CGameInfo::spells()  const
-{
-	return globalServices->spells();
-}
-
-const SkillService * CGameInfo::skills() const
-{
-	return globalServices->skills();
-}
-
-const ObstacleService * CGameInfo::obstacles() const
-{
-	return globalServices->obstacles();
-}
-
-const IGameSettings * CGameInfo::settings() const
-{
-	return globalServices->settings();
-}
-
-void CGameInfo::updateEntity(Metatype metatype, int32_t index, const JsonNode & data)
-{
-	logGlobal->error("CGameInfo::updateEntity call is not expected.");
-}
-
-spells::effects::Registry * CGameInfo::spellEffects()
-{
-	return nullptr;
-}
-
-const spells::effects::Registry * CGameInfo::spellEffects() const
-{
-	return globalServices->spellEffects();
-}
+/*
+ * CGameInfo.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 "CGameInfo.h"
+
+#include "../lib/VCMI_Lib.h"
+
+const CGameInfo * CGI;
+CClientState * CCS = nullptr;
+CServerHandler * CSH;
+
+
+CGameInfo::CGameInfo()
+{
+	generaltexth = nullptr;
+	mh = nullptr;
+	townh = nullptr;
+	globalServices = nullptr;
+}
+
+void CGameInfo::setFromLib()
+{
+	globalServices = VLC;
+	modh = VLC->modh;
+	generaltexth = VLC->generaltexth;
+	creh = VLC->creh;
+	townh = VLC->townh;
+	heroh = VLC->heroh;
+	objh = VLC->objh;
+	spellh = VLC->spellh;
+	skillh = VLC->skillh;
+	objtypeh = VLC->objtypeh;
+	terrainTypeHandler = VLC->terrainTypeHandler;
+	battleFieldHandler = VLC->battlefieldsHandler;
+	obstacleHandler = VLC->obstacleHandler;
+}
+
+const ArtifactService * CGameInfo::artifacts() const
+{
+	return globalServices->artifacts();
+}
+
+const BattleFieldService * CGameInfo::battlefields() const
+{
+	return globalServices->battlefields();
+}
+
+const CreatureService * CGameInfo::creatures() const
+{
+	return globalServices->creatures();
+}
+
+const FactionService * CGameInfo::factions() const
+{
+	return globalServices->factions();
+}
+
+const HeroClassService * CGameInfo::heroClasses() const
+{
+	return globalServices->heroClasses();
+}
+
+const HeroTypeService * CGameInfo::heroTypes() const
+{
+	return globalServices->heroTypes();
+}
+
+#if SCRIPTING_ENABLED
+const scripting::Service * CGameInfo::scripts()  const
+{
+	return globalServices->scripts();
+}
+#endif
+
+const spells::Service * CGameInfo::spells()  const
+{
+	return globalServices->spells();
+}
+
+const SkillService * CGameInfo::skills() const
+{
+	return globalServices->skills();
+}
+
+const ObstacleService * CGameInfo::obstacles() const
+{
+	return globalServices->obstacles();
+}
+
+const IGameSettings * CGameInfo::settings() const
+{
+	return globalServices->settings();
+}
+
+void CGameInfo::updateEntity(Metatype metatype, int32_t index, const JsonNode & data)
+{
+	logGlobal->error("CGameInfo::updateEntity call is not expected.");
+}
+
+spells::effects::Registry * CGameInfo::spellEffects()
+{
+	return nullptr;
+}
+
+const spells::effects::Registry * CGameInfo::spellEffects() const
+{
+	return globalServices->spellEffects();
+}

+ 101 - 101
client/CGameInfo.h

@@ -1,101 +1,101 @@
-/*
- * CGameInfo.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 <vcmi/Services.h>
-
-#include "../lib/ConstTransitivePtr.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-class CModHandler;
-class CHeroHandler;
-class CCreatureHandler;
-class CSpellHandler;
-class CSkillHandler;
-class CBuildingHandler;
-class CObjectHandler;
-class CObjectClassesHandler;
-class CTownHandler;
-class CGeneralTextHandler;
-class CConsoleHandler;
-class CGameState;
-class BattleFieldHandler;
-class ObstacleHandler;
-class TerrainTypeHandler;
-
-class CMap;
-
-VCMI_LIB_NAMESPACE_END
-
-class CMapHandler;
-class CSoundHandler;
-class CMusicHandler;
-class CursorHandler;
-class IMainVideoPlayer;
-class CServerHandler;
-
-//a class for non-mechanical client GUI classes
-class CClientState
-{
-public:
-	CSoundHandler * soundh;
-	CMusicHandler * musich;
-	CConsoleHandler * consoleh;
-	CursorHandler * curh;
-	IMainVideoPlayer * videoh;
-};
-extern CClientState * CCS;
-
-/// CGameInfo class
-/// for allowing different functions for accessing game informations
-class CGameInfo : public Services
-{
-public:
-	const ArtifactService * artifacts() const override;
-	const CreatureService * creatures() const override;
-	const FactionService * factions() const override;
-	const HeroClassService * heroClasses() const override;
-	const HeroTypeService * heroTypes() const override;
-#if SCRIPTING_ENABLED
-	const scripting::Service * scripts() const override;
-#endif
-	const spells::Service * spells() const override;
-	const SkillService * skills() const override;
-	const BattleFieldService * battlefields() const override;
-	const ObstacleService * obstacles() const override;
-	const IGameSettings * settings() const override;
-
-	void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) override;
-
-	const spells::effects::Registry * spellEffects() const override;
-	spells::effects::Registry * spellEffects() override;
-
-	ConstTransitivePtr<CModHandler> modh; //public?
-	ConstTransitivePtr<BattleFieldHandler> battleFieldHandler;
-	ConstTransitivePtr<CHeroHandler> heroh;
-	ConstTransitivePtr<CCreatureHandler> creh;
-	ConstTransitivePtr<CSpellHandler> spellh;
-	ConstTransitivePtr<CSkillHandler> skillh;
-	ConstTransitivePtr<CObjectHandler> objh;
-	ConstTransitivePtr<TerrainTypeHandler> terrainTypeHandler;
-	ConstTransitivePtr<CObjectClassesHandler> objtypeh;
-	ConstTransitivePtr<ObstacleHandler> obstacleHandler;
-	CGeneralTextHandler * generaltexth;
-	CMapHandler * mh;
-	CTownHandler * townh;
-
-	void setFromLib();
-
-	CGameInfo();
-private:
-	const Services * globalServices;
-};
-extern const CGameInfo* CGI;
+/*
+ * CGameInfo.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 <vcmi/Services.h>
+
+#include "../lib/ConstTransitivePtr.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class CModHandler;
+class CHeroHandler;
+class CCreatureHandler;
+class CSpellHandler;
+class CSkillHandler;
+class CBuildingHandler;
+class CObjectHandler;
+class CObjectClassesHandler;
+class CTownHandler;
+class CGeneralTextHandler;
+class CConsoleHandler;
+class CGameState;
+class BattleFieldHandler;
+class ObstacleHandler;
+class TerrainTypeHandler;
+
+class CMap;
+
+VCMI_LIB_NAMESPACE_END
+
+class CMapHandler;
+class CSoundHandler;
+class CMusicHandler;
+class CursorHandler;
+class IMainVideoPlayer;
+class CServerHandler;
+
+//a class for non-mechanical client GUI classes
+class CClientState
+{
+public:
+	CSoundHandler * soundh;
+	CMusicHandler * musich;
+	CConsoleHandler * consoleh;
+	CursorHandler * curh;
+	IMainVideoPlayer * videoh;
+};
+extern CClientState * CCS;
+
+/// CGameInfo class
+/// for allowing different functions for accessing game informations
+class CGameInfo : public Services
+{
+public:
+	const ArtifactService * artifacts() const override;
+	const CreatureService * creatures() const override;
+	const FactionService * factions() const override;
+	const HeroClassService * heroClasses() const override;
+	const HeroTypeService * heroTypes() const override;
+#if SCRIPTING_ENABLED
+	const scripting::Service * scripts() const override;
+#endif
+	const spells::Service * spells() const override;
+	const SkillService * skills() const override;
+	const BattleFieldService * battlefields() const override;
+	const ObstacleService * obstacles() const override;
+	const IGameSettings * settings() const override;
+
+	void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) override;
+
+	const spells::effects::Registry * spellEffects() const override;
+	spells::effects::Registry * spellEffects() override;
+
+	ConstTransitivePtr<CModHandler> modh; //public?
+	ConstTransitivePtr<BattleFieldHandler> battleFieldHandler;
+	ConstTransitivePtr<CHeroHandler> heroh;
+	ConstTransitivePtr<CCreatureHandler> creh;
+	ConstTransitivePtr<CSpellHandler> spellh;
+	ConstTransitivePtr<CSkillHandler> skillh;
+	ConstTransitivePtr<CObjectHandler> objh;
+	ConstTransitivePtr<TerrainTypeHandler> terrainTypeHandler;
+	ConstTransitivePtr<CObjectClassesHandler> objtypeh;
+	ConstTransitivePtr<ObstacleHandler> obstacleHandler;
+	CGeneralTextHandler * generaltexth;
+	CMapHandler * mh;
+	CTownHandler * townh;
+
+	void setFromLib();
+
+	CGameInfo();
+private:
+	const Services * globalServices;
+};
+extern const CGameInfo* CGI;

+ 529 - 529
client/CMT.cpp

@@ -1,529 +1,529 @@
-/*
- * CMT.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
- *
- */
-
-// CMT.cpp : Defines the entry point for the console application.
-#include "StdInc.h"
-#include "CMT.h"
-
-#include "CGameInfo.h"
-#include "mainmenu/CMainMenu.h"
-#include "gui/CursorHandler.h"
-#include "eventsSDL/InputHandler.h"
-#include "CPlayerInterface.h"
-#include "CVideoHandler.h"
-#include "CMusicHandler.h"
-#include "gui/CGuiHandler.h"
-#include "gui/WindowHandler.h"
-#include "CServerHandler.h"
-#include "ClientCommandManager.h"
-#include "windows/CMessage.h"
-#include "render/IScreenHandler.h"
-#include "render/Graphics.h"
-
-#include "../lib/CConfigHandler.h"
-#include "../lib/CGeneralTextHandler.h"
-#include "../lib/CThreadHelper.h"
-#include "../lib/VCMIDirs.h"
-#include "../lib/VCMI_Lib.h"
-#include "../lib/filesystem/Filesystem.h"
-
-#include "../lib/logging/CBasicLogConfigurator.h"
-
-#include <boost/program_options.hpp>
-#include <vstd/StringUtils.h>
-
-#include <SDL_main.h>
-#include <SDL.h>
-
-#ifdef VCMI_ANDROID
-#include "../lib/CAndroidVMHelper.h"
-#include <SDL_system.h>
-#endif
-
-#if __MINGW32__
-#undef main
-#endif
-
-namespace po = boost::program_options;
-namespace po_style = boost::program_options::command_line_style;
-
-static po::variables_map vm;
-
-#ifndef VCMI_IOS
-void processCommand(const std::string &message);
-#endif
-void playIntro();
-static void mainLoop();
-
-static CBasicLogConfigurator *logConfig;
-
-void init()
-{
-	CStopWatch tmh;
-
-	loadDLLClasses();
-	const_cast<CGameInfo*>(CGI)->setFromLib();
-
-	logGlobal->info("Initializing VCMI_Lib: %d ms", tmh.getDiff());
-
-	// Debug code to load all maps on start
-	//ClientCommandManager commandController;
-	//commandController.processCommand("convert txt", false);
-}
-
-static void prog_version()
-{
-	printf("%s\n", GameConstants::VCMI_VERSION.c_str());
-	std::cout << VCMIDirs::get().genHelpString();
-}
-
-static void prog_help(const po::options_description &opts)
-{
-	auto time = std::time(0);
-	printf("%s - A Heroes of Might and Magic 3 clone\n", GameConstants::VCMI_VERSION.c_str());
-	printf("Copyright (C) 2007-%d VCMI dev team - see AUTHORS file\n", std::localtime(&time)->tm_year + 1900);
-	printf("This is free software; see the source for copying conditions. There is NO\n");
-	printf("warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n");
-	printf("\n");
-	std::cout << opts;
-}
-
-#if defined(VCMI_WINDOWS) && !defined(__GNUC__) && defined(VCMI_WITH_DEBUG_CONSOLE)
-int wmain(int argc, wchar_t* argv[])
-#elif defined(VCMI_MOBILE)
-int SDL_main(int argc, char *argv[])
-#else
-int main(int argc, char * argv[])
-#endif
-{
-#ifdef VCMI_ANDROID
-	CAndroidVMHelper::initClassloader(SDL_AndroidGetJNIEnv());
-	// boost will crash without this
-	setenv("LANG", "C", 1);
-#endif
-
-#if !defined(VCMI_MOBILE)
-	// Correct working dir executable folder (not bundle folder) so we can use executable relative paths
-	boost::filesystem::current_path(boost::filesystem::system_complete(argv[0]).parent_path());
-#endif
-	std::cout << "Starting... " << std::endl;
-	po::options_description opts("Allowed options");
-	opts.add_options()
-		("help,h", "display help and exit")
-		("version,v", "display version information and exit")
-		("testmap", po::value<std::string>(), "")
-		("testsave", po::value<std::string>(), "")
-		("spectate,s", "enable spectator interface for AI-only games")
-		("spectate-ignore-hero", "wont follow heroes on adventure map")
-		("spectate-hero-speed", po::value<int>(), "hero movement speed on adventure map")
-		("spectate-battle-speed", po::value<int>(), "battle animation speed for spectator")
-		("spectate-skip-battle", "skip battles in spectator view")
-		("spectate-skip-battle-result", "skip battle result window")
-		("onlyAI", "allow one to run without human player, all players will be default AI")
-		("headless", "runs without GUI, implies --onlyAI")
-		("ai", po::value<std::vector<std::string>>(), "AI to be used for the player, can be specified several times for the consecutive players")
-		("oneGoodAI", "puts one default AI and the rest will be EmptyAI")
-		("autoSkip", "automatically skip turns in GUI")
-		("disable-video", "disable video player")
-		("nointro,i", "skips intro movies")
-		("donotstartserver,d","do not attempt to start server and just connect to it instead server")
-		("serverport", po::value<si64>(), "override port specified in config file")
-		("savefrequency", po::value<si64>(), "limit auto save creation to each N days")
-		("lobby", "parameters address, port, uuid to connect ro remote lobby session")
-		("lobby-address", po::value<std::string>(), "address to remote lobby")
-		("lobby-port", po::value<ui16>(), "port to remote lobby")
-		("lobby-host", "if this client hosts session")
-		("lobby-uuid", po::value<std::string>(), "uuid to the server")
-		("lobby-connections", po::value<ui16>(), "connections of server")
-		("lobby-username", po::value<std::string>(), "player name")
-		("lobby-gamemode", po::value<ui16>(), "use 0 for new game and 1 for load game")
-		("uuid", po::value<std::string>(), "uuid for the client");
-
-	if(argc > 1)
-	{
-		try
-		{
-			po::store(po::parse_command_line(argc, argv, opts, po_style::unix_style|po_style::case_insensitive), vm);
-		}
-		catch(boost::program_options::error &e)
-		{
-			std::cerr << "Failure during parsing command-line options:\n" << e.what() << std::endl;
-		}
-	}
-
-	po::notify(vm);
-	if(vm.count("help"))
-	{
-		prog_help(opts);
-#ifdef VCMI_IOS
-		exit(0);
-#else
-		return 0;
-#endif
-	}
-	if(vm.count("version"))
-	{
-		prog_version();
-#ifdef VCMI_IOS
-		exit(0);
-#else
-		return 0;
-#endif
-	}
-
-	// Init old logging system and new (temporary) logging system
-	CStopWatch total, pomtime;
-	std::cout.flags(std::ios::unitbuf);
-#ifndef VCMI_IOS
-	console = new CConsoleHandler();
-
-	auto callbackFunction = [](std::string buffer, bool calledFromIngameConsole)
-	{
-		ClientCommandManager commandController;
-		commandController.processCommand(buffer, calledFromIngameConsole);
-	};
-
-	*console->cb = callbackFunction;
-	console->start();
-#endif
-
-	const boost::filesystem::path logPath = VCMIDirs::get().userLogsPath() / "VCMI_Client_log.txt";
-	logConfig = new CBasicLogConfigurator(logPath, console);
-	logConfig->configureDefault();
-	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);
-
-	// Init filesystem and settings
-	preinitDLL(::console);
-
-	Settings session = settings.write["session"];
-	auto setSettingBool = [](std::string key, std::string arg) {
-		Settings s = settings.write(vstd::split(key, "/"));
-		if(::vm.count(arg))
-			s->Bool() = true;
-		else if(s->isNull())
-			s->Bool() = false;
-	};
-	auto setSettingInteger = [](std::string key, std::string arg, si64 defaultValue) {
-		Settings s = settings.write(vstd::split(key, "/"));
-		if(::vm.count(arg))
-			s->Integer() = ::vm[arg].as<si64>();
-		else if(s->isNull())
-			s->Integer() = defaultValue;
-	};
-
-	setSettingBool("session/onlyai", "onlyAI");
-	if(vm.count("headless"))
-	{
-		session["headless"].Bool() = true;
-		session["onlyai"].Bool() = true;
-	}
-	else if(vm.count("spectate"))
-	{
-		session["spectate"].Bool() = true;
-		session["spectate-ignore-hero"].Bool() = vm.count("spectate-ignore-hero");
-		session["spectate-skip-battle"].Bool() = vm.count("spectate-skip-battle");
-		session["spectate-skip-battle-result"].Bool() = vm.count("spectate-skip-battle-result");
-		if(vm.count("spectate-hero-speed"))
-			session["spectate-hero-speed"].Integer() = vm["spectate-hero-speed"].as<int>();
-		if(vm.count("spectate-battle-speed"))
-			session["spectate-battle-speed"].Float() = vm["spectate-battle-speed"].as<int>();
-	}
-	// Server settings
-	setSettingBool("session/donotstartserver", "donotstartserver");
-
-	// Init special testing settings
-	setSettingInteger("session/serverport", "serverport", 0);
-	setSettingInteger("general/saveFrequency", "savefrequency", 1);
-
-	// Initialize logging based on settings
-	logConfig->configure();
-	logGlobal->debug("settings = %s", settings.toJsonNode().toJson());
-
-	// Some basic data validation to produce better error messages in cases of incorrect install
-	auto testFile = [](std::string filename, std::string message)
-	{
-		if (!CResourceHandler::get()->existsResource(ResourcePath(filename)))
-			handleFatalError(message, false);
-	};
-
-	testFile("DATA/HELP.TXT", "VCMI requires Heroes III: Shadow of Death or Heroes III: Complete data files to run!");
-	testFile("MODS/VCMI/MOD.JSON", "VCMI installation is corrupted! Built-in mod was not found!");
-	testFile("DATA/PLAYERS.PAL", "Heroes III data files are missing or corruped! Please reinstall them.");
-	testFile("SPRITES/DEFAULT.DEF", "Heroes III data files are missing or corruped! Please reinstall them.");
-	testFile("DATA/TENTCOLR.TXT", "Heroes III: Restoration of Erathia (including HD Edition) data files are not supported!");
-
-	srand ( (unsigned int)time(nullptr) );
-
-	if(!settings["session"]["headless"].Bool())
-		GH.init();
-
-	CCS = new CClientState();
-	CGI = new CGameInfo(); //contains all global informations about game (texts, lodHandlers, map handler etc.)
-	CSH = new CServerHandler();
-	
-	// Initialize video
-#ifdef DISABLE_VIDEO
-	CCS->videoh = new CEmptyVideoPlayer();
-#else
-	if (!settings["session"]["headless"].Bool() && !vm.count("disable-video"))
-		CCS->videoh = new CVideoPlayer();
-	else
-		CCS->videoh = new CEmptyVideoPlayer();
-#endif
-
-	logGlobal->info("\tInitializing video: %d ms", pomtime.getDiff());
-
-	if(!settings["session"]["headless"].Bool())
-	{
-		//initializing audio
-		CCS->soundh = new CSoundHandler();
-		CCS->soundh->init();
-		CCS->soundh->setVolume((ui32)settings["general"]["sound"].Float());
-		CCS->musich = new CMusicHandler();
-		CCS->musich->init();
-		CCS->musich->setVolume((ui32)settings["general"]["music"].Float());
-		logGlobal->info("Initializing screen and sound handling: %d ms", pomtime.getDiff());
-	}
-
-#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([]()
-	{
-		setThreadName("initialize");
-		init();
-	});
-#else
-	init();
-#endif
-
-	if(!settings["session"]["headless"].Bool())
-	{
-		if(!vm.count("battle") && !vm.count("nointro") && settings["video"]["showIntro"].Bool())
-			playIntro();
-		GH.screenHandler().clearScreen();
-	}
-
-
-#ifndef VCMI_NO_THREADED_LOAD
-	#ifdef VCMI_ANDROID // android loads the data quite slowly so we display native progressbar to prevent having only black screen for few seconds
-	{
-		CAndroidVMHelper vmHelper;
-		vmHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "showProgress");
-	#endif // ANDROID
-		loading.join();
-	#ifdef VCMI_ANDROID
-		vmHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "hideProgress");
-	}
-	#endif // ANDROID
-#endif // THREADED
-
-	if(!settings["session"]["headless"].Bool())
-	{
-		pomtime.getDiff();
-		graphics = new Graphics(); // should be before curh
-
-		CCS->curh = new CursorHandler();
-		logGlobal->info("Screen handler: %d ms", pomtime.getDiff());
-
-		CMessage::init();
-		logGlobal->info("Message handler: %d ms", pomtime.getDiff());
-
-		CCS->curh->show();
-	}
-
-	logGlobal->info("Initialization of VCMI (together): %d ms", total.getDiff());
-
-	session["autoSkip"].Bool()  = vm.count("autoSkip");
-	session["oneGoodAI"].Bool() = vm.count("oneGoodAI");
-	session["aiSolo"].Bool() = false;
-	std::shared_ptr<CMainMenu> mmenu;
-	
-	if(vm.count("testmap"))
-	{
-		session["testmap"].String() = vm["testmap"].as<std::string>();
-		session["onlyai"].Bool() = true;
-		boost::thread(&CServerHandler::debugStartTest, CSH, session["testmap"].String(), false);
-	}
-	else if(vm.count("testsave"))
-	{
-		session["testsave"].String() = vm["testsave"].as<std::string>();
-		session["onlyai"].Bool() = true;
-		boost::thread(&CServerHandler::debugStartTest, CSH, session["testsave"].String(), true);
-	}
-	else
-	{
-		mmenu = CMainMenu::create();
-		GH.curInt = mmenu.get();
-	}
-	
-	std::vector<std::string> names;
-	session["lobby"].Bool() = false;
-	if(vm.count("lobby"))
-	{
-		session["lobby"].Bool() = true;
-		session["host"].Bool() = false;
-		session["address"].String() = vm["lobby-address"].as<std::string>();
-		if(vm.count("lobby-username"))
-			session["username"].String() = vm["lobby-username"].as<std::string>();
-		else
-			session["username"].String() = settings["launcher"]["lobbyUsername"].String();
-		if(vm.count("lobby-gamemode"))
-			session["gamemode"].Integer() = vm["lobby-gamemode"].as<ui16>();
-		else
-			session["gamemode"].Integer() = 0;
-		CSH->uuid = vm["uuid"].as<std::string>();
-		session["port"].Integer() = vm["lobby-port"].as<ui16>();
-		logGlobal->info("Remote lobby mode at %s:%d, uuid is %s", session["address"].String(), session["port"].Integer(), CSH->uuid);
-		if(vm.count("lobby-host"))
-		{
-			session["host"].Bool() = true;
-			session["hostConnections"].String() = std::to_string(vm["lobby-connections"].as<ui16>());
-			session["hostUuid"].String() = vm["lobby-uuid"].as<std::string>();
-			logGlobal->info("This client will host session, server uuid is %s", session["hostUuid"].String());
-		}
-		
-		//we should not reconnect to previous game in online mode
-		Settings saveSession = settings.write["server"]["reconnect"];
-		saveSession->Bool() = false;
-		
-		//start lobby immediately
-		names.push_back(session["username"].String());
-		ESelectionScreen sscreen = session["gamemode"].Integer() == 0 ? ESelectionScreen::newGame : ESelectionScreen::loadGame;
-		mmenu->openLobby(sscreen, session["host"].Bool(), &names, ELoadMode::MULTI);
-	}
-	
-	// Restore remote session - start game immediately
-	if(settings["server"]["reconnect"].Bool())
-	{
-		CSH->restoreLastSession();
-	}
-
-	if(!settings["session"]["headless"].Bool())
-	{
-		mainLoop();
-	}
-	else
-	{
-		while(true)
-			boost::this_thread::sleep_for(boost::chrono::milliseconds(1000));
-	}
-
-	return 0;
-}
-
-//plays intro, ends when intro is over or button has been pressed (handles events)
-void playIntro()
-{
-	auto audioData = CCS->videoh->getAudio(VideoPath::builtin("3DOLOGO.SMK"));
-	int sound = CCS->soundh->playSound(audioData);
-	if(CCS->videoh->openAndPlayVideo(VideoPath::builtin("3DOLOGO.SMK"), 0, 1, true, true))
-	{
-		audioData = CCS->videoh->getAudio(VideoPath::builtin("NWCLOGO.SMK"));
-		sound = CCS->soundh->playSound(audioData);
-		if (CCS->videoh->openAndPlayVideo(VideoPath::builtin("NWCLOGO.SMK"), 0, 1, true, true))
-		{
-			audioData = CCS->videoh->getAudio(VideoPath::builtin("H3INTRO.SMK"));
-			sound = CCS->soundh->playSound(audioData);
-			CCS->videoh->openAndPlayVideo(VideoPath::builtin("H3INTRO.SMK"), 0, 1, true, true);
-		}
-	}
-	CCS->soundh->stopSound(sound);
-}
-
-static void mainLoop()
-{
-	setThreadName("MainGUI");
-
-	while(1) //main SDL events loop
-	{
-		GH.input().fetchEvents();
-		CSH->applyPacksOnLobbyScreen();
-		GH.renderFrame();
-	}
-}
-
-static void quitApplication()
-{
-	if(!settings["session"]["headless"].Bool())
-	{
-		if(CSH->client)
-			CSH->endGameplay();
-	}
-
-	GH.windows().clear();
-
-	CMM.reset();
-
-	if(!settings["session"]["headless"].Bool())
-	{
-		// cleanup, mostly to remove false leaks from analyzer
-		if(CCS)
-		{
-			CCS->musich->release();
-			CCS->soundh->release();
-
-			vstd::clear_pointer(CCS);
-		}
-		CMessage::dispose();
-
-		vstd::clear_pointer(graphics);
-	}
-
-	vstd::clear_pointer(VLC);
-
-	vstd::clear_pointer(console);// should be removed after everything else since used by logging
-
-	if(!settings["session"]["headless"].Bool())
-		GH.screenHandler().close();
-
-	if(logConfig != nullptr)
-	{
-		logConfig->deconfigure();
-		delete logConfig;
-		logConfig = nullptr;
-	}
-
-	std::cout << "Ending...\n";
-
-	// this method is always called from event/network threads, which keep interface mutex locked
-	// unlock it here to avoid assertion failure on GH destruction in exit()
-	GH.interfaceMutex.unlock();
-	exit(0);
-}
-
-void handleQuit(bool ask)
-{
-	if(CSH->client && LOCPLINT && ask)
-	{
-		CCS->curh->set(Cursor::Map::POINTER);
-		LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[69], quitApplication, nullptr);
-	}
-	else
-	{
-		quitApplication();
-	}
-}
-
-void handleFatalError(const std::string & message, bool terminate)
-{
-	logGlobal->error("FATAL ERROR ENCOUTERED, VCMI WILL NOW TERMINATE");
-	logGlobal->error("Reason: %s", message);
-
-	std::string messageToShow = "Fatal error! " + message;
-
-	SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Fatal error!", messageToShow.c_str(), nullptr);
-
-	if (terminate)
-		throw std::runtime_error(message);
-	else
-		exit(1);
-}
+/*
+ * CMT.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
+ *
+ */
+
+// CMT.cpp : Defines the entry point for the console application.
+#include "StdInc.h"
+#include "CMT.h"
+
+#include "CGameInfo.h"
+#include "mainmenu/CMainMenu.h"
+#include "gui/CursorHandler.h"
+#include "eventsSDL/InputHandler.h"
+#include "CPlayerInterface.h"
+#include "CVideoHandler.h"
+#include "CMusicHandler.h"
+#include "gui/CGuiHandler.h"
+#include "gui/WindowHandler.h"
+#include "CServerHandler.h"
+#include "ClientCommandManager.h"
+#include "windows/CMessage.h"
+#include "render/IScreenHandler.h"
+#include "render/Graphics.h"
+
+#include "../lib/CConfigHandler.h"
+#include "../lib/CGeneralTextHandler.h"
+#include "../lib/CThreadHelper.h"
+#include "../lib/VCMIDirs.h"
+#include "../lib/VCMI_Lib.h"
+#include "../lib/filesystem/Filesystem.h"
+
+#include "../lib/logging/CBasicLogConfigurator.h"
+
+#include <boost/program_options.hpp>
+#include <vstd/StringUtils.h>
+
+#include <SDL_main.h>
+#include <SDL.h>
+
+#ifdef VCMI_ANDROID
+#include "../lib/CAndroidVMHelper.h"
+#include <SDL_system.h>
+#endif
+
+#if __MINGW32__
+#undef main
+#endif
+
+namespace po = boost::program_options;
+namespace po_style = boost::program_options::command_line_style;
+
+static po::variables_map vm;
+
+#ifndef VCMI_IOS
+void processCommand(const std::string &message);
+#endif
+void playIntro();
+static void mainLoop();
+
+static CBasicLogConfigurator *logConfig;
+
+void init()
+{
+	CStopWatch tmh;
+
+	loadDLLClasses();
+	const_cast<CGameInfo*>(CGI)->setFromLib();
+
+	logGlobal->info("Initializing VCMI_Lib: %d ms", tmh.getDiff());
+
+	// Debug code to load all maps on start
+	//ClientCommandManager commandController;
+	//commandController.processCommand("convert txt", false);
+}
+
+static void prog_version()
+{
+	printf("%s\n", GameConstants::VCMI_VERSION.c_str());
+	std::cout << VCMIDirs::get().genHelpString();
+}
+
+static void prog_help(const po::options_description &opts)
+{
+	auto time = std::time(0);
+	printf("%s - A Heroes of Might and Magic 3 clone\n", GameConstants::VCMI_VERSION.c_str());
+	printf("Copyright (C) 2007-%d VCMI dev team - see AUTHORS file\n", std::localtime(&time)->tm_year + 1900);
+	printf("This is free software; see the source for copying conditions. There is NO\n");
+	printf("warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n");
+	printf("\n");
+	std::cout << opts;
+}
+
+#if defined(VCMI_WINDOWS) && !defined(__GNUC__) && defined(VCMI_WITH_DEBUG_CONSOLE)
+int wmain(int argc, wchar_t* argv[])
+#elif defined(VCMI_MOBILE)
+int SDL_main(int argc, char *argv[])
+#else
+int main(int argc, char * argv[])
+#endif
+{
+#ifdef VCMI_ANDROID
+	CAndroidVMHelper::initClassloader(SDL_AndroidGetJNIEnv());
+	// boost will crash without this
+	setenv("LANG", "C", 1);
+#endif
+
+#if !defined(VCMI_MOBILE)
+	// Correct working dir executable folder (not bundle folder) so we can use executable relative paths
+	boost::filesystem::current_path(boost::filesystem::system_complete(argv[0]).parent_path());
+#endif
+	std::cout << "Starting... " << std::endl;
+	po::options_description opts("Allowed options");
+	opts.add_options()
+		("help,h", "display help and exit")
+		("version,v", "display version information and exit")
+		("testmap", po::value<std::string>(), "")
+		("testsave", po::value<std::string>(), "")
+		("spectate,s", "enable spectator interface for AI-only games")
+		("spectate-ignore-hero", "wont follow heroes on adventure map")
+		("spectate-hero-speed", po::value<int>(), "hero movement speed on adventure map")
+		("spectate-battle-speed", po::value<int>(), "battle animation speed for spectator")
+		("spectate-skip-battle", "skip battles in spectator view")
+		("spectate-skip-battle-result", "skip battle result window")
+		("onlyAI", "allow one to run without human player, all players will be default AI")
+		("headless", "runs without GUI, implies --onlyAI")
+		("ai", po::value<std::vector<std::string>>(), "AI to be used for the player, can be specified several times for the consecutive players")
+		("oneGoodAI", "puts one default AI and the rest will be EmptyAI")
+		("autoSkip", "automatically skip turns in GUI")
+		("disable-video", "disable video player")
+		("nointro,i", "skips intro movies")
+		("donotstartserver,d","do not attempt to start server and just connect to it instead server")
+		("serverport", po::value<si64>(), "override port specified in config file")
+		("savefrequency", po::value<si64>(), "limit auto save creation to each N days")
+		("lobby", "parameters address, port, uuid to connect ro remote lobby session")
+		("lobby-address", po::value<std::string>(), "address to remote lobby")
+		("lobby-port", po::value<ui16>(), "port to remote lobby")
+		("lobby-host", "if this client hosts session")
+		("lobby-uuid", po::value<std::string>(), "uuid to the server")
+		("lobby-connections", po::value<ui16>(), "connections of server")
+		("lobby-username", po::value<std::string>(), "player name")
+		("lobby-gamemode", po::value<ui16>(), "use 0 for new game and 1 for load game")
+		("uuid", po::value<std::string>(), "uuid for the client");
+
+	if(argc > 1)
+	{
+		try
+		{
+			po::store(po::parse_command_line(argc, argv, opts, po_style::unix_style|po_style::case_insensitive), vm);
+		}
+		catch(boost::program_options::error &e)
+		{
+			std::cerr << "Failure during parsing command-line options:\n" << e.what() << std::endl;
+		}
+	}
+
+	po::notify(vm);
+	if(vm.count("help"))
+	{
+		prog_help(opts);
+#ifdef VCMI_IOS
+		exit(0);
+#else
+		return 0;
+#endif
+	}
+	if(vm.count("version"))
+	{
+		prog_version();
+#ifdef VCMI_IOS
+		exit(0);
+#else
+		return 0;
+#endif
+	}
+
+	// Init old logging system and new (temporary) logging system
+	CStopWatch total, pomtime;
+	std::cout.flags(std::ios::unitbuf);
+#ifndef VCMI_IOS
+	console = new CConsoleHandler();
+
+	auto callbackFunction = [](std::string buffer, bool calledFromIngameConsole)
+	{
+		ClientCommandManager commandController;
+		commandController.processCommand(buffer, calledFromIngameConsole);
+	};
+
+	*console->cb = callbackFunction;
+	console->start();
+#endif
+
+	const boost::filesystem::path logPath = VCMIDirs::get().userLogsPath() / "VCMI_Client_log.txt";
+	logConfig = new CBasicLogConfigurator(logPath, console);
+	logConfig->configureDefault();
+	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);
+
+	// Init filesystem and settings
+	preinitDLL(::console);
+
+	Settings session = settings.write["session"];
+	auto setSettingBool = [](std::string key, std::string arg) {
+		Settings s = settings.write(vstd::split(key, "/"));
+		if(::vm.count(arg))
+			s->Bool() = true;
+		else if(s->isNull())
+			s->Bool() = false;
+	};
+	auto setSettingInteger = [](std::string key, std::string arg, si64 defaultValue) {
+		Settings s = settings.write(vstd::split(key, "/"));
+		if(::vm.count(arg))
+			s->Integer() = ::vm[arg].as<si64>();
+		else if(s->isNull())
+			s->Integer() = defaultValue;
+	};
+
+	setSettingBool("session/onlyai", "onlyAI");
+	if(vm.count("headless"))
+	{
+		session["headless"].Bool() = true;
+		session["onlyai"].Bool() = true;
+	}
+	else if(vm.count("spectate"))
+	{
+		session["spectate"].Bool() = true;
+		session["spectate-ignore-hero"].Bool() = vm.count("spectate-ignore-hero");
+		session["spectate-skip-battle"].Bool() = vm.count("spectate-skip-battle");
+		session["spectate-skip-battle-result"].Bool() = vm.count("spectate-skip-battle-result");
+		if(vm.count("spectate-hero-speed"))
+			session["spectate-hero-speed"].Integer() = vm["spectate-hero-speed"].as<int>();
+		if(vm.count("spectate-battle-speed"))
+			session["spectate-battle-speed"].Float() = vm["spectate-battle-speed"].as<int>();
+	}
+	// Server settings
+	setSettingBool("session/donotstartserver", "donotstartserver");
+
+	// Init special testing settings
+	setSettingInteger("session/serverport", "serverport", 0);
+	setSettingInteger("general/saveFrequency", "savefrequency", 1);
+
+	// Initialize logging based on settings
+	logConfig->configure();
+	logGlobal->debug("settings = %s", settings.toJsonNode().toJson());
+
+	// Some basic data validation to produce better error messages in cases of incorrect install
+	auto testFile = [](std::string filename, std::string message)
+	{
+		if (!CResourceHandler::get()->existsResource(ResourcePath(filename)))
+			handleFatalError(message, false);
+	};
+
+	testFile("DATA/HELP.TXT", "VCMI requires Heroes III: Shadow of Death or Heroes III: Complete data files to run!");
+	testFile("MODS/VCMI/MOD.JSON", "VCMI installation is corrupted! Built-in mod was not found!");
+	testFile("DATA/PLAYERS.PAL", "Heroes III data files are missing or corruped! Please reinstall them.");
+	testFile("SPRITES/DEFAULT.DEF", "Heroes III data files are missing or corruped! Please reinstall them.");
+	testFile("DATA/TENTCOLR.TXT", "Heroes III: Restoration of Erathia (including HD Edition) data files are not supported!");
+
+	srand ( (unsigned int)time(nullptr) );
+
+	if(!settings["session"]["headless"].Bool())
+		GH.init();
+
+	CCS = new CClientState();
+	CGI = new CGameInfo(); //contains all global informations about game (texts, lodHandlers, map handler etc.)
+	CSH = new CServerHandler();
+	
+	// Initialize video
+#ifdef DISABLE_VIDEO
+	CCS->videoh = new CEmptyVideoPlayer();
+#else
+	if (!settings["session"]["headless"].Bool() && !vm.count("disable-video"))
+		CCS->videoh = new CVideoPlayer();
+	else
+		CCS->videoh = new CEmptyVideoPlayer();
+#endif
+
+	logGlobal->info("\tInitializing video: %d ms", pomtime.getDiff());
+
+	if(!settings["session"]["headless"].Bool())
+	{
+		//initializing audio
+		CCS->soundh = new CSoundHandler();
+		CCS->soundh->init();
+		CCS->soundh->setVolume((ui32)settings["general"]["sound"].Float());
+		CCS->musich = new CMusicHandler();
+		CCS->musich->init();
+		CCS->musich->setVolume((ui32)settings["general"]["music"].Float());
+		logGlobal->info("Initializing screen and sound handling: %d ms", pomtime.getDiff());
+	}
+
+#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([]()
+	{
+		setThreadName("initialize");
+		init();
+	});
+#else
+	init();
+#endif
+
+	if(!settings["session"]["headless"].Bool())
+	{
+		if(!vm.count("battle") && !vm.count("nointro") && settings["video"]["showIntro"].Bool())
+			playIntro();
+		GH.screenHandler().clearScreen();
+	}
+
+
+#ifndef VCMI_NO_THREADED_LOAD
+	#ifdef VCMI_ANDROID // android loads the data quite slowly so we display native progressbar to prevent having only black screen for few seconds
+	{
+		CAndroidVMHelper vmHelper;
+		vmHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "showProgress");
+	#endif // ANDROID
+		loading.join();
+	#ifdef VCMI_ANDROID
+		vmHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "hideProgress");
+	}
+	#endif // ANDROID
+#endif // THREADED
+
+	if(!settings["session"]["headless"].Bool())
+	{
+		pomtime.getDiff();
+		graphics = new Graphics(); // should be before curh
+
+		CCS->curh = new CursorHandler();
+		logGlobal->info("Screen handler: %d ms", pomtime.getDiff());
+
+		CMessage::init();
+		logGlobal->info("Message handler: %d ms", pomtime.getDiff());
+
+		CCS->curh->show();
+	}
+
+	logGlobal->info("Initialization of VCMI (together): %d ms", total.getDiff());
+
+	session["autoSkip"].Bool()  = vm.count("autoSkip");
+	session["oneGoodAI"].Bool() = vm.count("oneGoodAI");
+	session["aiSolo"].Bool() = false;
+	std::shared_ptr<CMainMenu> mmenu;
+	
+	if(vm.count("testmap"))
+	{
+		session["testmap"].String() = vm["testmap"].as<std::string>();
+		session["onlyai"].Bool() = true;
+		boost::thread(&CServerHandler::debugStartTest, CSH, session["testmap"].String(), false);
+	}
+	else if(vm.count("testsave"))
+	{
+		session["testsave"].String() = vm["testsave"].as<std::string>();
+		session["onlyai"].Bool() = true;
+		boost::thread(&CServerHandler::debugStartTest, CSH, session["testsave"].String(), true);
+	}
+	else
+	{
+		mmenu = CMainMenu::create();
+		GH.curInt = mmenu.get();
+	}
+	
+	std::vector<std::string> names;
+	session["lobby"].Bool() = false;
+	if(vm.count("lobby"))
+	{
+		session["lobby"].Bool() = true;
+		session["host"].Bool() = false;
+		session["address"].String() = vm["lobby-address"].as<std::string>();
+		if(vm.count("lobby-username"))
+			session["username"].String() = vm["lobby-username"].as<std::string>();
+		else
+			session["username"].String() = settings["launcher"]["lobbyUsername"].String();
+		if(vm.count("lobby-gamemode"))
+			session["gamemode"].Integer() = vm["lobby-gamemode"].as<ui16>();
+		else
+			session["gamemode"].Integer() = 0;
+		CSH->uuid = vm["uuid"].as<std::string>();
+		session["port"].Integer() = vm["lobby-port"].as<ui16>();
+		logGlobal->info("Remote lobby mode at %s:%d, uuid is %s", session["address"].String(), session["port"].Integer(), CSH->uuid);
+		if(vm.count("lobby-host"))
+		{
+			session["host"].Bool() = true;
+			session["hostConnections"].String() = std::to_string(vm["lobby-connections"].as<ui16>());
+			session["hostUuid"].String() = vm["lobby-uuid"].as<std::string>();
+			logGlobal->info("This client will host session, server uuid is %s", session["hostUuid"].String());
+		}
+		
+		//we should not reconnect to previous game in online mode
+		Settings saveSession = settings.write["server"]["reconnect"];
+		saveSession->Bool() = false;
+		
+		//start lobby immediately
+		names.push_back(session["username"].String());
+		ESelectionScreen sscreen = session["gamemode"].Integer() == 0 ? ESelectionScreen::newGame : ESelectionScreen::loadGame;
+		mmenu->openLobby(sscreen, session["host"].Bool(), &names, ELoadMode::MULTI);
+	}
+	
+	// Restore remote session - start game immediately
+	if(settings["server"]["reconnect"].Bool())
+	{
+		CSH->restoreLastSession();
+	}
+
+	if(!settings["session"]["headless"].Bool())
+	{
+		mainLoop();
+	}
+	else
+	{
+		while(true)
+			boost::this_thread::sleep_for(boost::chrono::milliseconds(1000));
+	}
+
+	return 0;
+}
+
+//plays intro, ends when intro is over or button has been pressed (handles events)
+void playIntro()
+{
+	auto audioData = CCS->videoh->getAudio(VideoPath::builtin("3DOLOGO.SMK"));
+	int sound = CCS->soundh->playSound(audioData);
+	if(CCS->videoh->openAndPlayVideo(VideoPath::builtin("3DOLOGO.SMK"), 0, 1, true, true))
+	{
+		audioData = CCS->videoh->getAudio(VideoPath::builtin("NWCLOGO.SMK"));
+		sound = CCS->soundh->playSound(audioData);
+		if (CCS->videoh->openAndPlayVideo(VideoPath::builtin("NWCLOGO.SMK"), 0, 1, true, true))
+		{
+			audioData = CCS->videoh->getAudio(VideoPath::builtin("H3INTRO.SMK"));
+			sound = CCS->soundh->playSound(audioData);
+			CCS->videoh->openAndPlayVideo(VideoPath::builtin("H3INTRO.SMK"), 0, 1, true, true);
+		}
+	}
+	CCS->soundh->stopSound(sound);
+}
+
+static void mainLoop()
+{
+	setThreadName("MainGUI");
+
+	while(1) //main SDL events loop
+	{
+		GH.input().fetchEvents();
+		CSH->applyPacksOnLobbyScreen();
+		GH.renderFrame();
+	}
+}
+
+static void quitApplication()
+{
+	if(!settings["session"]["headless"].Bool())
+	{
+		if(CSH->client)
+			CSH->endGameplay();
+	}
+
+	GH.windows().clear();
+
+	CMM.reset();
+
+	if(!settings["session"]["headless"].Bool())
+	{
+		// cleanup, mostly to remove false leaks from analyzer
+		if(CCS)
+		{
+			CCS->musich->release();
+			CCS->soundh->release();
+
+			vstd::clear_pointer(CCS);
+		}
+		CMessage::dispose();
+
+		vstd::clear_pointer(graphics);
+	}
+
+	vstd::clear_pointer(VLC);
+
+	vstd::clear_pointer(console);// should be removed after everything else since used by logging
+
+	if(!settings["session"]["headless"].Bool())
+		GH.screenHandler().close();
+
+	if(logConfig != nullptr)
+	{
+		logConfig->deconfigure();
+		delete logConfig;
+		logConfig = nullptr;
+	}
+
+	std::cout << "Ending...\n";
+
+	// this method is always called from event/network threads, which keep interface mutex locked
+	// unlock it here to avoid assertion failure on GH destruction in exit()
+	GH.interfaceMutex.unlock();
+	exit(0);
+}
+
+void handleQuit(bool ask)
+{
+	if(CSH->client && LOCPLINT && ask)
+	{
+		CCS->curh->set(Cursor::Map::POINTER);
+		LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[69], quitApplication, nullptr);
+	}
+	else
+	{
+		quitApplication();
+	}
+}
+
+void handleFatalError(const std::string & message, bool terminate)
+{
+	logGlobal->error("FATAL ERROR ENCOUTERED, VCMI WILL NOW TERMINATE");
+	logGlobal->error("Reason: %s", message);
+
+	std::string messageToShow = "Fatal error! " + message;
+
+	SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Fatal error!", messageToShow.c_str(), nullptr);
+
+	if (terminate)
+		throw std::runtime_error(message);
+	else
+		exit(1);
+}

+ 718 - 718
client/CMusicHandler.cpp

@@ -1,718 +1,718 @@
-/*
- * CMusicHandler.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 <SDL_mixer.h>
-#include <SDL_timer.h>
-
-#include "CMusicHandler.h"
-#include "CGameInfo.h"
-#include "renderSDL/SDLRWwrapper.h"
-#include "eventsSDL/InputHandler.h"
-#include "gui/CGuiHandler.h"
-
-#include "../lib/JsonNode.h"
-#include "../lib/GameConstants.h"
-#include "../lib/filesystem/Filesystem.h"
-#include "../lib/constants/StringConstants.h"
-#include "../lib/CRandomGenerator.h"
-#include "../lib/VCMIDirs.h"
-#include "../lib/TerrainHandler.h"
-
-
-#define VCMI_SOUND_NAME(x)
-#define VCMI_SOUND_FILE(y) #y,
-
-// sounds mapped to soundBase enum
-static std::string sounds[] = {
-	"", // invalid
-	"", // todo
-	VCMI_SOUND_LIST
-};
-#undef VCMI_SOUND_NAME
-#undef VCMI_SOUND_FILE
-
-void CAudioBase::init()
-{
-	if (initialized)
-		return;
-
-	if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 1024)==-1)
-	{
-		logGlobal->error("Mix_OpenAudio error: %s", Mix_GetError());
-		return;
-	}
-
-	initialized = true;
-}
-
-void CAudioBase::release()
-{
-	if(!(CCS->soundh->initialized && CCS->musich->initialized))
-		Mix_CloseAudio();
-
-	initialized = false;
-}
-
-void CAudioBase::setVolume(ui32 percent)
-{
-	if (percent > 100)
-		percent = 100;
-
-	volume = percent;
-}
-
-void CSoundHandler::onVolumeChange(const JsonNode &volumeNode)
-{
-	setVolume((ui32)volumeNode.Float());
-}
-
-CSoundHandler::CSoundHandler():
-	listener(settings.listen["general"]["sound"]),
-	ambientConfig(JsonPath::builtin("config/ambientSounds.json"))
-{
-	listener(std::bind(&CSoundHandler::onVolumeChange, this, _1));
-
-	battleIntroSounds =
-	{
-		soundBase::battle00, soundBase::battle01,
-		soundBase::battle02, soundBase::battle03, soundBase::battle04,
-		soundBase::battle05, soundBase::battle06, soundBase::battle07
-	};
-}
-
-void CSoundHandler::init()
-{
-	CAudioBase::init();
-	if(ambientConfig["allocateChannels"].isNumber())
-		Mix_AllocateChannels((int)ambientConfig["allocateChannels"].Integer());
-
-	if (initialized)
-	{
-		Mix_ChannelFinished([](int channel)
-		{
-			CCS->soundh->soundFinishedCallback(channel);
-		});
-	}
-}
-
-void CSoundHandler::release()
-{
-	if (initialized)
-	{
-		Mix_HaltChannel(-1);
-
-		for (auto &chunk : soundChunks)
-		{
-			if (chunk.second.first)
-				Mix_FreeChunk(chunk.second.first);
-		}
-	}
-
-	CAudioBase::release();
-}
-
-// Allocate an SDL chunk and cache it.
-Mix_Chunk *CSoundHandler::GetSoundChunk(const AudioPath & sound, bool cache)
-{
-	try
-	{
-		if (cache && soundChunks.find(sound) != soundChunks.end())
-			return soundChunks[sound].first;
-
-		auto data = CResourceHandler::get()->load(sound.addPrefix("SOUNDS/"))->readAll();
-		SDL_RWops *ops = SDL_RWFromMem(data.first.get(), (int)data.second);
-		Mix_Chunk *chunk = Mix_LoadWAV_RW(ops, 1);	// will free ops
-
-		if (cache)
-			soundChunks.insert({sound, std::make_pair (chunk, std::move (data.first))});
-
-		return chunk;
-	}
-	catch(std::exception &e)
-	{
-		logGlobal->warn("Cannot get sound %s chunk: %s", sound.getOriginalName(), e.what());
-		return nullptr;
-	}
-}
-
-Mix_Chunk *CSoundHandler::GetSoundChunk(std::pair<std::unique_ptr<ui8 []>, si64> & data, bool cache)
-{
-	try
-	{
-		std::vector<ui8> startBytes = std::vector<ui8>(data.first.get(), data.first.get() + std::min((si64)100, data.second));
-
-		if (cache && soundChunksRaw.find(startBytes) != soundChunksRaw.end())
-			return soundChunksRaw[startBytes].first;
-
-		SDL_RWops *ops = SDL_RWFromMem(data.first.get(), (int)data.second);
-		Mix_Chunk *chunk = Mix_LoadWAV_RW(ops, 1);	// will free ops
-
-		if (cache)
-			soundChunksRaw.insert({startBytes, std::make_pair (chunk, std::move (data.first))});
-
-		return chunk;
-	}
-	catch(std::exception &e)
-	{
-		logGlobal->warn("Cannot get sound chunk: %s", e.what());
-		return nullptr;
-	}
-}
-
-int CSoundHandler::ambientDistToVolume(int distance) const
-{
-	const auto & distancesVector = ambientConfig["distances"].Vector();
-
-	if(distance >= distancesVector.size())
-		return 0;
-
-	int volume = static_cast<int>(distancesVector[distance].Integer());
-	return volume * (int)ambientConfig["volume"].Integer() / 100;
-}
-
-void CSoundHandler::ambientStopSound(const AudioPath & soundId)
-{
-	stopSound(ambientChannels[soundId]);
-	setChannelVolume(ambientChannels[soundId], volume);
-}
-
-// Plays a sound, and return its channel so we can fade it out later
-int CSoundHandler::playSound(soundBase::soundID soundID, int repeats)
-{
-	assert(soundID < soundBase::sound_after_last);
-	auto sound = AudioPath::builtin(sounds[soundID]);
-	logGlobal->trace("Attempt to play sound %d with file name %s with cache", soundID, sound.getOriginalName());
-
-	return playSound(sound, repeats, true);
-}
-
-int CSoundHandler::playSound(const AudioPath & sound, int repeats, bool cache)
-{
-	if (!initialized || sound.empty())
-		return -1;
-
-	int channel;
-	Mix_Chunk *chunk = GetSoundChunk(sound, cache);
-
-	if (chunk)
-	{
-		channel = Mix_PlayChannel(-1, chunk, repeats);
-		if (channel == -1)
-		{
-			logGlobal->error("Unable to play sound file %s , error %s", sound.getOriginalName(), Mix_GetError());
-			if (!cache)
-				Mix_FreeChunk(chunk);
-		}
-		else if (cache)
-			initCallback(channel);
-		else
-			initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);});
-	}
-	else
-		channel = -1;
-
-	return channel;
-}
-
-int CSoundHandler::playSound(std::pair<std::unique_ptr<ui8 []>, si64> & data, int repeats, bool cache)
-{
-	int channel = -1;
-	if (Mix_Chunk *chunk = GetSoundChunk(data, cache))
-	{
-		channel = Mix_PlayChannel(-1, chunk, repeats);
-		if (channel == -1)
-		{
-			logGlobal->error("Unable to play sound, error %s", Mix_GetError());
-			if (!cache)
-				Mix_FreeChunk(chunk);
-		}
-		else if (cache)
-			initCallback(channel);
-		else
-			initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);});
-	}
-	return channel;
-}
-
-// Helper. Randomly select a sound from an array and play it
-int CSoundHandler::playSoundFromSet(std::vector<soundBase::soundID> &sound_vec)
-{
-	return playSound(*RandomGeneratorUtil::nextItem(sound_vec, CRandomGenerator::getDefault()));
-}
-
-void CSoundHandler::stopSound(int handler)
-{
-	if (initialized && handler != -1)
-		Mix_HaltChannel(handler);
-}
-
-// Sets the sound volume, from 0 (mute) to 100
-void CSoundHandler::setVolume(ui32 percent)
-{
-	CAudioBase::setVolume(percent);
-
-	if (initialized)
-	{
-		setChannelVolume(-1, volume);
-
-		for (auto const & channel : channelVolumes)
-			updateChannelVolume(channel.first);
-	}
-}
-
-void CSoundHandler::updateChannelVolume(int channel)
-{
-	if (channelVolumes.count(channel))
-		setChannelVolume(channel, getVolume() * channelVolumes[channel] / 100);
-	else
-		setChannelVolume(channel, getVolume());
-}
-
-// Sets the sound volume, from 0 (mute) to 100
-void CSoundHandler::setChannelVolume(int channel, ui32 percent)
-{
-	Mix_Volume(channel, (MIX_MAX_VOLUME * percent)/100);
-}
-
-void CSoundHandler::setCallback(int channel, std::function<void()> function)
-{
-	boost::mutex::scoped_lock lockGuard(mutexCallbacks);
-
-	auto iter = callbacks.find(channel);
-
-	//channel not found. It may have finished so fire callback now
-	if(iter == callbacks.end())
-		function();
-	else
-		iter->second.push_back(function);
-}
-
-void CSoundHandler::soundFinishedCallback(int channel)
-{
-	boost::mutex::scoped_lock lockGuard(mutexCallbacks);
-
-	if (callbacks.count(channel) == 0)
-		return;
-
-	// store callbacks from container locally - SDL might reuse this channel for another sound
-	// but do actualy execution in separate thread, to avoid potential deadlocks in case if callback requires locks of its own
-	auto callback = callbacks.at(channel);
-	callbacks.erase(channel);
-
-	if (!callback.empty())
-	{
-		GH.dispatchMainThread([callback](){
-			for (auto entry : callback)
-				entry();
-		});
-	}
-}
-
-void CSoundHandler::initCallback(int channel)
-{
-	boost::mutex::scoped_lock lockGuard(mutexCallbacks);
-	assert(callbacks.count(channel) == 0);
-	callbacks[channel] = {};
-}
-
-void CSoundHandler::initCallback(int channel, const std::function<void()> & function)
-{
-	boost::mutex::scoped_lock lockGuard(mutexCallbacks);
-	assert(callbacks.count(channel) == 0);
-	callbacks[channel].push_back(function);
-}
-
-int CSoundHandler::ambientGetRange() const
-{
-	return static_cast<int>(ambientConfig["range"].Integer());
-}
-
-void CSoundHandler::ambientUpdateChannels(std::map<AudioPath, int> soundsArg)
-{
-	boost::mutex::scoped_lock guard(mutex);
-
-	std::vector<AudioPath> stoppedSounds;
-	for(auto & pair : ambientChannels)
-	{
-		const auto & soundId = pair.first;
-		const int channel = pair.second;
-
-		if(!vstd::contains(soundsArg, soundId))
-		{
-			ambientStopSound(soundId);
-			stoppedSounds.push_back(soundId);
-		}
-		else
-		{
-			int volume = ambientDistToVolume(soundsArg[soundId]);
-			channelVolumes[channel] = volume;
-			updateChannelVolume(channel);
-		}
-	}
-	for(auto soundId : stoppedSounds)
-	{
-		channelVolumes.erase(ambientChannels[soundId]);
-		ambientChannels.erase(soundId);
-	}
-
-	for(auto & pair : soundsArg)
-	{
-		const auto & soundId = pair.first;
-		const int distance = pair.second;
-
-		if(!vstd::contains(ambientChannels, soundId))
-		{
-			int channel = playSound(soundId, -1);
-			int volume = ambientDistToVolume(distance);
-			channelVolumes[channel] = volume;
-
-			updateChannelVolume(channel);
-			ambientChannels[soundId] = channel;
-		}
-	}
-}
-
-void CSoundHandler::ambientStopAllChannels()
-{
-	boost::mutex::scoped_lock guard(mutex);
-
-	for(auto ch : ambientChannels)
-	{
-		ambientStopSound(ch.first);
-	}
-	channelVolumes.clear();
-	ambientChannels.clear();
-}
-
-void CMusicHandler::onVolumeChange(const JsonNode &volumeNode)
-{
-	setVolume((ui32)volumeNode.Float());
-}
-
-CMusicHandler::CMusicHandler():
-	listener(settings.listen["general"]["music"])
-{
-	listener(std::bind(&CMusicHandler::onVolumeChange, this, _1));
-
-	auto mp3files = CResourceHandler::get()->getFilteredFiles([](const ResourcePath & id) ->  bool
-	{
-		if(id.getType() != EResType::SOUND)
-			return false;
-
-		if(!boost::algorithm::istarts_with(id.getName(), "MUSIC/"))
-			return false;
-
-		logGlobal->trace("Found music file %s", id.getName());
-		return true;
-	});
-
-	for(const ResourcePath & file : mp3files)
-	{
-		if(boost::algorithm::istarts_with(file.getName(), "MUSIC/Combat"))
-			addEntryToSet("battle", AudioPath::fromResource(file));
-		else if(boost::algorithm::istarts_with(file.getName(), "MUSIC/AITheme"))
-			addEntryToSet("enemy-turn", AudioPath::fromResource(file));
-	}
-
-}
-
-void CMusicHandler::loadTerrainMusicThemes()
-{
-	for (const auto & terrain : CGI->terrainTypeHandler->objects)
-	{
-		addEntryToSet("terrain_" + terrain->getJsonKey(), terrain->musicFilename);
-	}
-}
-
-void CMusicHandler::addEntryToSet(const std::string & set, const AudioPath & musicURI)
-{
-	musicsSet[set].push_back(musicURI);
-}
-
-void CMusicHandler::init()
-{
-	CAudioBase::init();
-
-	if (initialized)
-	{
-		Mix_HookMusicFinished([]()
-		{
-			CCS->musich->musicFinishedCallback();
-		});
-	}
-}
-
-void CMusicHandler::release()
-{
-	if (initialized)
-	{
-		boost::mutex::scoped_lock guard(mutex);
-
-		Mix_HookMusicFinished(nullptr);
-		current->stop();
-
-		current.reset();
-		next.reset();
-	}
-
-	CAudioBase::release();
-}
-
-void CMusicHandler::playMusic(const AudioPath & musicURI, bool loop, bool fromStart)
-{
-	boost::mutex::scoped_lock guard(mutex);
-
-	if (current && current->isPlaying() && current->isTrack(musicURI))
-		return;
-
-	queueNext(this, "", musicURI, loop, fromStart);
-}
-
-void CMusicHandler::playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart)
-{
-	playMusicFromSet(musicSet + "_" + entryID, loop, fromStart);
-}
-
-void CMusicHandler::playMusicFromSet(const std::string & whichSet, bool loop, bool fromStart)
-{
-	boost::mutex::scoped_lock guard(mutex);
-
-	auto selectedSet = musicsSet.find(whichSet);
-	if (selectedSet == musicsSet.end())
-	{
-		logGlobal->error("Error: playing music from non-existing set: %s", whichSet);
-		return;
-	}
-
-	if (current && current->isPlaying() && current->isSet(whichSet))
-		return;
-
-	// in this mode - play random track from set
-	queueNext(this, whichSet, AudioPath(), loop, fromStart);
-}
-
-void CMusicHandler::queueNext(std::unique_ptr<MusicEntry> queued)
-{
-	if (!initialized)
-		return;
-
-	next = std::move(queued);
-
-	if (current.get() == nullptr || !current->stop(1000))
-	{
-		current.reset(next.release());
-		current->play();
-	}
-}
-
-void CMusicHandler::queueNext(CMusicHandler *owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart)
-{
-	queueNext(std::make_unique<MusicEntry>(owner, setName, musicURI, looped, fromStart));
-}
-
-void CMusicHandler::stopMusic(int fade_ms)
-{
-	if (!initialized)
-		return;
-
-	boost::mutex::scoped_lock guard(mutex);
-
-	if (current.get() != nullptr)
-		current->stop(fade_ms);
-	next.reset();
-}
-
-void CMusicHandler::setVolume(ui32 percent)
-{
-	CAudioBase::setVolume(percent);
-
-	if (initialized)
-		Mix_VolumeMusic((MIX_MAX_VOLUME * volume)/100);
-}
-
-void CMusicHandler::musicFinishedCallback()
-{
-	// call music restart in separate thread to avoid deadlock in some cases
-	// It is possible for:
-	// 1) SDL thread to call this method on end of playback
-	// 2) VCMI code to call queueNext() method to queue new file
-	// this leads to:
-	// 1) SDL thread waiting to acquire music lock in this method (while keeping internal SDL mutex locked)
-	// 2) VCMI thread waiting to acquire internal SDL mutex (while keeping music mutex locked)
-
-	GH.dispatchMainThread([this]()
-	{
-		boost::unique_lock lockGuard(mutex);
-		if (current.get() != nullptr)
-		{
-			// if music is looped, play it again
-			if (current->play())
-				return;
-			else
-				current.reset();
-		}
-
-		if (current.get() == nullptr && next.get() != nullptr)
-		{
-			current.reset(next.release());
-			current->play();
-		}
-	});
-}
-
-MusicEntry::MusicEntry(CMusicHandler *owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart):
-	owner(owner),
-	music(nullptr),
-	playing(false),
-	startTime(uint32_t(-1)),
-	startPosition(0),
-	loop(looped ? -1 : 1),
-	fromStart(fromStart),
-	setName(std::move(setName))
-{
-	if (!musicURI.empty())
-		load(std::move(musicURI));
-}
-MusicEntry::~MusicEntry()
-{
-	if (playing && loop > 0)
-	{
-		assert(0);
-		logGlobal->error("Attempt to delete music while playing!");
-		Mix_HaltMusic();
-	}
-
-	if (loop == 0 && Mix_FadingMusic() != MIX_NO_FADING)
-	{
-		assert(0);
-		logGlobal->error("Attempt to delete music while fading out!");
-		Mix_HaltMusic();
-	}
-
-	logGlobal->trace("Del-ing music file %s", currentName.getOriginalName());
-	if (music)
-		Mix_FreeMusic(music);
-}
-
-void MusicEntry::load(const AudioPath & musicURI)
-{
-	if (music)
-	{
-		logGlobal->trace("Del-ing music file %s", currentName.getOriginalName());
-		Mix_FreeMusic(music);
-		music = nullptr;
-	}
-
-	if (CResourceHandler::get()->existsResource(musicURI))
-		currentName = musicURI;
-	else
-		currentName = musicURI.addPrefix("MUSIC/");
-
-	music = nullptr;
-
-	logGlobal->trace("Loading music file %s", currentName.getOriginalName());
-
-	try
-	{
-		auto musicFile = MakeSDLRWops(CResourceHandler::get()->load(currentName));
-		music = Mix_LoadMUS_RW(musicFile, SDL_TRUE);
-	}
-	catch(std::exception &e)
-	{
-		logGlobal->error("Failed to load music. setName=%s\tmusicURI=%s", setName, currentName.getOriginalName());
-		logGlobal->error("Exception: %s", e.what());
-	}
-
-	if(!music)
-	{
-		logGlobal->warn("Warning: Cannot open %s: %s", currentName.getOriginalName(), Mix_GetError());
-		return;
-	}
-}
-
-bool MusicEntry::play()
-{
-	if (!(loop--) && music) //already played once - return
-		return false;
-
-	if (!setName.empty())
-	{
-		const auto & set = owner->musicsSet[setName];
-		const auto & iter = RandomGeneratorUtil::nextItem(set, CRandomGenerator::getDefault());
-		load(*iter);
-	}
-
-	logGlobal->trace("Playing music file %s", currentName.getOriginalName());
-
-	if (!fromStart && owner->trackPositions.count(currentName) > 0 && owner->trackPositions[currentName] > 0)
-	{
-		float timeToStart = owner->trackPositions[currentName];
-		startPosition = std::round(timeToStart * 1000);
-
-		// erase stored position:
-		// if music track will be interrupted again - new position will be written in stop() method
-		// if music track is not interrupted and will finish by timeout/end of file - it will restart from begginning as it should
-		owner->trackPositions.erase(owner->trackPositions.find(currentName));
-
-		if (Mix_FadeInMusicPos(music, 1, 1000, timeToStart) == -1)
-		{
-			logGlobal->error("Unable to play music (%s)", Mix_GetError());
-			return false;
-		}
-	}
-	else
-	{
-		startPosition = 0;
-
-		if(Mix_PlayMusic(music, 1) == -1)
-		{
-			logGlobal->error("Unable to play music (%s)", Mix_GetError());
-			return false;
-		}
-	}
-
-	startTime = GH.input().getTicks();
-	
-	playing = true;
-	return true;
-}
-
-bool MusicEntry::stop(int fade_ms)
-{
-	if (Mix_PlayingMusic())
-	{
-		playing = false;
-		loop = 0;
-		uint32_t endTime = GH.input().getTicks();
-		assert(startTime != uint32_t(-1));
-		float playDuration = (endTime - startTime + startPosition) / 1000.f;
-		owner->trackPositions[currentName] = playDuration;
-		logGlobal->trace("Stopping music file %s at %f", currentName.getOriginalName(), playDuration);
-
-		Mix_FadeOutMusic(fade_ms);
-		return true;
-	}
-	return false;
-}
-
-bool MusicEntry::isPlaying()
-{
-	return playing;
-}
-
-bool MusicEntry::isSet(std::string set)
-{
-	return !setName.empty() && set == setName;
-}
-
-bool MusicEntry::isTrack(const AudioPath & track)
-{
-	return setName.empty() && track == currentName;
-}
+/*
+ * CMusicHandler.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 <SDL_mixer.h>
+#include <SDL_timer.h>
+
+#include "CMusicHandler.h"
+#include "CGameInfo.h"
+#include "renderSDL/SDLRWwrapper.h"
+#include "eventsSDL/InputHandler.h"
+#include "gui/CGuiHandler.h"
+
+#include "../lib/JsonNode.h"
+#include "../lib/GameConstants.h"
+#include "../lib/filesystem/Filesystem.h"
+#include "../lib/constants/StringConstants.h"
+#include "../lib/CRandomGenerator.h"
+#include "../lib/VCMIDirs.h"
+#include "../lib/TerrainHandler.h"
+
+
+#define VCMI_SOUND_NAME(x)
+#define VCMI_SOUND_FILE(y) #y,
+
+// sounds mapped to soundBase enum
+static std::string sounds[] = {
+	"", // invalid
+	"", // todo
+	VCMI_SOUND_LIST
+};
+#undef VCMI_SOUND_NAME
+#undef VCMI_SOUND_FILE
+
+void CAudioBase::init()
+{
+	if (initialized)
+		return;
+
+	if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 1024)==-1)
+	{
+		logGlobal->error("Mix_OpenAudio error: %s", Mix_GetError());
+		return;
+	}
+
+	initialized = true;
+}
+
+void CAudioBase::release()
+{
+	if(!(CCS->soundh->initialized && CCS->musich->initialized))
+		Mix_CloseAudio();
+
+	initialized = false;
+}
+
+void CAudioBase::setVolume(ui32 percent)
+{
+	if (percent > 100)
+		percent = 100;
+
+	volume = percent;
+}
+
+void CSoundHandler::onVolumeChange(const JsonNode &volumeNode)
+{
+	setVolume((ui32)volumeNode.Float());
+}
+
+CSoundHandler::CSoundHandler():
+	listener(settings.listen["general"]["sound"]),
+	ambientConfig(JsonPath::builtin("config/ambientSounds.json"))
+{
+	listener(std::bind(&CSoundHandler::onVolumeChange, this, _1));
+
+	battleIntroSounds =
+	{
+		soundBase::battle00, soundBase::battle01,
+		soundBase::battle02, soundBase::battle03, soundBase::battle04,
+		soundBase::battle05, soundBase::battle06, soundBase::battle07
+	};
+}
+
+void CSoundHandler::init()
+{
+	CAudioBase::init();
+	if(ambientConfig["allocateChannels"].isNumber())
+		Mix_AllocateChannels((int)ambientConfig["allocateChannels"].Integer());
+
+	if (initialized)
+	{
+		Mix_ChannelFinished([](int channel)
+		{
+			CCS->soundh->soundFinishedCallback(channel);
+		});
+	}
+}
+
+void CSoundHandler::release()
+{
+	if (initialized)
+	{
+		Mix_HaltChannel(-1);
+
+		for (auto &chunk : soundChunks)
+		{
+			if (chunk.second.first)
+				Mix_FreeChunk(chunk.second.first);
+		}
+	}
+
+	CAudioBase::release();
+}
+
+// Allocate an SDL chunk and cache it.
+Mix_Chunk *CSoundHandler::GetSoundChunk(const AudioPath & sound, bool cache)
+{
+	try
+	{
+		if (cache && soundChunks.find(sound) != soundChunks.end())
+			return soundChunks[sound].first;
+
+		auto data = CResourceHandler::get()->load(sound.addPrefix("SOUNDS/"))->readAll();
+		SDL_RWops *ops = SDL_RWFromMem(data.first.get(), (int)data.second);
+		Mix_Chunk *chunk = Mix_LoadWAV_RW(ops, 1);	// will free ops
+
+		if (cache)
+			soundChunks.insert({sound, std::make_pair (chunk, std::move (data.first))});
+
+		return chunk;
+	}
+	catch(std::exception &e)
+	{
+		logGlobal->warn("Cannot get sound %s chunk: %s", sound.getOriginalName(), e.what());
+		return nullptr;
+	}
+}
+
+Mix_Chunk *CSoundHandler::GetSoundChunk(std::pair<std::unique_ptr<ui8 []>, si64> & data, bool cache)
+{
+	try
+	{
+		std::vector<ui8> startBytes = std::vector<ui8>(data.first.get(), data.first.get() + std::min((si64)100, data.second));
+
+		if (cache && soundChunksRaw.find(startBytes) != soundChunksRaw.end())
+			return soundChunksRaw[startBytes].first;
+
+		SDL_RWops *ops = SDL_RWFromMem(data.first.get(), (int)data.second);
+		Mix_Chunk *chunk = Mix_LoadWAV_RW(ops, 1);	// will free ops
+
+		if (cache)
+			soundChunksRaw.insert({startBytes, std::make_pair (chunk, std::move (data.first))});
+
+		return chunk;
+	}
+	catch(std::exception &e)
+	{
+		logGlobal->warn("Cannot get sound chunk: %s", e.what());
+		return nullptr;
+	}
+}
+
+int CSoundHandler::ambientDistToVolume(int distance) const
+{
+	const auto & distancesVector = ambientConfig["distances"].Vector();
+
+	if(distance >= distancesVector.size())
+		return 0;
+
+	int volume = static_cast<int>(distancesVector[distance].Integer());
+	return volume * (int)ambientConfig["volume"].Integer() / 100;
+}
+
+void CSoundHandler::ambientStopSound(const AudioPath & soundId)
+{
+	stopSound(ambientChannels[soundId]);
+	setChannelVolume(ambientChannels[soundId], volume);
+}
+
+// Plays a sound, and return its channel so we can fade it out later
+int CSoundHandler::playSound(soundBase::soundID soundID, int repeats)
+{
+	assert(soundID < soundBase::sound_after_last);
+	auto sound = AudioPath::builtin(sounds[soundID]);
+	logGlobal->trace("Attempt to play sound %d with file name %s with cache", soundID, sound.getOriginalName());
+
+	return playSound(sound, repeats, true);
+}
+
+int CSoundHandler::playSound(const AudioPath & sound, int repeats, bool cache)
+{
+	if (!initialized || sound.empty())
+		return -1;
+
+	int channel;
+	Mix_Chunk *chunk = GetSoundChunk(sound, cache);
+
+	if (chunk)
+	{
+		channel = Mix_PlayChannel(-1, chunk, repeats);
+		if (channel == -1)
+		{
+			logGlobal->error("Unable to play sound file %s , error %s", sound.getOriginalName(), Mix_GetError());
+			if (!cache)
+				Mix_FreeChunk(chunk);
+		}
+		else if (cache)
+			initCallback(channel);
+		else
+			initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);});
+	}
+	else
+		channel = -1;
+
+	return channel;
+}
+
+int CSoundHandler::playSound(std::pair<std::unique_ptr<ui8 []>, si64> & data, int repeats, bool cache)
+{
+	int channel = -1;
+	if (Mix_Chunk *chunk = GetSoundChunk(data, cache))
+	{
+		channel = Mix_PlayChannel(-1, chunk, repeats);
+		if (channel == -1)
+		{
+			logGlobal->error("Unable to play sound, error %s", Mix_GetError());
+			if (!cache)
+				Mix_FreeChunk(chunk);
+		}
+		else if (cache)
+			initCallback(channel);
+		else
+			initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);});
+	}
+	return channel;
+}
+
+// Helper. Randomly select a sound from an array and play it
+int CSoundHandler::playSoundFromSet(std::vector<soundBase::soundID> &sound_vec)
+{
+	return playSound(*RandomGeneratorUtil::nextItem(sound_vec, CRandomGenerator::getDefault()));
+}
+
+void CSoundHandler::stopSound(int handler)
+{
+	if (initialized && handler != -1)
+		Mix_HaltChannel(handler);
+}
+
+// Sets the sound volume, from 0 (mute) to 100
+void CSoundHandler::setVolume(ui32 percent)
+{
+	CAudioBase::setVolume(percent);
+
+	if (initialized)
+	{
+		setChannelVolume(-1, volume);
+
+		for (auto const & channel : channelVolumes)
+			updateChannelVolume(channel.first);
+	}
+}
+
+void CSoundHandler::updateChannelVolume(int channel)
+{
+	if (channelVolumes.count(channel))
+		setChannelVolume(channel, getVolume() * channelVolumes[channel] / 100);
+	else
+		setChannelVolume(channel, getVolume());
+}
+
+// Sets the sound volume, from 0 (mute) to 100
+void CSoundHandler::setChannelVolume(int channel, ui32 percent)
+{
+	Mix_Volume(channel, (MIX_MAX_VOLUME * percent)/100);
+}
+
+void CSoundHandler::setCallback(int channel, std::function<void()> function)
+{
+	boost::mutex::scoped_lock lockGuard(mutexCallbacks);
+
+	auto iter = callbacks.find(channel);
+
+	//channel not found. It may have finished so fire callback now
+	if(iter == callbacks.end())
+		function();
+	else
+		iter->second.push_back(function);
+}
+
+void CSoundHandler::soundFinishedCallback(int channel)
+{
+	boost::mutex::scoped_lock lockGuard(mutexCallbacks);
+
+	if (callbacks.count(channel) == 0)
+		return;
+
+	// store callbacks from container locally - SDL might reuse this channel for another sound
+	// but do actualy execution in separate thread, to avoid potential deadlocks in case if callback requires locks of its own
+	auto callback = callbacks.at(channel);
+	callbacks.erase(channel);
+
+	if (!callback.empty())
+	{
+		GH.dispatchMainThread([callback](){
+			for (auto entry : callback)
+				entry();
+		});
+	}
+}
+
+void CSoundHandler::initCallback(int channel)
+{
+	boost::mutex::scoped_lock lockGuard(mutexCallbacks);
+	assert(callbacks.count(channel) == 0);
+	callbacks[channel] = {};
+}
+
+void CSoundHandler::initCallback(int channel, const std::function<void()> & function)
+{
+	boost::mutex::scoped_lock lockGuard(mutexCallbacks);
+	assert(callbacks.count(channel) == 0);
+	callbacks[channel].push_back(function);
+}
+
+int CSoundHandler::ambientGetRange() const
+{
+	return static_cast<int>(ambientConfig["range"].Integer());
+}
+
+void CSoundHandler::ambientUpdateChannels(std::map<AudioPath, int> soundsArg)
+{
+	boost::mutex::scoped_lock guard(mutex);
+
+	std::vector<AudioPath> stoppedSounds;
+	for(auto & pair : ambientChannels)
+	{
+		const auto & soundId = pair.first;
+		const int channel = pair.second;
+
+		if(!vstd::contains(soundsArg, soundId))
+		{
+			ambientStopSound(soundId);
+			stoppedSounds.push_back(soundId);
+		}
+		else
+		{
+			int volume = ambientDistToVolume(soundsArg[soundId]);
+			channelVolumes[channel] = volume;
+			updateChannelVolume(channel);
+		}
+	}
+	for(auto soundId : stoppedSounds)
+	{
+		channelVolumes.erase(ambientChannels[soundId]);
+		ambientChannels.erase(soundId);
+	}
+
+	for(auto & pair : soundsArg)
+	{
+		const auto & soundId = pair.first;
+		const int distance = pair.second;
+
+		if(!vstd::contains(ambientChannels, soundId))
+		{
+			int channel = playSound(soundId, -1);
+			int volume = ambientDistToVolume(distance);
+			channelVolumes[channel] = volume;
+
+			updateChannelVolume(channel);
+			ambientChannels[soundId] = channel;
+		}
+	}
+}
+
+void CSoundHandler::ambientStopAllChannels()
+{
+	boost::mutex::scoped_lock guard(mutex);
+
+	for(auto ch : ambientChannels)
+	{
+		ambientStopSound(ch.first);
+	}
+	channelVolumes.clear();
+	ambientChannels.clear();
+}
+
+void CMusicHandler::onVolumeChange(const JsonNode &volumeNode)
+{
+	setVolume((ui32)volumeNode.Float());
+}
+
+CMusicHandler::CMusicHandler():
+	listener(settings.listen["general"]["music"])
+{
+	listener(std::bind(&CMusicHandler::onVolumeChange, this, _1));
+
+	auto mp3files = CResourceHandler::get()->getFilteredFiles([](const ResourcePath & id) ->  bool
+	{
+		if(id.getType() != EResType::SOUND)
+			return false;
+
+		if(!boost::algorithm::istarts_with(id.getName(), "MUSIC/"))
+			return false;
+
+		logGlobal->trace("Found music file %s", id.getName());
+		return true;
+	});
+
+	for(const ResourcePath & file : mp3files)
+	{
+		if(boost::algorithm::istarts_with(file.getName(), "MUSIC/Combat"))
+			addEntryToSet("battle", AudioPath::fromResource(file));
+		else if(boost::algorithm::istarts_with(file.getName(), "MUSIC/AITheme"))
+			addEntryToSet("enemy-turn", AudioPath::fromResource(file));
+	}
+
+}
+
+void CMusicHandler::loadTerrainMusicThemes()
+{
+	for (const auto & terrain : CGI->terrainTypeHandler->objects)
+	{
+		addEntryToSet("terrain_" + terrain->getJsonKey(), terrain->musicFilename);
+	}
+}
+
+void CMusicHandler::addEntryToSet(const std::string & set, const AudioPath & musicURI)
+{
+	musicsSet[set].push_back(musicURI);
+}
+
+void CMusicHandler::init()
+{
+	CAudioBase::init();
+
+	if (initialized)
+	{
+		Mix_HookMusicFinished([]()
+		{
+			CCS->musich->musicFinishedCallback();
+		});
+	}
+}
+
+void CMusicHandler::release()
+{
+	if (initialized)
+	{
+		boost::mutex::scoped_lock guard(mutex);
+
+		Mix_HookMusicFinished(nullptr);
+		current->stop();
+
+		current.reset();
+		next.reset();
+	}
+
+	CAudioBase::release();
+}
+
+void CMusicHandler::playMusic(const AudioPath & musicURI, bool loop, bool fromStart)
+{
+	boost::mutex::scoped_lock guard(mutex);
+
+	if (current && current->isPlaying() && current->isTrack(musicURI))
+		return;
+
+	queueNext(this, "", musicURI, loop, fromStart);
+}
+
+void CMusicHandler::playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart)
+{
+	playMusicFromSet(musicSet + "_" + entryID, loop, fromStart);
+}
+
+void CMusicHandler::playMusicFromSet(const std::string & whichSet, bool loop, bool fromStart)
+{
+	boost::mutex::scoped_lock guard(mutex);
+
+	auto selectedSet = musicsSet.find(whichSet);
+	if (selectedSet == musicsSet.end())
+	{
+		logGlobal->error("Error: playing music from non-existing set: %s", whichSet);
+		return;
+	}
+
+	if (current && current->isPlaying() && current->isSet(whichSet))
+		return;
+
+	// in this mode - play random track from set
+	queueNext(this, whichSet, AudioPath(), loop, fromStart);
+}
+
+void CMusicHandler::queueNext(std::unique_ptr<MusicEntry> queued)
+{
+	if (!initialized)
+		return;
+
+	next = std::move(queued);
+
+	if (current.get() == nullptr || !current->stop(1000))
+	{
+		current.reset(next.release());
+		current->play();
+	}
+}
+
+void CMusicHandler::queueNext(CMusicHandler *owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart)
+{
+	queueNext(std::make_unique<MusicEntry>(owner, setName, musicURI, looped, fromStart));
+}
+
+void CMusicHandler::stopMusic(int fade_ms)
+{
+	if (!initialized)
+		return;
+
+	boost::mutex::scoped_lock guard(mutex);
+
+	if (current.get() != nullptr)
+		current->stop(fade_ms);
+	next.reset();
+}
+
+void CMusicHandler::setVolume(ui32 percent)
+{
+	CAudioBase::setVolume(percent);
+
+	if (initialized)
+		Mix_VolumeMusic((MIX_MAX_VOLUME * volume)/100);
+}
+
+void CMusicHandler::musicFinishedCallback()
+{
+	// call music restart in separate thread to avoid deadlock in some cases
+	// It is possible for:
+	// 1) SDL thread to call this method on end of playback
+	// 2) VCMI code to call queueNext() method to queue new file
+	// this leads to:
+	// 1) SDL thread waiting to acquire music lock in this method (while keeping internal SDL mutex locked)
+	// 2) VCMI thread waiting to acquire internal SDL mutex (while keeping music mutex locked)
+
+	GH.dispatchMainThread([this]()
+	{
+		boost::unique_lock lockGuard(mutex);
+		if (current.get() != nullptr)
+		{
+			// if music is looped, play it again
+			if (current->play())
+				return;
+			else
+				current.reset();
+		}
+
+		if (current.get() == nullptr && next.get() != nullptr)
+		{
+			current.reset(next.release());
+			current->play();
+		}
+	});
+}
+
+MusicEntry::MusicEntry(CMusicHandler *owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart):
+	owner(owner),
+	music(nullptr),
+	playing(false),
+	startTime(uint32_t(-1)),
+	startPosition(0),
+	loop(looped ? -1 : 1),
+	fromStart(fromStart),
+	setName(std::move(setName))
+{
+	if (!musicURI.empty())
+		load(std::move(musicURI));
+}
+MusicEntry::~MusicEntry()
+{
+	if (playing && loop > 0)
+	{
+		assert(0);
+		logGlobal->error("Attempt to delete music while playing!");
+		Mix_HaltMusic();
+	}
+
+	if (loop == 0 && Mix_FadingMusic() != MIX_NO_FADING)
+	{
+		assert(0);
+		logGlobal->error("Attempt to delete music while fading out!");
+		Mix_HaltMusic();
+	}
+
+	logGlobal->trace("Del-ing music file %s", currentName.getOriginalName());
+	if (music)
+		Mix_FreeMusic(music);
+}
+
+void MusicEntry::load(const AudioPath & musicURI)
+{
+	if (music)
+	{
+		logGlobal->trace("Del-ing music file %s", currentName.getOriginalName());
+		Mix_FreeMusic(music);
+		music = nullptr;
+	}
+
+	if (CResourceHandler::get()->existsResource(musicURI))
+		currentName = musicURI;
+	else
+		currentName = musicURI.addPrefix("MUSIC/");
+
+	music = nullptr;
+
+	logGlobal->trace("Loading music file %s", currentName.getOriginalName());
+
+	try
+	{
+		auto musicFile = MakeSDLRWops(CResourceHandler::get()->load(currentName));
+		music = Mix_LoadMUS_RW(musicFile, SDL_TRUE);
+	}
+	catch(std::exception &e)
+	{
+		logGlobal->error("Failed to load music. setName=%s\tmusicURI=%s", setName, currentName.getOriginalName());
+		logGlobal->error("Exception: %s", e.what());
+	}
+
+	if(!music)
+	{
+		logGlobal->warn("Warning: Cannot open %s: %s", currentName.getOriginalName(), Mix_GetError());
+		return;
+	}
+}
+
+bool MusicEntry::play()
+{
+	if (!(loop--) && music) //already played once - return
+		return false;
+
+	if (!setName.empty())
+	{
+		const auto & set = owner->musicsSet[setName];
+		const auto & iter = RandomGeneratorUtil::nextItem(set, CRandomGenerator::getDefault());
+		load(*iter);
+	}
+
+	logGlobal->trace("Playing music file %s", currentName.getOriginalName());
+
+	if (!fromStart && owner->trackPositions.count(currentName) > 0 && owner->trackPositions[currentName] > 0)
+	{
+		float timeToStart = owner->trackPositions[currentName];
+		startPosition = std::round(timeToStart * 1000);
+
+		// erase stored position:
+		// if music track will be interrupted again - new position will be written in stop() method
+		// if music track is not interrupted and will finish by timeout/end of file - it will restart from begginning as it should
+		owner->trackPositions.erase(owner->trackPositions.find(currentName));
+
+		if (Mix_FadeInMusicPos(music, 1, 1000, timeToStart) == -1)
+		{
+			logGlobal->error("Unable to play music (%s)", Mix_GetError());
+			return false;
+		}
+	}
+	else
+	{
+		startPosition = 0;
+
+		if(Mix_PlayMusic(music, 1) == -1)
+		{
+			logGlobal->error("Unable to play music (%s)", Mix_GetError());
+			return false;
+		}
+	}
+
+	startTime = GH.input().getTicks();
+	
+	playing = true;
+	return true;
+}
+
+bool MusicEntry::stop(int fade_ms)
+{
+	if (Mix_PlayingMusic())
+	{
+		playing = false;
+		loop = 0;
+		uint32_t endTime = GH.input().getTicks();
+		assert(startTime != uint32_t(-1));
+		float playDuration = (endTime - startTime + startPosition) / 1000.f;
+		owner->trackPositions[currentName] = playDuration;
+		logGlobal->trace("Stopping music file %s at %f", currentName.getOriginalName(), playDuration);
+
+		Mix_FadeOutMusic(fade_ms);
+		return true;
+	}
+	return false;
+}
+
+bool MusicEntry::isPlaying()
+{
+	return playing;
+}
+
+bool MusicEntry::isSet(std::string set)
+{
+	return !setName.empty() && set == setName;
+}
+
+bool MusicEntry::isTrack(const AudioPath & track)
+{
+	return setName.empty() && track == currentName;
+}

+ 167 - 167
client/CMusicHandler.h

@@ -1,167 +1,167 @@
-/*
- * CMusicHandler.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/CConfigHandler.h"
-#include "../lib/CSoundBase.h"
-
-struct _Mix_Music;
-struct SDL_RWops;
-using Mix_Music = struct _Mix_Music;
-struct Mix_Chunk;
-
-class CAudioBase {
-protected:
-	boost::mutex mutex;
-	bool initialized;
-	int volume;					// from 0 (mute) to 100
-
-public:
-	CAudioBase(): initialized(false), volume(0) {};
-	virtual void init() = 0;
-	virtual void release() = 0;
-
-	virtual void setVolume(ui32 percent);
-	ui32 getVolume() const { return volume; };
-};
-
-class CSoundHandler: public CAudioBase
-{
-private:
-	//update volume on configuration change
-	SettingsListener listener;
-	void onVolumeChange(const JsonNode &volumeNode);
-
-	using CachedChunk = std::pair<Mix_Chunk *, std::unique_ptr<ui8[]>>;
-	std::map<AudioPath, CachedChunk> soundChunks;
-	std::map<std::vector<ui8>, CachedChunk> soundChunksRaw;
-
-	Mix_Chunk *GetSoundChunk(const AudioPath & sound, bool cache);
-	Mix_Chunk *GetSoundChunk(std::pair<std::unique_ptr<ui8 []>, si64> & data, bool cache);
-
-	/// have entry for every currently active channel
-	/// vector will be empty if callback was not set
-	std::map<int, std::vector<std::function<void()>> > callbacks;
-
-	/// Protects access to callbacks member to avoid data races:
-	/// SDL calls sound finished callbacks from audio thread
-	boost::mutex mutexCallbacks;
-
-	int ambientDistToVolume(int distance) const;
-	void ambientStopSound(const AudioPath & soundId);
-	void updateChannelVolume(int channel);
-
-	const JsonNode ambientConfig;
-
-	std::map<AudioPath, int> ambientChannels;
-	std::map<int, int> channelVolumes;
-
-	void initCallback(int channel, const std::function<void()> & function);
-	void initCallback(int channel);
-
-public:
-	CSoundHandler();
-
-	void init() override;
-	void release() override;
-
-	void setVolume(ui32 percent) override;
-	void setChannelVolume(int channel, ui32 percent);
-
-	// Sounds
-	int playSound(soundBase::soundID soundID, int repeats=0);
-	int playSound(const AudioPath & sound, int repeats=0, bool cache=false);
-	int playSound(std::pair<std::unique_ptr<ui8 []>, si64> & data, int repeats=0, bool cache=false);
-	int playSoundFromSet(std::vector<soundBase::soundID> &sound_vec);
-	void stopSound(int handler);
-
-	void setCallback(int channel, std::function<void()> function);
-	void soundFinishedCallback(int channel);
-
-	int ambientGetRange() const;
-	void ambientUpdateChannels(std::map<AudioPath, int> currentSounds);
-	void ambientStopAllChannels();
-
-	// Sets
-	std::vector<soundBase::soundID> battleIntroSounds;
-};
-
-class CMusicHandler;
-
-//Class for handling one music file
-class MusicEntry
-{
-	CMusicHandler *owner;
-	Mix_Music *music;
-
-	int loop; // -1 = indefinite
-	bool fromStart;
-	bool playing;
-	uint32_t startTime;
-	uint32_t startPosition;
-	//if not null - set from which music will be randomly selected
-	std::string setName;
-	AudioPath currentName;
-
-	void load(const AudioPath & musicURI);
-
-public:
-	MusicEntry(CMusicHandler *owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart);
-	~MusicEntry();
-
-	bool isSet(std::string setName);
-	bool isTrack(const AudioPath & trackName);
-	bool isPlaying();
-
-	bool play();
-	bool stop(int fade_ms=0);
-};
-
-class CMusicHandler: public CAudioBase
-{
-private:
-	//update volume on configuration change
-	SettingsListener listener;
-	void onVolumeChange(const JsonNode &volumeNode);
-
-	std::unique_ptr<MusicEntry> current;
-	std::unique_ptr<MusicEntry> next;
-
-	void queueNext(CMusicHandler *owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart);
-	void queueNext(std::unique_ptr<MusicEntry> queued);
-	void musicFinishedCallback();
-
-	/// map <set name> -> <list of URI's to tracks belonging to the said set>
-	std::map<std::string, std::vector<AudioPath>> musicsSet;
-	/// stored position, in seconds at which music player should resume playing this track
-	std::map<AudioPath, float> trackPositions;
-
-public:
-	CMusicHandler();
-
-	/// add entry with URI musicURI in set. Track will have ID musicID
-	void addEntryToSet(const std::string & set, const AudioPath & musicURI);
-
-	void init() override;
-	void loadTerrainMusicThemes();
-	void release() override;
-	void setVolume(ui32 percent) override;
-
-	/// play track by URI, if loop = true music will be looped
-	void playMusic(const AudioPath & musicURI, bool loop, bool fromStart);
-	/// play random track from this set
-	void playMusicFromSet(const std::string & musicSet, bool loop, bool fromStart);
-	/// play random track from set (musicSet, entryID)
-	void playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart);
-	/// stops currently playing music by fading out it over fade_ms and starts next scheduled track, if any
-	void stopMusic(int fade_ms=1000);
-
-	friend class MusicEntry;
-};
+/*
+ * CMusicHandler.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/CConfigHandler.h"
+#include "../lib/CSoundBase.h"
+
+struct _Mix_Music;
+struct SDL_RWops;
+using Mix_Music = struct _Mix_Music;
+struct Mix_Chunk;
+
+class CAudioBase {
+protected:
+	boost::mutex mutex;
+	bool initialized;
+	int volume;					// from 0 (mute) to 100
+
+public:
+	CAudioBase(): initialized(false), volume(0) {};
+	virtual void init() = 0;
+	virtual void release() = 0;
+
+	virtual void setVolume(ui32 percent);
+	ui32 getVolume() const { return volume; };
+};
+
+class CSoundHandler: public CAudioBase
+{
+private:
+	//update volume on configuration change
+	SettingsListener listener;
+	void onVolumeChange(const JsonNode &volumeNode);
+
+	using CachedChunk = std::pair<Mix_Chunk *, std::unique_ptr<ui8[]>>;
+	std::map<AudioPath, CachedChunk> soundChunks;
+	std::map<std::vector<ui8>, CachedChunk> soundChunksRaw;
+
+	Mix_Chunk *GetSoundChunk(const AudioPath & sound, bool cache);
+	Mix_Chunk *GetSoundChunk(std::pair<std::unique_ptr<ui8 []>, si64> & data, bool cache);
+
+	/// have entry for every currently active channel
+	/// vector will be empty if callback was not set
+	std::map<int, std::vector<std::function<void()>> > callbacks;
+
+	/// Protects access to callbacks member to avoid data races:
+	/// SDL calls sound finished callbacks from audio thread
+	boost::mutex mutexCallbacks;
+
+	int ambientDistToVolume(int distance) const;
+	void ambientStopSound(const AudioPath & soundId);
+	void updateChannelVolume(int channel);
+
+	const JsonNode ambientConfig;
+
+	std::map<AudioPath, int> ambientChannels;
+	std::map<int, int> channelVolumes;
+
+	void initCallback(int channel, const std::function<void()> & function);
+	void initCallback(int channel);
+
+public:
+	CSoundHandler();
+
+	void init() override;
+	void release() override;
+
+	void setVolume(ui32 percent) override;
+	void setChannelVolume(int channel, ui32 percent);
+
+	// Sounds
+	int playSound(soundBase::soundID soundID, int repeats=0);
+	int playSound(const AudioPath & sound, int repeats=0, bool cache=false);
+	int playSound(std::pair<std::unique_ptr<ui8 []>, si64> & data, int repeats=0, bool cache=false);
+	int playSoundFromSet(std::vector<soundBase::soundID> &sound_vec);
+	void stopSound(int handler);
+
+	void setCallback(int channel, std::function<void()> function);
+	void soundFinishedCallback(int channel);
+
+	int ambientGetRange() const;
+	void ambientUpdateChannels(std::map<AudioPath, int> currentSounds);
+	void ambientStopAllChannels();
+
+	// Sets
+	std::vector<soundBase::soundID> battleIntroSounds;
+};
+
+class CMusicHandler;
+
+//Class for handling one music file
+class MusicEntry
+{
+	CMusicHandler *owner;
+	Mix_Music *music;
+
+	int loop; // -1 = indefinite
+	bool fromStart;
+	bool playing;
+	uint32_t startTime;
+	uint32_t startPosition;
+	//if not null - set from which music will be randomly selected
+	std::string setName;
+	AudioPath currentName;
+
+	void load(const AudioPath & musicURI);
+
+public:
+	MusicEntry(CMusicHandler *owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart);
+	~MusicEntry();
+
+	bool isSet(std::string setName);
+	bool isTrack(const AudioPath & trackName);
+	bool isPlaying();
+
+	bool play();
+	bool stop(int fade_ms=0);
+};
+
+class CMusicHandler: public CAudioBase
+{
+private:
+	//update volume on configuration change
+	SettingsListener listener;
+	void onVolumeChange(const JsonNode &volumeNode);
+
+	std::unique_ptr<MusicEntry> current;
+	std::unique_ptr<MusicEntry> next;
+
+	void queueNext(CMusicHandler *owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart);
+	void queueNext(std::unique_ptr<MusicEntry> queued);
+	void musicFinishedCallback();
+
+	/// map <set name> -> <list of URI's to tracks belonging to the said set>
+	std::map<std::string, std::vector<AudioPath>> musicsSet;
+	/// stored position, in seconds at which music player should resume playing this track
+	std::map<AudioPath, float> trackPositions;
+
+public:
+	CMusicHandler();
+
+	/// add entry with URI musicURI in set. Track will have ID musicID
+	void addEntryToSet(const std::string & set, const AudioPath & musicURI);
+
+	void init() override;
+	void loadTerrainMusicThemes();
+	void release() override;
+	void setVolume(ui32 percent) override;
+
+	/// play track by URI, if loop = true music will be looped
+	void playMusic(const AudioPath & musicURI, bool loop, bool fromStart);
+	/// play random track from this set
+	void playMusicFromSet(const std::string & musicSet, bool loop, bool fromStart);
+	/// play random track from set (musicSet, entryID)
+	void playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart);
+	/// stops currently playing music by fading out it over fade_ms and starts next scheduled track, if any
+	void stopMusic(int fade_ms=1000);
+
+	friend class MusicEntry;
+};

+ 1891 - 1891
client/CPlayerInterface.cpp

@@ -1,1891 +1,1891 @@
-/*
- * CPlayerInterface.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 "CPlayerInterface.h"
-
-#include <vcmi/Artifact.h>
-
-#include "CGameInfo.h"
-#include "CMT.h"
-#include "CMusicHandler.h"
-#include "CServerHandler.h"
-#include "HeroMovementController.h"
-#include "PlayerLocalState.h"
-
-#include "adventureMap/AdventureMapInterface.h"
-#include "adventureMap/CInGameConsole.h"
-#include "adventureMap/CList.h"
-
-#include "battle/BattleEffectsController.h"
-#include "battle/BattleFieldController.h"
-#include "battle/BattleInterface.h"
-#include "battle/BattleInterfaceClasses.h"
-#include "battle/BattleWindow.h"
-
-#include "eventsSDL/InputHandler.h"
-#include "eventsSDL/NotificationHandler.h"
-
-#include "gui/CGuiHandler.h"
-#include "gui/CursorHandler.h"
-#include "gui/WindowHandler.h"
-
-#include "mainmenu/CMainMenu.h"
-#include "mainmenu/CHighScoreScreen.h"
-
-#include "mapView/mapHandler.h"
-
-#include "render/CAnimation.h"
-#include "render/IImage.h"
-
-#include "widgets/Buttons.h"
-#include "widgets/CComponent.h"
-#include "widgets/CGarrisonInt.h"
-
-#include "windows/CCastleInterface.h"
-#include "windows/CCreatureWindow.h"
-#include "windows/CHeroWindow.h"
-#include "windows/CKingdomInterface.h"
-#include "windows/CPuzzleWindow.h"
-#include "windows/CQuestLog.h"
-#include "windows/CSpellWindow.h"
-#include "windows/CTradeWindow.h"
-#include "windows/GUIClasses.h"
-#include "windows/InfoWindows.h"
-
-#include "../CCallback.h"
-
-#include "../lib/CArtHandler.h"
-#include "../lib/CConfigHandler.h"
-#include "../lib/CGeneralTextHandler.h"
-#include "../lib/CHeroHandler.h"
-#include "../lib/CPlayerState.h"
-#include "../lib/CStack.h"
-#include "../lib/CStopWatch.h"
-#include "../lib/CThreadHelper.h"
-#include "../lib/CTownHandler.h"
-#include "../lib/CondSh.h"
-#include "../lib/GameConstants.h"
-#include "../lib/JsonNode.h"
-#include "../lib/NetPacks.h" //todo: remove
-#include "../lib/NetPacksBase.h"
-#include "../lib/RoadHandler.h"
-#include "../lib/StartInfo.h"
-#include "../lib/TerrainHandler.h"
-#include "../lib/TextOperations.h"
-#include "../lib/UnlockGuard.h"
-#include "../lib/VCMIDirs.h"
-
-#include "../lib/bonuses/CBonusSystemNode.h"
-#include "../lib/bonuses/Limiters.h"
-#include "../lib/bonuses/Propagators.h"
-#include "../lib/bonuses/Updaters.h"
-
-#include "../lib/gameState/CGameState.h"
-
-#include "../lib/mapObjects/CGTownInstance.h"
-#include "../lib/mapObjects/MiscObjects.h"
-#include "../lib/mapObjects/ObjectTemplate.h"
-
-#include "../lib/mapping/CMapHeader.h"
-
-#include "../lib/pathfinder/CGPathNode.h"
-
-#include "../lib/serializer/BinaryDeserializer.h"
-#include "../lib/serializer/BinarySerializer.h"
-#include "../lib/serializer/CTypeList.h"
-
-#include "../lib/spells/CSpellHandler.h"
-
-// The macro below is used to mark functions that are called by client when game state changes.
-// They all assume that interface mutex is locked.
-#define EVENT_HANDLER_CALLED_BY_CLIENT
-
-#define BATTLE_EVENT_POSSIBLE_RETURN	\
-	if (LOCPLINT != this)				\
-		return;							\
-	if (isAutoFightOn && !battleInt)	\
-		return;
-
-CPlayerInterface * LOCPLINT;
-
-std::shared_ptr<BattleInterface> CPlayerInterface::battleInt;
-
-struct HeroObjectRetriever
-{
-	const CGHeroInstance * operator()(const ConstTransitivePtr<CGHeroInstance> &h) const
-	{
-		return h;
-	}
-	const CGHeroInstance * operator()(const ConstTransitivePtr<CStackInstance> &s) const
-	{
-		return nullptr;
-	}
-};
-
-CPlayerInterface::CPlayerInterface(PlayerColor Player):
-	localState(std::make_unique<PlayerLocalState>(*this)),
-	movementController(std::make_unique<HeroMovementController>())
-{
-	logGlobal->trace("\tHuman player interface for player %s being constructed", Player.toString());
-	GH.defActionsDef = 0;
-	LOCPLINT = this;
-	playerID=Player;
-	human=true;
-	battleInt = nullptr;
-	castleInt = nullptr;
-	makingTurn = false;
-	showingDialog = new CondSh<bool>(false);
-	cingconsole = new CInGameConsole();
-	firstCall = 1; //if loading will be overwritten in serialize
-	autosaveCount = 0;
-	isAutoFightOn = false;
-	ignoreEvents = false;
-	numOfMovedArts = 0;
-}
-
-CPlayerInterface::~CPlayerInterface()
-{
-	logGlobal->trace("\tHuman player interface for player %s being destructed", playerID.toString());
-	delete showingDialog;
-	delete cingconsole;
-	if (LOCPLINT == this)
-		LOCPLINT = nullptr;
-}
-void CPlayerInterface::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB)
-{
-	cb = CB;
-	env = ENV;
-
-	CCS->musich->loadTerrainMusicThemes();
-	initializeHeroTownList();
-
-	adventureInt.reset(new AdventureMapInterface());
-}
-
-void CPlayerInterface::playerEndsTurn(PlayerColor player)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	if (player == playerID)
-	{
-		makingTurn = false;
-
-		// remove all active dialogs that do not expect query answer
-		for (;;)
-		{
-			auto adventureWindow = GH.windows().topWindow<AdventureMapInterface>();
-			auto infoWindow = GH.windows().topWindow<CInfoWindow>();
-
-			if(adventureWindow != nullptr)
-				break;
-
-			if(infoWindow && infoWindow->ID != QueryID::NONE)
-				break;
-
-			if (infoWindow)
-				infoWindow->close();
-			else
-				GH.windows().popWindows(1);
-		}
-
-		// remove all pending dialogs that do not expect query answer
-		vstd::erase_if(dialogs, [](const std::shared_ptr<CInfoWindow> & window){
-			return window->ID == QueryID::NONE;
-		});
-	}
-}
-
-void CPlayerInterface::playerStartsTurn(PlayerColor player)
-{
-	if(GH.windows().findWindows<AdventureMapInterface>().empty())
-	{
-		// after map load - remove all active windows and replace them with adventure map
-		GH.windows().clear();
-		GH.windows().pushWindow(adventureInt);
-	}
-
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	if (player != playerID && LOCPLINT == this)
-	{
-		waitWhileDialog();
-
-		bool isHuman = cb->getStartInfo()->playerInfos.count(player) && cb->getStartInfo()->playerInfos.at(player).isControlledByHuman();
-
-		if (makingTurn == false)
-			adventureInt->onEnemyTurnStarted(player, isHuman);
-	}
-}
-
-void CPlayerInterface::performAutosave()
-{
-	int frequency = static_cast<int>(settings["general"]["saveFrequency"].Integer());
-	if(frequency > 0 && cb->getDate() % frequency == 0)
-	{
-		bool usePrefix = settings["general"]["useSavePrefix"].Bool();
-		std::string prefix = std::string();
-
-		if(usePrefix)
-		{
-			prefix = settings["general"]["savePrefix"].String();
-			if(prefix.empty())
-			{
-				std::string name = cb->getMapHeader()->name.toString();
-				int txtlen = TextOperations::getUnicodeCharactersCount(name);
-
-				TextOperations::trimRightUnicode(name, std::max(0, txtlen - 15));
-				std::string forbiddenChars("\\/:?\"<>| ");
-				std::replace_if(name.begin(), name.end(), [&](char c) { return std::string::npos != forbiddenChars.find(c); }, '_' );
-
-				prefix = name + "_" + cb->getStartInfo()->startTimeIso8601 + "/";
-			}
-		}
-
-		autosaveCount++;
-
-		int autosaveCountLimit = settings["general"]["autosaveCountLimit"].Integer();
-		if(autosaveCountLimit > 0)
-		{
-			cb->save("Saves/Autosave/" + prefix + std::to_string(autosaveCount));
-			autosaveCount %= autosaveCountLimit;
-		}
-		else
-		{
-			std::string stringifiedDate = std::to_string(cb->getDate(Date::MONTH))
-					+ std::to_string(cb->getDate(Date::WEEK))
-					+ std::to_string(cb->getDate(Date::DAY_OF_WEEK));
-
-			cb->save("Saves/Autosave/" + prefix + stringifiedDate);
-		}
-	}
-}
-
-void CPlayerInterface::gamePause(bool pause)
-{
-	cb->gamePause(pause);
-}
-
-void CPlayerInterface::yourTurn(QueryID queryID)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	{
-		LOCPLINT = this;
-		GH.curInt = this;
-
-		NotificationHandler::notify("Your turn");
-		if(settings["general"]["startTurnAutosave"].Bool())
-		{
-			performAutosave();
-		}
-
-		if (CSH->howManyPlayerInterfaces() > 1) //hot seat message
-		{
-			adventureInt->onHotseatWaitStarted(playerID);
-
-			makingTurn = true;
-			std::string msg = CGI->generaltexth->allTexts[13];
-			boost::replace_first(msg, "%s", cb->getStartInfo()->playerInfos.find(playerID)->second.name);
-			std::vector<std::shared_ptr<CComponent>> cmp;
-			cmp.push_back(std::make_shared<CComponent>(CComponent::flag, playerID.getNum(), 0));
-			showInfoDialog(msg, cmp);
-		}
-		else
-		{
-			makingTurn = true;
-			adventureInt->onPlayerTurnStarted(playerID);
-		}
-	}
-	acceptTurn(queryID);
-}
-
-void CPlayerInterface::acceptTurn(QueryID queryID)
-{
-	if (settings["session"]["autoSkip"].Bool())
-	{
-		while(auto iw = GH.windows().topWindow<CInfoWindow>())
-			iw->close();
-	}
-
-	if(CSH->howManyPlayerInterfaces() > 1)
-	{
-		waitWhileDialog(); // wait for player to accept turn in hot-seat mode
-
-		adventureInt->onPlayerTurnStarted(playerID);
-	}
-
-	// warn player if he has no town
-	if (cb->howManyTowns() == 0)
-	{
-		auto playerColor = *cb->getPlayerID();
-
-		std::vector<Component> components;
-		components.emplace_back(Component::EComponentType::FLAG, playerColor.getNum(), 0, 0);
-		MetaString text;
-
-		const auto & optDaysWithoutCastle = cb->getPlayerState(playerColor)->daysWithoutCastle;
-
-		if(optDaysWithoutCastle)
-		{
-			auto daysWithoutCastle = optDaysWithoutCastle.value();
-			if (daysWithoutCastle < 6)
-			{
-				text.appendLocalString(EMetaText::ARRAY_TXT,128); //%s, you only have %d days left to capture a town or you will be banished from this land.
-				text.replaceLocalString(EMetaText::COLOR, playerColor.getNum());
-				text.replaceNumber(7 - daysWithoutCastle);
-			}
-			else if (daysWithoutCastle == 6)
-			{
-				text.appendLocalString(EMetaText::ARRAY_TXT,129); //%s, this is your last day to capture a town or you will be banished from this land.
-				text.replaceLocalString(EMetaText::COLOR, playerColor.getNum());
-			}
-
-			showInfoDialogAndWait(components, text);
-		}
-		else
-			logGlobal->warn("Player has no towns, but daysWithoutCastle is not set");
-	}
-	
-	cb->selectionMade(0, queryID);
-	movementController->onPlayerTurnStarted();
-}
-
-void CPlayerInterface::heroMoved(const TryMoveHero & details, bool verbose)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	waitWhileDialog();
-	if(LOCPLINT != this)
-		return;
-
-	//FIXME: read once and store
-	if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-ignore-hero"].Bool())
-		return;
-
-	const CGHeroInstance * hero = cb->getHero(details.id); //object representing this hero
-
-	if (!hero)
-		return;
-
-	movementController->onTryMoveHero(hero, details);
-}
-
-void CPlayerInterface::heroKilled(const CGHeroInstance* hero)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	LOG_TRACE_PARAMS(logGlobal, "Hero %s killed handler for player %s", hero->getNameTranslated() % playerID);
-
-	// if hero is not in town garrison
-	if (vstd::contains(localState->getWanderingHeroes(), hero))
-		localState->removeWanderingHero(hero);
-
-	adventureInt->onHeroChanged(hero);
-	localState->erasePath(hero);
-}
-
-void CPlayerInterface::heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	if(start && visitedObj)
-	{
-		if(visitedObj->getVisitSound())
-			CCS->soundh->playSound(visitedObj->getVisitSound().value());
-	}
-}
-
-void CPlayerInterface::heroCreated(const CGHeroInstance * hero)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	localState->addWanderingHero(hero);
-	adventureInt->onHeroChanged(hero);
-}
-void CPlayerInterface::openTownWindow(const CGTownInstance * town)
-{
-	if(castleInt)
-		castleInt->close();
-	castleInt = nullptr;
-
-	auto newCastleInt = std::make_shared<CCastleInterface>(town);
-
-	GH.windows().pushWindow(newCastleInt);
-}
-
-void CPlayerInterface::heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	if (which == PrimarySkill::EXPERIENCE)
-	{
-		for (auto ctw : GH.windows().findWindows<CAltarWindow>())
-			ctw->setExpToLevel();
-	}
-	else
-		adventureInt->onHeroChanged(hero);
-}
-
-void CPlayerInterface::heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	for (auto cuw : GH.windows().findWindows<CUniversityWindow>())
-		cuw->redraw();
-}
-
-void CPlayerInterface::heroManaPointsChanged(const CGHeroInstance * hero)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	adventureInt->onHeroChanged(hero);
-	if (makingTurn && hero->tempOwner == playerID)
-		adventureInt->onHeroChanged(hero);
-}
-void CPlayerInterface::heroMovePointsChanged(const CGHeroInstance * hero)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	if (makingTurn && hero->tempOwner == playerID)
-		adventureInt->onHeroChanged(hero);
-}
-void CPlayerInterface::receivedResource()
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	for (auto mw : GH.windows().findWindows<CMarketplaceWindow>())
-		mw->resourceChanged();
-
-	GH.windows().totalRedraw();
-}
-
-void CPlayerInterface::heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector<SecondarySkill>& skills, QueryID queryID)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	waitWhileDialog();
-	CCS->soundh->playSound(soundBase::heroNewLevel);
-	GH.windows().createAndPushWindow<CLevelWindow>(hero, pskill, skills, [=](ui32 selection)
-	{
-		cb->selectionMade(selection, queryID);
-	});
-}
-
-void CPlayerInterface::commanderGotLevel (const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	waitWhileDialog();
-	CCS->soundh->playSound(soundBase::heroNewLevel);
-	GH.windows().createAndPushWindow<CStackWindow>(commander, skills, [=](ui32 selection)
-	{
-		cb->selectionMade(selection, queryID);
-	});
-}
-
-void CPlayerInterface::heroInGarrisonChange(const CGTownInstance *town)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-
-	if(town->garrisonHero) //wandering hero moved to the garrison
-	{
-		// This method also gets called on hero recruitment -> garrisoned hero is already in garrison
-		if(town->garrisonHero->tempOwner == playerID && vstd::contains(localState->getWanderingHeroes(), town->garrisonHero))
-			localState->removeWanderingHero(town->garrisonHero);
-	}
-
-	if(town->visitingHero) //hero leaves garrison
-	{
-		// This method also gets called on hero recruitment -> wandering heroes already contains new hero
-		if(town->visitingHero->tempOwner == playerID && !vstd::contains(localState->getWanderingHeroes(), town->visitingHero))
-			localState->addWanderingHero(town->visitingHero);
-	}
-	adventureInt->onHeroChanged(nullptr);
-	adventureInt->onTownChanged(town);
-
-	for (auto gh : GH.windows().findWindows<IGarrisonHolder>())
-		gh->updateGarrisons();
-
-	for (auto ki : GH.windows().findWindows<CKingdomInterface>())
-		ki->townChanged(town);
-
-	// Perform totalRedraw to update hero list on adventure map, if any dialogs are open
-	GH.windows().totalRedraw();
-}
-
-void CPlayerInterface::heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	if (hero->tempOwner != playerID )
-		return;
-
-	waitWhileDialog();
-	openTownWindow(town);
-}
-
-void CPlayerInterface::garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2)
-{
-	std::vector<const CGObjectInstance *> instances;
-
-	if(auto obj = cb->getObj(id1))
-		instances.push_back(obj);
-
-
-	if(id2 != ObjectInstanceID() && id2 != id1)
-	{
-		if(auto obj = cb->getObj(id2))
-			instances.push_back(obj);
-	}
-
-	garrisonsChanged(instances);
-}
-
-void CPlayerInterface::garrisonsChanged(std::vector<const CGObjectInstance *> objs)
-{
-	for (auto object : objs)
-	{
-		auto * hero = dynamic_cast<const CGHeroInstance*>(object);
-		auto * town = dynamic_cast<const CGTownInstance*>(object);
-
-		if (hero)
-		{
-			adventureInt->onHeroChanged(hero);
-
-			if(hero->inTownGarrison)
-			{
-				adventureInt->onTownChanged(hero->visitedTown);
-			}
-		}
-		if (town)
-			adventureInt->onTownChanged(town);
-	}
-
-	for (auto cgh : GH.windows().findWindows<IGarrisonHolder>())
-		cgh->updateGarrisons();
-
-	for (auto cmw : GH.windows().findWindows<CTradeWindow>())
-	{
-		if (vstd::contains(objs, cmw->hero))
-			cmw->garrisonChanged();
-	}
-
-	GH.windows().totalRedraw();
-}
-
-void CPlayerInterface::buildChanged(const CGTownInstance *town, BuildingID buildingID, int what) //what: 1 - built, 2 - demolished
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	adventureInt->onTownChanged(town);
-
-	if (castleInt)
-	{
-		castleInt->townlist->updateElement(town);
-
-		if (castleInt->town == town)
-		{
-			switch(what)
-			{
-			case 1:
-				CCS->soundh->playSound(soundBase::newBuilding);
-				castleInt->addBuilding(buildingID);
-				break;
-			case 2:
-				castleInt->removeBuilding(buildingID);
-				break;
-			}
-		}
-
-		// Perform totalRedraw in order to force redraw of updated town list icon from adventure map
-		GH.windows().totalRedraw();
-	}
-
-	for (auto cgh : GH.windows().findWindows<ITownHolder>())
-		cgh->buildChanged();
-}
-
-void CPlayerInterface::battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2)
-{
-	movementController->onBattleStarted();
-
-	//Don't wait for dialogs when we are non-active hot-seat player
-	if (LOCPLINT == this)
-		waitForAllDialogs();
-}
-
-void CPlayerInterface::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-
-	bool useQuickCombat = settings["adventure"]["quickCombat"].Bool();
-	bool forceQuickCombat = settings["adventure"]["forceQuickCombat"].Bool();
-
-	if ((replayAllowed && useQuickCombat) || forceQuickCombat)
-	{
-		autofightingAI = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String());
-
-		AutocombatPreferences autocombatPreferences = AutocombatPreferences();
-		autocombatPreferences.enableSpellsUsage = settings["battle"]["enableAutocombatSpells"].Bool();
-
-		autofightingAI->initBattleInterface(env, cb, autocombatPreferences);
-		autofightingAI->battleStart(battleID, army1, army2, tile, hero1, hero2, side, false);
-		isAutoFightOn = true;
-		cb->registerBattleInterface(autofightingAI);
-	}
-
-	//Don't wait for dialogs when we are non-active hot-seat player
-	if (LOCPLINT == this)
-		waitForAllDialogs();
-
-	BATTLE_EVENT_POSSIBLE_RETURN;
-}
-
-void CPlayerInterface::battleUnitsChanged(const BattleID & battleID, const std::vector<UnitChanges> & units)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	BATTLE_EVENT_POSSIBLE_RETURN;
-
-	for(auto & info : units)
-	{
-		switch(info.operation)
-		{
-		case UnitChanges::EOperation::RESET_STATE:
-			{
-				const CStack * stack = cb->getBattle(battleID)->battleGetStackByID(info.id );
-
-				if(!stack)
-				{
-					logGlobal->error("Invalid unit ID %d", info.id);
-					continue;
-				}
-				battleInt->stackReset(stack);
-			}
-			break;
-		case UnitChanges::EOperation::REMOVE:
-			battleInt->stackRemoved(info.id);
-			break;
-		case UnitChanges::EOperation::ADD:
-			{
-				const CStack * unit = cb->getBattle(battleID)->battleGetStackByID(info.id);
-				if(!unit)
-				{
-					logGlobal->error("Invalid unit ID %d", info.id);
-					continue;
-				}
-				battleInt->stackAdded(unit);
-			}
-			break;
-		default:
-			logGlobal->error("Unknown unit operation %d", (int)info.operation);
-			break;
-		}
-	}
-}
-
-void CPlayerInterface::battleObstaclesChanged(const BattleID & battleID, const std::vector<ObstacleChanges> & obstacles)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	BATTLE_EVENT_POSSIBLE_RETURN;
-
-	std::vector<std::shared_ptr<const CObstacleInstance>> newObstacles;
-	std::vector<ObstacleChanges> removedObstacles;
-
-	for(auto & change : obstacles)
-	{
-		if(change.operation == BattleChanges::EOperation::ADD)
-		{
-			auto instance = cb->getBattle(battleID)->battleGetObstacleByID(change.id);
-			if(instance)
-				newObstacles.push_back(instance);
-			else
-				logNetwork->error("Invalid obstacle instance %d", change.id);
-		}
-		if(change.operation == BattleChanges::EOperation::REMOVE)
-			removedObstacles.push_back(change); //Obstacles are already removed, so, show animation based on json struct
-	}
-
-	if (!newObstacles.empty())
-		battleInt->obstaclePlaced(newObstacles);
-
-	if (!removedObstacles.empty())
-		battleInt->obstacleRemoved(removedObstacles);
-
-	battleInt->fieldController->redrawBackgroundWithHexes();
-}
-
-void CPlayerInterface::battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	BATTLE_EVENT_POSSIBLE_RETURN;
-
-	battleInt->stackIsCatapulting(ca);
-}
-
-void CPlayerInterface::battleNewRound(const BattleID & battleID) //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	BATTLE_EVENT_POSSIBLE_RETURN;
-
-	battleInt->newRound();
-}
-
-void CPlayerInterface::actionStarted(const BattleID & battleID, const BattleAction &action)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	BATTLE_EVENT_POSSIBLE_RETURN;
-
-	battleInt->startAction(action);
-}
-
-void CPlayerInterface::actionFinished(const BattleID & battleID, const BattleAction &action)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	BATTLE_EVENT_POSSIBLE_RETURN;
-
-	battleInt->endAction(action);
-}
-
-void CPlayerInterface::activeStack(const BattleID & battleID, const CStack * stack) //called when it's turn of that stack
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	logGlobal->trace("Awaiting command for %s", stack->nodeName());
-
-	assert(!cb->getBattle(battleID)->battleIsFinished());
-	if (cb->getBattle(battleID)->battleIsFinished())
-	{
-		logGlobal->error("Received CPlayerInterface::activeStack after battle is finished!");
-
-		cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack));
-		return ;
-	}
-
-	if (autofightingAI)
-	{
-		if (isAutoFightOn)
-		{
-			//FIXME: we want client rendering to proceed while AI is making actions
-			// so unlock mutex while AI is busy since this might take quite a while, especially if hero has many spells
-			auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex);
-			autofightingAI->activeStack(battleID, stack);
-			return;
-		}
-
-		cb->unregisterBattleInterface(autofightingAI);
-		autofightingAI.reset();
-	}
-
-	assert(battleInt);
-	if(!battleInt)
-	{
-		// probably battle is finished already
-		cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack));
-	}
-
-	battleInt->stackActivated(stack);
-}
-
-void CPlayerInterface::battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	if(isAutoFightOn || autofightingAI)
-	{
-		isAutoFightOn = false;
-		cb->unregisterBattleInterface(autofightingAI);
-		autofightingAI.reset();
-
-		if(!battleInt)
-		{
-			bool allowManualReplay = queryID != QueryID::NONE;
-
-			auto wnd = std::make_shared<BattleResultWindow>(*br, *this, allowManualReplay);
-
-			if (allowManualReplay)
-			{
-				wnd->resultCallback = [=](ui32 selection)
-				{
-					cb->selectionMade(selection, queryID);
-				};
-			}
-			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();
-			return;
-		}
-	}
-
-	BATTLE_EVENT_POSSIBLE_RETURN;
-
-	battleInt->battleFinished(*br, queryID);
-}
-
-void CPlayerInterface::battleLogMessage(const BattleID & battleID, const std::vector<MetaString> & lines)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	BATTLE_EVENT_POSSIBLE_RETURN;
-
-	battleInt->displayBattleLog(lines);
-}
-
-void CPlayerInterface::battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	BATTLE_EVENT_POSSIBLE_RETURN;
-
-	battleInt->stackMoved(stack, dest, distance, teleport);
-}
-void CPlayerInterface::battleSpellCast(const BattleID & battleID, const BattleSpellCast * sc)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	BATTLE_EVENT_POSSIBLE_RETURN;
-
-	battleInt->spellCast(sc);
-}
-void CPlayerInterface::battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	BATTLE_EVENT_POSSIBLE_RETURN;
-
-	battleInt->battleStacksEffectsSet(sse);
-}
-void CPlayerInterface::battleTriggerEffect(const BattleID & battleID, const BattleTriggerEffect & bte)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	BATTLE_EVENT_POSSIBLE_RETURN;
-
-	battleInt->effectsController->battleTriggerEffect(bte);
-
-	if(bte.effect == vstd::to_underlying(BonusType::MANA_DRAIN))
-	{
-		const CGHeroInstance * manaDrainedHero = LOCPLINT->cb->getHero(ObjectInstanceID(bte.additionalInfo));
-		battleInt->windowObject->heroManaPointsChanged(manaDrainedHero);
-	}
-}
-void CPlayerInterface::battleStacksAttacked(const BattleID & battleID, const std::vector<BattleStackAttacked> & bsa, bool ranged)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	BATTLE_EVENT_POSSIBLE_RETURN;
-
-	std::vector<StackAttackedInfo> arg;
-	for(auto & elem : bsa)
-	{
-		const CStack * defender = cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked, false);
-		const CStack * attacker = cb->getBattle(battleID)->battleGetStackByID(elem.attackerID, false);
-
-		assert(defender);
-
-		StackAttackedInfo     info;
-		info.defender       = defender;
-		info.attacker       = attacker;
-		info.damageDealt    = elem.damageAmount;
-		info.amountKilled   = elem.killedAmount;
-		info.spellEffect    = SpellID::NONE;
-		info.indirectAttack = ranged;
-		info.killed         = elem.killed();
-		info.rebirth        = elem.willRebirth();
-		info.cloneKilled    = elem.cloneKilled();
-		info.fireShield     = elem.fireShield();
-
-		if (elem.isSpell())
-			info.spellEffect = elem.spellID;
-
-		arg.push_back(info);
-	}
-	battleInt->stacksAreAttacked(arg);
-}
-void CPlayerInterface::battleAttack(const BattleID & battleID, const BattleAttack * ba)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	BATTLE_EVENT_POSSIBLE_RETURN;
-
-	StackAttackInfo info;
-	info.attacker = cb->getBattle(battleID)->battleGetStackByID(ba->stackAttacking);
-	info.defender = nullptr;
-	info.indirectAttack = ba->shot();
-	info.lucky = ba->lucky();
-	info.unlucky = ba->unlucky();
-	info.deathBlow = ba->deathBlow();
-	info.lifeDrain = ba->lifeDrain();
-	info.tile = ba->tile;
-	info.spellEffect = SpellID::NONE;
-
-	if (ba->spellLike())
-		info.spellEffect = ba->spellID;
-
-	for(auto & elem : ba->bsa)
-	{
-		if(!elem.isSecondary())
-		{
-			assert(info.defender == nullptr);
-			info.defender = cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked);
-		}
-		else
-		{
-			info.secondaryDefender.push_back(cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked));
-		}
-	}
-	assert(info.defender != nullptr);
-	assert(info.attacker != nullptr);
-
-	battleInt->stackAttacking(info);
-}
-
-void CPlayerInterface::battleGateStateChanged(const BattleID & battleID, const EGateState state)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	BATTLE_EVENT_POSSIBLE_RETURN;
-
-	battleInt->gateStateChanged(state);
-}
-
-void CPlayerInterface::yourTacticPhase(const BattleID & battleID, int distance)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-}
-
-void CPlayerInterface::showInfoDialog(EInfoWindowMode type, const std::string &text, const std::vector<Component> & components, int soundID)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-
-	bool autoTryHover = settings["gameTweaks"]["infoBarPick"].Bool() && type == EInfoWindowMode::AUTO;
-	auto timer = type == EInfoWindowMode::INFO ? 3000 : 4500; //Implement long info windows like in HD mod
-
-	if(autoTryHover || type == EInfoWindowMode::INFO)
-	{
-		waitWhileDialog(); //Fix for mantis #98
-		adventureInt->showInfoBoxMessage(components, text, timer);
-
-		// abort movement, if any. Strictly speaking unnecessary, but prevents some edge cases, like movement sound on visiting Magi Hut with "show messages in status window" on
-		movementController->requestMovementAbort();
-
-		if (makingTurn && GH.windows().count() > 0 && LOCPLINT == this)
-			CCS->soundh->playSound(static_cast<soundBase::soundID>(soundID));
-		return;
-	}
-
-	if (settings["session"]["autoSkip"].Bool() && !GH.isKeyboardShiftDown())
-	{
-		return;
-	}
-	std::vector<Component> vect = components; //I do not know currently how to avoid copy here
-	do
-	{
-		std::vector<Component> sender = {vect.begin(), vect.begin() + std::min(vect.size(), static_cast<size_t>(8))};
-		std::vector<std::shared_ptr<CComponent>> intComps;
-		for (auto & component : sender)
-			intComps.push_back(std::make_shared<CComponent>(component));
-		showInfoDialog(text,intComps,soundID);
-		vect.erase(vect.begin(), vect.begin() + std::min(vect.size(), static_cast<size_t>(8)));
-	}
-	while(!vect.empty());
-}
-
-void CPlayerInterface::showInfoDialog(const std::string & text, std::shared_ptr<CComponent> component)
-{
-	std::vector<std::shared_ptr<CComponent>> intComps;
-	intComps.push_back(component);
-
-	showInfoDialog(text, intComps, soundBase::sound_todo);
-}
-
-void CPlayerInterface::showInfoDialog(const std::string &text, const std::vector<std::shared_ptr<CComponent>> & components, int soundID)
-{
-	LOG_TRACE_PARAMS(logGlobal, "player=%s, text=%s, is LOCPLINT=%d", playerID % text % (this==LOCPLINT));
-	waitWhileDialog();
-
-	if (settings["session"]["autoSkip"].Bool() && !GH.isKeyboardShiftDown())
-	{
-		return;
-	}
-	std::shared_ptr<CInfoWindow> temp = CInfoWindow::create(text, playerID, components);
-
-	if (makingTurn && GH.windows().count() > 0 && LOCPLINT == this)
-	{
-		CCS->soundh->playSound(static_cast<soundBase::soundID>(soundID));
-		showingDialog->set(true);
-		movementController->requestMovementAbort(); // interrupt movement to show dialog
-		GH.windows().pushWindow(temp);
-	}
-	else
-	{
-		dialogs.push_back(temp);
-	}
-}
-
-void CPlayerInterface::showInfoDialogAndWait(std::vector<Component> & components, const MetaString & text)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-
-	std::string str = text.toString();
-
-	showInfoDialog(EInfoWindowMode::MODAL, str, components, 0);
-	waitWhileDialog();
-}
-
-void CPlayerInterface::showYesNoDialog(const std::string &text, CFunctionList<void()> onYes, CFunctionList<void()> onNo, const std::vector<std::shared_ptr<CComponent>> & components)
-{
-	movementController->requestMovementAbort();
-	LOCPLINT->showingDialog->setn(true);
-	CInfoWindow::showYesNoDialog(text, components, onYes, onNo, playerID);
-}
-
-void CPlayerInterface::showBlockingDialog( const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel )
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	waitWhileDialog();
-
-	movementController->requestMovementAbort();
-	CCS->soundh->playSound(static_cast<soundBase::soundID>(soundID));
-
-	if (!selection && cancel) //simple yes/no dialog
-	{
-		std::vector<std::shared_ptr<CComponent>> intComps;
-		for (auto & component : components)
-			intComps.push_back(std::make_shared<CComponent>(component)); //will be deleted by close in window
-
-		showYesNoDialog(text, [=](){ cb->selectionMade(1, askID); }, [=](){ cb->selectionMade(0, askID); }, intComps);
-	}
-	else if (selection)
-	{
-		std::vector<std::shared_ptr<CSelectableComponent>> intComps;
-		for (auto & component : components)
-			intComps.push_back(std::make_shared<CSelectableComponent>(component)); //will be deleted by CSelWindow::close
-
-		std::vector<std::pair<AnimationPath,CFunctionList<void()> > > pom;
-		pom.push_back({ AnimationPath::builtin("IOKAY.DEF"),0});
-		if (cancel)
-		{
-			pom.push_back({AnimationPath::builtin("ICANCEL.DEF"),0});
-		}
-
-		int charperline = 35;
-		if (pom.size() > 1)
-			charperline = 50;
-		GH.windows().createAndPushWindow<CSelWindow>(text, playerID, charperline, intComps, pom, askID);
-		intComps[0]->clickPressed(GH.getCursorPosition());
-		intComps[0]->clickReleased(GH.getCursorPosition());
-	}
-}
-
-void CPlayerInterface::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	movementController->showTeleportDialog(hero, channel, exits, impassable, askID);
-}
-
-void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-
-	auto selectCallback = [=](int selection)
-	{
-		cb->sendQueryReply(selection, askID);
-	};
-
-	auto cancelCallback = [=]()
-	{
-		cb->sendQueryReply(std::nullopt, askID);
-	};
-
-	const std::string localTitle = title.toString();
-	const std::string localDescription = description.toString();
-
-	std::vector<int> tempList;
-	tempList.reserve(objects.size());
-
-	for(auto item : objects)
-		tempList.push_back(item.getNum());
-
-	CComponent localIconC(icon);
-
-	std::shared_ptr<CIntObject> localIcon = localIconC.image;
-	localIconC.removeChild(localIcon.get(), false);
-
-	std::shared_ptr<CObjectListWindow> wnd = std::make_shared<CObjectListWindow>(tempList, localIcon, localTitle, localDescription, selectCallback);
-	wnd->onExit = cancelCallback;
-	GH.windows().pushWindow(wnd);
-}
-
-void CPlayerInterface::tileRevealed(const std::unordered_set<int3> &pos)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	//FIXME: wait for dialog? Magi hut/eye would benefit from this but may break other areas
-	adventureInt->onMapTilesChanged(pos);
-}
-
-void CPlayerInterface::tileHidden(const std::unordered_set<int3> &pos)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	adventureInt->onMapTilesChanged(pos);
-}
-
-void CPlayerInterface::openHeroWindow(const CGHeroInstance *hero)
-{
-	GH.windows().createAndPushWindow<CHeroWindow>(hero);
-}
-
-void CPlayerInterface::availableCreaturesChanged( const CGDwelling *town )
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	if (const CGTownInstance * townObj = dynamic_cast<const CGTownInstance*>(town))
-	{
-		for (auto fortScreen : GH.windows().findWindows<CFortScreen>())
-			fortScreen->creaturesChangedEventHandler();
-
-		for (auto castleInterface : GH.windows().findWindows<CCastleInterface>())
-			if(castleInterface->town == town)
-				castleInterface->creaturesChangedEventHandler();
-
-		if (townObj)
-			for (auto ki : GH.windows().findWindows<CKingdomInterface>())
-				ki->townChanged(townObj);
-	}
-	else if(town && GH.windows().count() > 0 && (town->ID == Obj::CREATURE_GENERATOR1
-		||  town->ID == Obj::CREATURE_GENERATOR4  ||  town->ID == Obj::WAR_MACHINE_FACTORY))
-	{
-		for (auto crw : GH.windows().findWindows<CRecruitmentWindow>())
-			if (crw->dwelling == town)
-				crw->availableCreaturesChanged();
-	}
-}
-
-void CPlayerInterface::heroBonusChanged( const CGHeroInstance *hero, const Bonus &bonus, bool gain )
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	if (bonus.type == BonusType::NONE)
-		return;
-
-	adventureInt->onHeroChanged(hero);
-	if ((bonus.type == BonusType::FLYING_MOVEMENT || bonus.type == BonusType::WATER_WALKING) && !gain)
-	{
-		//recalculate paths because hero has lost bonus influencing pathfinding
-		localState->erasePath(hero);
-	}
-}
-
-void CPlayerInterface::saveGame( BinarySerializer & h, const int version )
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	localState->serialize(h, version);
-}
-
-void CPlayerInterface::loadGame( BinaryDeserializer & h, const int version )
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	localState->serialize(h, version);
-	firstCall = -1;
-}
-
-void CPlayerInterface::moveHero( const CGHeroInstance *h, const CGPath& path )
-{
-	assert(LOCPLINT->makingTurn);
-	assert(h);
-	assert(!showingDialog->get());
-	assert(dialogs.empty());
-
-	LOG_TRACE(logGlobal);
-	if (!LOCPLINT->makingTurn)
-		return;
-	if (!h)
-		return; //can't find hero
-
-	//It shouldn't be possible to move hero with open dialog (or dialog waiting in bg)
-	if (showingDialog->get() || !dialogs.empty())
-		return;
-
-	if (localState->isHeroSleeping(h))
-		localState->setHeroAwaken(h);
-
-	movementController->requestMovementStart(h, path);
-}
-
-void CPlayerInterface::showGarrisonDialog( const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	auto onEnd = [=](){ cb->selectionMade(0, queryID); };
-
-	if (movementController->isHeroMovingThroughGarrison(down, up))
-	{
-		onEnd();
-		return;
-	}
-
-	waitForAllDialogs();
-
-	auto cgw = std::make_shared<CGarrisonWindow>(up, down, removableUnits);
-	cgw->quit->addCallback(onEnd);
-	GH.windows().pushWindow(cgw);
-}
-
-/**
- * Shows the dialog that appears when right-clicking an artifact that can be assembled
- * into a combinational one on an artifact screen. Does not require the combination of
- * artifacts to be legal.
- */
-void CPlayerInterface::showArtifactAssemblyDialog(const Artifact * artifact, const Artifact * assembledArtifact, CFunctionList<void()> onYes)
-{
-	std::string text = artifact->getDescriptionTranslated();
-	text += "\n\n";
-	std::vector<std::shared_ptr<CComponent>> scs;
-
-	if(assembledArtifact)
-	{
-		// You possess all of the components to...
-		text += boost::str(boost::format(CGI->generaltexth->allTexts[732]) % assembledArtifact->getNameTranslated());
-
-		// Picture of assembled artifact at bottom.
-		auto sc = std::make_shared<CComponent>(CComponent::artifact, assembledArtifact->getIndex(), 0);
-		scs.push_back(sc);
-	}
-	else
-	{
-		// Do you wish to disassemble this artifact?
-		text += CGI->generaltexth->allTexts[733];
-	}
-
-	showYesNoDialog(text, onYes, nullptr, scs);
-}
-
-void CPlayerInterface::requestRealized( PackageApplied *pa )
-{
-	if(pa->packType == typeList.getTypeID<MoveHero>())
-		movementController->onMoveHeroApplied();
-
-	if(pa->packType == typeList.getTypeID<QueryReply>())
-		movementController->onQueryReplyApplied();
-}
-
-void CPlayerInterface::showHeroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2)
-{
-	heroExchangeStarted(hero1, hero2, QueryID(-1));
-}
-
-void CPlayerInterface::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	GH.windows().createAndPushWindow<CExchangeWindow>(hero1, hero2, query);
-}
-
-void CPlayerInterface::beforeObjectPropertyChanged(const SetObjectProperty * sop)
-{
-	if (sop->what == ObjProperty::OWNER)
-	{
-		const CGObjectInstance * obj = cb->getObj(sop->id);
-
-		if(obj->ID == Obj::TOWN)
-		{
-			auto town = static_cast<const CGTownInstance *>(obj);
-
-			if(obj->tempOwner == playerID)
-			{
-				localState->removeOwnedTown(town);
-				adventureInt->onTownChanged(town);
-			}
-		}
-	}
-}
-
-void CPlayerInterface::objectPropertyChanged(const SetObjectProperty * sop)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-
-	if (sop->what == ObjProperty::OWNER)
-	{
-		const CGObjectInstance * obj = cb->getObj(sop->id);
-
-		if(obj->ID == Obj::TOWN)
-		{
-			auto town = static_cast<const CGTownInstance *>(obj);
-
-			if(obj->tempOwner == playerID)
-			{
-				localState->addOwnedTown(town);
-				adventureInt->onTownChanged(town);
-			}
-		}
-
-		//redraw minimap if owner changed
-		std::set<int3> pos = obj->getBlockedPos();
-		std::unordered_set<int3> upos(pos.begin(), pos.end());
-		adventureInt->onMapTilesChanged(upos);
-
-		assert(cb->getTownsInfo().size() == localState->getOwnedTowns().size());
-	}
-}
-
-void CPlayerInterface::initializeHeroTownList()
-{
-	if(localState->getWanderingHeroes().empty())
-	{
-		for(auto & hero : cb->getHeroesInfo())
-		{
-			if(!hero->inTownGarrison)
-				localState->addWanderingHero(hero);
-		}
-	}
-
-	if(localState->getOwnedTowns().empty())
-	{
-		for(auto & town : cb->getTownsInfo())
-			localState->addOwnedTown(town);
-	}
-
-	if(adventureInt)
-		adventureInt->onHeroChanged(nullptr);
-}
-
-void CPlayerInterface::showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level, QueryID queryID)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	waitWhileDialog();
-	auto recruitCb = [=](CreatureID id, int count)
-	{
-		cb->recruitCreatures(dwelling, dst, id, count, -1);
-	};
-	auto closeCb = [=]()
-	{
-		cb->selectionMade(0, queryID);
-	};
-	GH.windows().createAndPushWindow<CRecruitmentWindow>(dwelling, level, dst, recruitCb, closeCb);
-}
-
-void CPlayerInterface::waitWhileDialog()
-{
-	if (GH.amIGuiThread())
-	{
-		logGlobal->warn("Cannot wait for dialogs in gui thread (deadlock risk)!");
-		return;
-	}
-
-	auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex);
-	boost::unique_lock<boost::mutex> un(showingDialog->mx);
-	while(showingDialog->data)
-		showingDialog->cond.wait(un);
-}
-
-void CPlayerInterface::showShipyardDialog(const IShipyard *obj)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	auto state = obj->shipyardStatus();
-	TResources cost;
-	obj->getBoatCost(cost);
-	GH.windows().createAndPushWindow<CShipyardWindow>(cost, state, obj->getBoatType(), [=](){ cb->buildBoat(obj); });
-}
-
-void CPlayerInterface::newObject( const CGObjectInstance * obj )
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	//we might have built a boat in shipyard in opened town screen
-	if (obj->ID == Obj::BOAT
-		&& LOCPLINT->castleInt
-		&&  obj->visitablePos() == LOCPLINT->castleInt->town->bestLocation())
-	{
-		CCS->soundh->playSound(soundBase::newBuilding);
-		LOCPLINT->castleInt->addBuilding(BuildingID::SHIP);
-	}
-}
-
-void CPlayerInterface::centerView (int3 pos, int focusTime)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	waitWhileDialog();
-	CCS->curh->hide();
-	adventureInt->centerOnTile(pos);
-	if (focusTime)
-	{
-		GH.windows().totalRedraw();
-		{
-			IgnoreEvents ignore(*this);
-			auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex);
-			boost::this_thread::sleep_for(boost::chrono::milliseconds(focusTime));
-		}
-	}
-	CCS->curh->show();
-}
-
-void CPlayerInterface::objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	if(playerID == initiator && obj->getRemovalSound())
-	{
-		waitWhileDialog();
-		CCS->soundh->playSound(obj->getRemovalSound().value());
-	}
-	CGI->mh->waitForOngoingAnimations();
-
-	if(obj->ID == Obj::HERO && obj->tempOwner == playerID)
-	{
-		const CGHeroInstance * h = static_cast<const CGHeroInstance *>(obj);
-		heroKilled(h);
-	}
-	GH.fakeMouseMove();
-}
-
-void CPlayerInterface::objectRemovedAfter()
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	adventureInt->onMapTilesChanged(boost::none);
-
-	// visiting or garrisoned hero removed - update window
-	if (castleInt)
-		castleInt->updateGarrisons();
-
-	for (auto ki : GH.windows().findWindows<CKingdomInterface>())
-		ki->heroRemoved();
-}
-
-void CPlayerInterface::playerBlocked(int reason, bool start)
-{
-	if(reason == PlayerBlocked::EReason::UPCOMING_BATTLE)
-	{
-		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)
-			LOCPLINT = this;
-			GH.curInt = this;
-			adventureInt->onCurrentPlayerChanged(playerID);
-			std::string msg = CGI->generaltexth->translate("vcmi.adventureMap.playerAttacked");
-			boost::replace_first(msg, "%s", cb->getStartInfo()->playerInfos.find(playerID)->second.name);
-			std::vector<std::shared_ptr<CComponent>> cmp;
-			cmp.push_back(std::make_shared<CComponent>(CComponent::flag, playerID.getNum(), 0));
-			makingTurn = true; //workaround for stiff showInfoDialog implementation
-			showInfoDialog(msg, cmp);
-			makingTurn = false;
-		}
-	}
-}
-
-void CPlayerInterface::update()
-{
-	// Make sure that gamestate won't change when GUI objects may obtain its parts on event processing or drawing request
-	boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
-
-	// While mutexes were locked away we may be have stopped being the active interface
-	if (LOCPLINT != this)
-		return;
-
-	//if there are any waiting dialogs, show them
-	if ((CSH->howManyPlayerInterfaces() <= 1 || makingTurn) && !dialogs.empty() && !showingDialog->get())
-	{
-		showingDialog->set(true);
-		GH.windows().pushWindow(dialogs.front());
-		dialogs.pop_front();
-	}
-
-	assert(adventureInt);
-
-	// Handles mouse and key input
-	GH.handleEvents();
-	GH.windows().simpleRedraw();
-}
-
-int CPlayerInterface::getLastIndex( std::string namePrefix)
-{
-	using namespace boost::filesystem;
-	using namespace boost::algorithm;
-
-	path gamesDir = VCMIDirs::get().userSavePath();
-	std::map<std::time_t, int> dates; //save number => datestamp
-
-	const directory_iterator enddir;
-	if (!exists(gamesDir))
-		create_directory(gamesDir);
-	else
-	for (directory_iterator dir(gamesDir); dir != enddir; ++dir)
-	{
-		if (is_regular_file(dir->status()))
-		{
-			std::string name = dir->path().filename().string();
-			if (starts_with(name, namePrefix) && ends_with(name, ".vcgm1"))
-			{
-				char nr = name[namePrefix.size()];
-				if (std::isdigit(nr))
-					dates[last_write_time(dir->path())] = boost::lexical_cast<int>(nr);
-			}
-		}
-	}
-
-	if (!dates.empty())
-		return (--dates.end())->second; //return latest file number
-	return 0;
-}
-
-void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult )
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-
-	if (player == playerID)
-	{
-		if (victoryLossCheckResult.loss())
-			showInfoDialog(CGI->generaltexth->allTexts[95]);
-
-		assert(GH.curInt == LOCPLINT);
-		auto previousInterface = LOCPLINT; //without multiple player interfaces some of lines below are useless, but for hotseat we wanna swap player interface temporarily
-
-		LOCPLINT = this; //this is needed for dialog to show and avoid freeze, dialog showing logic should be reworked someday
-		GH.curInt = this; //waiting for dialogs requires this to get events
-
-		if(!makingTurn)
-		{
-			makingTurn = true; //also needed for dialog to show with current implementation
-			waitForAllDialogs();
-			makingTurn = false;
-		}
-		else
-			waitForAllDialogs();
-
-		GH.curInt = previousInterface;
-		LOCPLINT = previousInterface;
-
-		if(CSH->howManyPlayerInterfaces() == 1 && !settings["session"]["spectate"].Bool()) //all human players eliminated
-		{
-			if(adventureInt)
-			{
-				GH.windows().popWindows(GH.windows().count());
-				adventureInt.reset();
-			}
-		}
-
-		if (victoryLossCheckResult.victory() && LOCPLINT == this)
-		{
-			// end game if current human player has won
-			CSH->sendClientDisconnecting();
-			requestReturningToMainMenu(true);
-		}
-		else if(CSH->howManyPlayerInterfaces() == 1 && !settings["session"]["spectate"].Bool())
-		{
-			//all human players eliminated
-			CSH->sendClientDisconnecting();
-			requestReturningToMainMenu(false);
-		}
-
-		if (GH.curInt == this)
-			GH.curInt = nullptr;
-	}
-}
-
-void CPlayerInterface::playerBonusChanged( const Bonus &bonus, bool gain )
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-}
-
-void CPlayerInterface::showPuzzleMap()
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	waitWhileDialog();
-
-	//TODO: interface should not know the real position of Grail...
-	double ratio = 0;
-	int3 grailPos = cb->getGrailPos(&ratio);
-
-	GH.windows().createAndPushWindow<CPuzzleWindow>(grailPos, ratio);
-}
-
-void CPlayerInterface::viewWorldMap()
-{
-	adventureInt->openWorldView();
-}
-
-void CPlayerInterface::advmapSpellCast(const CGHeroInstance * caster, SpellID spellID)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-
-	if(GH.windows().topWindow<CSpellWindow>())
-		GH.windows().popWindows(1);
-
-	if(spellID == SpellID::FLY || spellID == SpellID::WATER_WALK)
-		localState->erasePath(caster);
-
-	auto castSoundPath = spellID.toSpell()->getCastSound();
-	if(!castSoundPath.empty())
-		CCS->soundh->playSound(castSoundPath);
-}
-
-void CPlayerInterface::tryDigging(const CGHeroInstance * h)
-{
-	int msgToShow = -1;
-
-	const auto diggingStatus = h->diggingStatus();
-
-	switch(diggingStatus)
-	{
-	case EDiggingStatus::CAN_DIG:
-		break;
-	case EDiggingStatus::LACK_OF_MOVEMENT:
-		msgToShow = 56; //"Digging for artifacts requires a whole day, try again tomorrow."
-		break;
-	case EDiggingStatus::TILE_OCCUPIED:
-		msgToShow = 97; //Try searching on clear ground.
-		break;
-	case EDiggingStatus::WRONG_TERRAIN:
-		msgToShow = 60; ////Try looking on land!
-		break;
-	case EDiggingStatus::BACKPACK_IS_FULL:
-		msgToShow = 247; //Searching for the Grail is fruitless...
-		break;
-	default:
-		assert(0);
-	}
-
-	if(msgToShow < 0)
-		cb->dig(h);
-	else
-		showInfoDialog(CGI->generaltexth->allTexts[msgToShow]);
-}
-
-void CPlayerInterface::battleNewRoundFirst(const BattleID & battleID)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	BATTLE_EVENT_POSSIBLE_RETURN;
-
-	battleInt->newRoundFirst();
-}
-
-void CPlayerInterface::showMarketWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	auto onWindowClosed = [this, queryID](){
-		cb->selectionMade(0, queryID);
-	};
-
-	if(market->allowsTrade(EMarketMode::ARTIFACT_EXP) && visitor->getAlignment() != EAlignment::EVIL)
-		GH.windows().createAndPushWindow<CAltarWindow>(market, visitor, onWindowClosed, EMarketMode::ARTIFACT_EXP);
-	else if(market->allowsTrade(EMarketMode::CREATURE_EXP) && visitor->getAlignment() != EAlignment::GOOD)
-		GH.windows().createAndPushWindow<CAltarWindow>(market, visitor, onWindowClosed, EMarketMode::CREATURE_EXP);
-	else if(market->allowsTrade(EMarketMode::CREATURE_UNDEAD))
-		GH.windows().createAndPushWindow<CTransformerWindow>(market, visitor, onWindowClosed);
-	else if(!market->availableModes().empty())
-		GH.windows().createAndPushWindow<CMarketplaceWindow>(market, visitor, onWindowClosed, market->availableModes().front());
-}
-
-void CPlayerInterface::showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	auto onWindowClosed = [this, queryID](){
-		cb->selectionMade(0, queryID);
-	};
-	GH.windows().createAndPushWindow<CUniversityWindow>(visitor, market, onWindowClosed);
-}
-
-void CPlayerInterface::showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	GH.windows().createAndPushWindow<CHillFortWindow>(visitor, object);
-}
-
-void CPlayerInterface::availableArtifactsChanged(const CGBlackMarket * bm)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	for (auto cmw : GH.windows().findWindows<CMarketplaceWindow>())
-		cmw->artifactsChanged(false);
-}
-
-void CPlayerInterface::showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	auto onWindowClosed = [this, queryID](){
-		cb->selectionMade(0, queryID);
-	};
-	GH.windows().createAndPushWindow<CTavernWindow>(object, onWindowClosed);
-}
-
-void CPlayerInterface::showThievesGuildWindow (const CGObjectInstance * obj)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	GH.windows().createAndPushWindow<CThievesGuildWindow>(obj);
-}
-
-void CPlayerInterface::showQuestLog()
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	GH.windows().createAndPushWindow<CQuestLog>(LOCPLINT->cb->getMyQuests());
-}
-
-void CPlayerInterface::showShipyardDialogOrProblemPopup(const IShipyard *obj)
-{
-	if (obj->shipyardStatus() != IBoatGenerator::GOOD)
-	{
-		MetaString txt;
-		obj->getProblemText(txt);
-		showInfoDialog(txt.toString());
-	}
-	else
-		showShipyardDialog(obj);
-}
-
-void CPlayerInterface::requestReturningToMainMenu(bool won)
-{
-	HighScoreParameter param;
-	param.difficulty = cb->getStartInfo()->difficulty;
-	param.day = cb->getDate();
-	param.townAmount = cb->howManyTowns();
-	param.usedCheat = cb->getPlayerState(*cb->getPlayerID())->cheated;
-	param.hasGrail = false;
-	for(const CGHeroInstance * h : cb->getHeroesInfo())
-		if(h->hasArt(ArtifactID::GRAIL))
-			param.hasGrail = true;
-	for(const CGTownInstance * t : cb->getTownsInfo())
-		if(t->builtBuildings.count(BuildingID::GRAIL))
-			param.hasGrail = true;
-	param.allDefeated = true;
-	for (PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player)
-	{
-		auto ps = cb->getPlayerState(player, false);
-		if(ps && player != *cb->getPlayerID())
-			if(!ps->checkVanquished())
-				param.allDefeated = false;
-	}
-	param.scenarioName = cb->getMapHeader()->name.toString();
-	param.playerName = cb->getStartInfo()->playerInfos.find(*cb->getPlayerID())->second.name;
-	HighScoreCalculation highScoreCalc;
-	highScoreCalc.parameters.push_back(param);
-	highScoreCalc.isCampaign = false;
-
-	if(won && cb->getStartInfo()->campState)
-		CSH->startCampaignScenario(param, cb->getStartInfo()->campState);
-	else
-	{
-		GH.dispatchMainThread(
-			[won, highScoreCalc]()
-			{
-				CSH->endGameplay();
-				GH.defActionsDef = 63;
-				CMM->menu->switchToTab("main");
-				GH.windows().createAndPushWindow<CHighScoreInputScreen>(won, highScoreCalc);
-			}
-		);
-	}
-}
-
-void CPlayerInterface::askToAssembleArtifact(const ArtifactLocation &al)
-{
-	auto hero = std::visit(HeroObjectRetriever(), al.artHolder);
-	if(hero)
-	{
-		auto art = hero->getArt(al.slot);
-		if(art == nullptr)
-		{
-			logGlobal->error("artifact location %d points to nothing",
-							 al.slot.num);
-			return;
-		}
-		ArtifactUtilsClient::askToAssemble(hero, al.slot);
-	}
-}
-
-void CPlayerInterface::artifactPut(const ArtifactLocation &al)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	auto hero = std::visit(HeroObjectRetriever(), al.artHolder);
-	adventureInt->onHeroChanged(hero);
-}
-
-void CPlayerInterface::artifactRemoved(const ArtifactLocation &al)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	auto hero = std::visit(HeroObjectRetriever(), al.artHolder);
-	adventureInt->onHeroChanged(hero);
-
-	for(auto artWin : GH.windows().findWindows<CArtifactHolder>())
-		artWin->artifactRemoved(al);
-
-	waitWhileDialog();
-}
-
-void CPlayerInterface::artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	auto hero = std::visit(HeroObjectRetriever(), dst.artHolder);
-	adventureInt->onHeroChanged(hero);
-
-	bool redraw = true;
-	// If a bulk transfer has arrived, then redrawing only the last art movement.
-	if(numOfMovedArts != 0)
-	{
-		numOfMovedArts--;
-		if(numOfMovedArts != 0)
-			redraw = false;
-	}
-
-	for(auto artWin : GH.windows().findWindows<CArtifactHolder>())
-		artWin->artifactMoved(src, dst, redraw);
-
-	waitWhileDialog();
-}
-
-void CPlayerInterface::bulkArtMovementStart(size_t numOfArts)
-{
-	numOfMovedArts = numOfArts;
-}
-
-void CPlayerInterface::artifactAssembled(const ArtifactLocation &al)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	auto hero = std::visit(HeroObjectRetriever(), al.artHolder);
-	adventureInt->onHeroChanged(hero);
-
-	for(auto artWin : GH.windows().findWindows<CArtifactHolder>())
-		artWin->artifactAssembled(al);
-}
-
-void CPlayerInterface::artifactDisassembled(const ArtifactLocation &al)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	auto hero = std::visit(HeroObjectRetriever(), al.artHolder);
-	adventureInt->onHeroChanged(hero);
-
-	for(auto artWin : GH.windows().findWindows<CArtifactHolder>())
-		artWin->artifactDisassembled(al);
-}
-
-void CPlayerInterface::waitForAllDialogs()
-{
-	while(!dialogs.empty())
-	{
-		auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex);
-		boost::this_thread::sleep_for(boost::chrono::milliseconds(5));
-	}
-	waitWhileDialog();
-}
-
-void CPlayerInterface::proposeLoadingGame()
-{
-	showYesNoDialog(
-		CGI->generaltexth->allTexts[68],
-		[]()
-		{
-			GH.dispatchMainThread(
-				[]()
-				{
-					CSH->endGameplay();
-					GH.defActionsDef = 63;
-					CMM->menu->switchToTab("load");
-				}
-			);
-		},
-		nullptr
-	);
-}
-
-bool CPlayerInterface::capturedAllEvents()
-{
-	if(movementController->isHeroMoving())
-	{
-		//just inform that we are capturing events. they will be processed by heroMoved() in client thread.
-		return true;
-	}
-
-	bool needToLockAdventureMap = adventureInt && adventureInt->isActive() && CGI->mh->hasOngoingAnimations();
-	bool quickCombatOngoing = isAutoFightOn && !battleInt;
-
-	if (ignoreEvents || needToLockAdventureMap || quickCombatOngoing )
-	{
-		GH.input().ignoreEventsUntilInput();
-		return true;
-	}
-
-	return false;
-}
-
-void CPlayerInterface::showWorldViewEx(const std::vector<ObjectPosInfo>& objectPositions, bool showTerrain)
-{
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	adventureInt->openWorldView(objectPositions, showTerrain );
-}
-
-std::optional<BattleAction> CPlayerInterface::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState)
-{
-	return std::nullopt;
-}
+/*
+ * CPlayerInterface.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 "CPlayerInterface.h"
+
+#include <vcmi/Artifact.h>
+
+#include "CGameInfo.h"
+#include "CMT.h"
+#include "CMusicHandler.h"
+#include "CServerHandler.h"
+#include "HeroMovementController.h"
+#include "PlayerLocalState.h"
+
+#include "adventureMap/AdventureMapInterface.h"
+#include "adventureMap/CInGameConsole.h"
+#include "adventureMap/CList.h"
+
+#include "battle/BattleEffectsController.h"
+#include "battle/BattleFieldController.h"
+#include "battle/BattleInterface.h"
+#include "battle/BattleInterfaceClasses.h"
+#include "battle/BattleWindow.h"
+
+#include "eventsSDL/InputHandler.h"
+#include "eventsSDL/NotificationHandler.h"
+
+#include "gui/CGuiHandler.h"
+#include "gui/CursorHandler.h"
+#include "gui/WindowHandler.h"
+
+#include "mainmenu/CMainMenu.h"
+#include "mainmenu/CHighScoreScreen.h"
+
+#include "mapView/mapHandler.h"
+
+#include "render/CAnimation.h"
+#include "render/IImage.h"
+
+#include "widgets/Buttons.h"
+#include "widgets/CComponent.h"
+#include "widgets/CGarrisonInt.h"
+
+#include "windows/CCastleInterface.h"
+#include "windows/CCreatureWindow.h"
+#include "windows/CHeroWindow.h"
+#include "windows/CKingdomInterface.h"
+#include "windows/CPuzzleWindow.h"
+#include "windows/CQuestLog.h"
+#include "windows/CSpellWindow.h"
+#include "windows/CTradeWindow.h"
+#include "windows/GUIClasses.h"
+#include "windows/InfoWindows.h"
+
+#include "../CCallback.h"
+
+#include "../lib/CArtHandler.h"
+#include "../lib/CConfigHandler.h"
+#include "../lib/CGeneralTextHandler.h"
+#include "../lib/CHeroHandler.h"
+#include "../lib/CPlayerState.h"
+#include "../lib/CStack.h"
+#include "../lib/CStopWatch.h"
+#include "../lib/CThreadHelper.h"
+#include "../lib/CTownHandler.h"
+#include "../lib/CondSh.h"
+#include "../lib/GameConstants.h"
+#include "../lib/JsonNode.h"
+#include "../lib/NetPacks.h" //todo: remove
+#include "../lib/NetPacksBase.h"
+#include "../lib/RoadHandler.h"
+#include "../lib/StartInfo.h"
+#include "../lib/TerrainHandler.h"
+#include "../lib/TextOperations.h"
+#include "../lib/UnlockGuard.h"
+#include "../lib/VCMIDirs.h"
+
+#include "../lib/bonuses/CBonusSystemNode.h"
+#include "../lib/bonuses/Limiters.h"
+#include "../lib/bonuses/Propagators.h"
+#include "../lib/bonuses/Updaters.h"
+
+#include "../lib/gameState/CGameState.h"
+
+#include "../lib/mapObjects/CGTownInstance.h"
+#include "../lib/mapObjects/MiscObjects.h"
+#include "../lib/mapObjects/ObjectTemplate.h"
+
+#include "../lib/mapping/CMapHeader.h"
+
+#include "../lib/pathfinder/CGPathNode.h"
+
+#include "../lib/serializer/BinaryDeserializer.h"
+#include "../lib/serializer/BinarySerializer.h"
+#include "../lib/serializer/CTypeList.h"
+
+#include "../lib/spells/CSpellHandler.h"
+
+// The macro below is used to mark functions that are called by client when game state changes.
+// They all assume that interface mutex is locked.
+#define EVENT_HANDLER_CALLED_BY_CLIENT
+
+#define BATTLE_EVENT_POSSIBLE_RETURN	\
+	if (LOCPLINT != this)				\
+		return;							\
+	if (isAutoFightOn && !battleInt)	\
+		return;
+
+CPlayerInterface * LOCPLINT;
+
+std::shared_ptr<BattleInterface> CPlayerInterface::battleInt;
+
+struct HeroObjectRetriever
+{
+	const CGHeroInstance * operator()(const ConstTransitivePtr<CGHeroInstance> &h) const
+	{
+		return h;
+	}
+	const CGHeroInstance * operator()(const ConstTransitivePtr<CStackInstance> &s) const
+	{
+		return nullptr;
+	}
+};
+
+CPlayerInterface::CPlayerInterface(PlayerColor Player):
+	localState(std::make_unique<PlayerLocalState>(*this)),
+	movementController(std::make_unique<HeroMovementController>())
+{
+	logGlobal->trace("\tHuman player interface for player %s being constructed", Player.toString());
+	GH.defActionsDef = 0;
+	LOCPLINT = this;
+	playerID=Player;
+	human=true;
+	battleInt = nullptr;
+	castleInt = nullptr;
+	makingTurn = false;
+	showingDialog = new CondSh<bool>(false);
+	cingconsole = new CInGameConsole();
+	firstCall = 1; //if loading will be overwritten in serialize
+	autosaveCount = 0;
+	isAutoFightOn = false;
+	ignoreEvents = false;
+	numOfMovedArts = 0;
+}
+
+CPlayerInterface::~CPlayerInterface()
+{
+	logGlobal->trace("\tHuman player interface for player %s being destructed", playerID.toString());
+	delete showingDialog;
+	delete cingconsole;
+	if (LOCPLINT == this)
+		LOCPLINT = nullptr;
+}
+void CPlayerInterface::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB)
+{
+	cb = CB;
+	env = ENV;
+
+	CCS->musich->loadTerrainMusicThemes();
+	initializeHeroTownList();
+
+	adventureInt.reset(new AdventureMapInterface());
+}
+
+void CPlayerInterface::playerEndsTurn(PlayerColor player)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	if (player == playerID)
+	{
+		makingTurn = false;
+
+		// remove all active dialogs that do not expect query answer
+		for (;;)
+		{
+			auto adventureWindow = GH.windows().topWindow<AdventureMapInterface>();
+			auto infoWindow = GH.windows().topWindow<CInfoWindow>();
+
+			if(adventureWindow != nullptr)
+				break;
+
+			if(infoWindow && infoWindow->ID != QueryID::NONE)
+				break;
+
+			if (infoWindow)
+				infoWindow->close();
+			else
+				GH.windows().popWindows(1);
+		}
+
+		// remove all pending dialogs that do not expect query answer
+		vstd::erase_if(dialogs, [](const std::shared_ptr<CInfoWindow> & window){
+			return window->ID == QueryID::NONE;
+		});
+	}
+}
+
+void CPlayerInterface::playerStartsTurn(PlayerColor player)
+{
+	if(GH.windows().findWindows<AdventureMapInterface>().empty())
+	{
+		// after map load - remove all active windows and replace them with adventure map
+		GH.windows().clear();
+		GH.windows().pushWindow(adventureInt);
+	}
+
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	if (player != playerID && LOCPLINT == this)
+	{
+		waitWhileDialog();
+
+		bool isHuman = cb->getStartInfo()->playerInfos.count(player) && cb->getStartInfo()->playerInfos.at(player).isControlledByHuman();
+
+		if (makingTurn == false)
+			adventureInt->onEnemyTurnStarted(player, isHuman);
+	}
+}
+
+void CPlayerInterface::performAutosave()
+{
+	int frequency = static_cast<int>(settings["general"]["saveFrequency"].Integer());
+	if(frequency > 0 && cb->getDate() % frequency == 0)
+	{
+		bool usePrefix = settings["general"]["useSavePrefix"].Bool();
+		std::string prefix = std::string();
+
+		if(usePrefix)
+		{
+			prefix = settings["general"]["savePrefix"].String();
+			if(prefix.empty())
+			{
+				std::string name = cb->getMapHeader()->name.toString();
+				int txtlen = TextOperations::getUnicodeCharactersCount(name);
+
+				TextOperations::trimRightUnicode(name, std::max(0, txtlen - 15));
+				std::string forbiddenChars("\\/:?\"<>| ");
+				std::replace_if(name.begin(), name.end(), [&](char c) { return std::string::npos != forbiddenChars.find(c); }, '_' );
+
+				prefix = name + "_" + cb->getStartInfo()->startTimeIso8601 + "/";
+			}
+		}
+
+		autosaveCount++;
+
+		int autosaveCountLimit = settings["general"]["autosaveCountLimit"].Integer();
+		if(autosaveCountLimit > 0)
+		{
+			cb->save("Saves/Autosave/" + prefix + std::to_string(autosaveCount));
+			autosaveCount %= autosaveCountLimit;
+		}
+		else
+		{
+			std::string stringifiedDate = std::to_string(cb->getDate(Date::MONTH))
+					+ std::to_string(cb->getDate(Date::WEEK))
+					+ std::to_string(cb->getDate(Date::DAY_OF_WEEK));
+
+			cb->save("Saves/Autosave/" + prefix + stringifiedDate);
+		}
+	}
+}
+
+void CPlayerInterface::gamePause(bool pause)
+{
+	cb->gamePause(pause);
+}
+
+void CPlayerInterface::yourTurn(QueryID queryID)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	{
+		LOCPLINT = this;
+		GH.curInt = this;
+
+		NotificationHandler::notify("Your turn");
+		if(settings["general"]["startTurnAutosave"].Bool())
+		{
+			performAutosave();
+		}
+
+		if (CSH->howManyPlayerInterfaces() > 1) //hot seat message
+		{
+			adventureInt->onHotseatWaitStarted(playerID);
+
+			makingTurn = true;
+			std::string msg = CGI->generaltexth->allTexts[13];
+			boost::replace_first(msg, "%s", cb->getStartInfo()->playerInfos.find(playerID)->second.name);
+			std::vector<std::shared_ptr<CComponent>> cmp;
+			cmp.push_back(std::make_shared<CComponent>(CComponent::flag, playerID.getNum(), 0));
+			showInfoDialog(msg, cmp);
+		}
+		else
+		{
+			makingTurn = true;
+			adventureInt->onPlayerTurnStarted(playerID);
+		}
+	}
+	acceptTurn(queryID);
+}
+
+void CPlayerInterface::acceptTurn(QueryID queryID)
+{
+	if (settings["session"]["autoSkip"].Bool())
+	{
+		while(auto iw = GH.windows().topWindow<CInfoWindow>())
+			iw->close();
+	}
+
+	if(CSH->howManyPlayerInterfaces() > 1)
+	{
+		waitWhileDialog(); // wait for player to accept turn in hot-seat mode
+
+		adventureInt->onPlayerTurnStarted(playerID);
+	}
+
+	// warn player if he has no town
+	if (cb->howManyTowns() == 0)
+	{
+		auto playerColor = *cb->getPlayerID();
+
+		std::vector<Component> components;
+		components.emplace_back(Component::EComponentType::FLAG, playerColor.getNum(), 0, 0);
+		MetaString text;
+
+		const auto & optDaysWithoutCastle = cb->getPlayerState(playerColor)->daysWithoutCastle;
+
+		if(optDaysWithoutCastle)
+		{
+			auto daysWithoutCastle = optDaysWithoutCastle.value();
+			if (daysWithoutCastle < 6)
+			{
+				text.appendLocalString(EMetaText::ARRAY_TXT,128); //%s, you only have %d days left to capture a town or you will be banished from this land.
+				text.replaceLocalString(EMetaText::COLOR, playerColor.getNum());
+				text.replaceNumber(7 - daysWithoutCastle);
+			}
+			else if (daysWithoutCastle == 6)
+			{
+				text.appendLocalString(EMetaText::ARRAY_TXT,129); //%s, this is your last day to capture a town or you will be banished from this land.
+				text.replaceLocalString(EMetaText::COLOR, playerColor.getNum());
+			}
+
+			showInfoDialogAndWait(components, text);
+		}
+		else
+			logGlobal->warn("Player has no towns, but daysWithoutCastle is not set");
+	}
+	
+	cb->selectionMade(0, queryID);
+	movementController->onPlayerTurnStarted();
+}
+
+void CPlayerInterface::heroMoved(const TryMoveHero & details, bool verbose)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	waitWhileDialog();
+	if(LOCPLINT != this)
+		return;
+
+	//FIXME: read once and store
+	if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-ignore-hero"].Bool())
+		return;
+
+	const CGHeroInstance * hero = cb->getHero(details.id); //object representing this hero
+
+	if (!hero)
+		return;
+
+	movementController->onTryMoveHero(hero, details);
+}
+
+void CPlayerInterface::heroKilled(const CGHeroInstance* hero)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	LOG_TRACE_PARAMS(logGlobal, "Hero %s killed handler for player %s", hero->getNameTranslated() % playerID);
+
+	// if hero is not in town garrison
+	if (vstd::contains(localState->getWanderingHeroes(), hero))
+		localState->removeWanderingHero(hero);
+
+	adventureInt->onHeroChanged(hero);
+	localState->erasePath(hero);
+}
+
+void CPlayerInterface::heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	if(start && visitedObj)
+	{
+		if(visitedObj->getVisitSound())
+			CCS->soundh->playSound(visitedObj->getVisitSound().value());
+	}
+}
+
+void CPlayerInterface::heroCreated(const CGHeroInstance * hero)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	localState->addWanderingHero(hero);
+	adventureInt->onHeroChanged(hero);
+}
+void CPlayerInterface::openTownWindow(const CGTownInstance * town)
+{
+	if(castleInt)
+		castleInt->close();
+	castleInt = nullptr;
+
+	auto newCastleInt = std::make_shared<CCastleInterface>(town);
+
+	GH.windows().pushWindow(newCastleInt);
+}
+
+void CPlayerInterface::heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	if (which == PrimarySkill::EXPERIENCE)
+	{
+		for (auto ctw : GH.windows().findWindows<CAltarWindow>())
+			ctw->setExpToLevel();
+	}
+	else
+		adventureInt->onHeroChanged(hero);
+}
+
+void CPlayerInterface::heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	for (auto cuw : GH.windows().findWindows<CUniversityWindow>())
+		cuw->redraw();
+}
+
+void CPlayerInterface::heroManaPointsChanged(const CGHeroInstance * hero)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	adventureInt->onHeroChanged(hero);
+	if (makingTurn && hero->tempOwner == playerID)
+		adventureInt->onHeroChanged(hero);
+}
+void CPlayerInterface::heroMovePointsChanged(const CGHeroInstance * hero)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	if (makingTurn && hero->tempOwner == playerID)
+		adventureInt->onHeroChanged(hero);
+}
+void CPlayerInterface::receivedResource()
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	for (auto mw : GH.windows().findWindows<CMarketplaceWindow>())
+		mw->resourceChanged();
+
+	GH.windows().totalRedraw();
+}
+
+void CPlayerInterface::heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector<SecondarySkill>& skills, QueryID queryID)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	waitWhileDialog();
+	CCS->soundh->playSound(soundBase::heroNewLevel);
+	GH.windows().createAndPushWindow<CLevelWindow>(hero, pskill, skills, [=](ui32 selection)
+	{
+		cb->selectionMade(selection, queryID);
+	});
+}
+
+void CPlayerInterface::commanderGotLevel (const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	waitWhileDialog();
+	CCS->soundh->playSound(soundBase::heroNewLevel);
+	GH.windows().createAndPushWindow<CStackWindow>(commander, skills, [=](ui32 selection)
+	{
+		cb->selectionMade(selection, queryID);
+	});
+}
+
+void CPlayerInterface::heroInGarrisonChange(const CGTownInstance *town)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+
+	if(town->garrisonHero) //wandering hero moved to the garrison
+	{
+		// This method also gets called on hero recruitment -> garrisoned hero is already in garrison
+		if(town->garrisonHero->tempOwner == playerID && vstd::contains(localState->getWanderingHeroes(), town->garrisonHero))
+			localState->removeWanderingHero(town->garrisonHero);
+	}
+
+	if(town->visitingHero) //hero leaves garrison
+	{
+		// This method also gets called on hero recruitment -> wandering heroes already contains new hero
+		if(town->visitingHero->tempOwner == playerID && !vstd::contains(localState->getWanderingHeroes(), town->visitingHero))
+			localState->addWanderingHero(town->visitingHero);
+	}
+	adventureInt->onHeroChanged(nullptr);
+	adventureInt->onTownChanged(town);
+
+	for (auto gh : GH.windows().findWindows<IGarrisonHolder>())
+		gh->updateGarrisons();
+
+	for (auto ki : GH.windows().findWindows<CKingdomInterface>())
+		ki->townChanged(town);
+
+	// Perform totalRedraw to update hero list on adventure map, if any dialogs are open
+	GH.windows().totalRedraw();
+}
+
+void CPlayerInterface::heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	if (hero->tempOwner != playerID )
+		return;
+
+	waitWhileDialog();
+	openTownWindow(town);
+}
+
+void CPlayerInterface::garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2)
+{
+	std::vector<const CGObjectInstance *> instances;
+
+	if(auto obj = cb->getObj(id1))
+		instances.push_back(obj);
+
+
+	if(id2 != ObjectInstanceID() && id2 != id1)
+	{
+		if(auto obj = cb->getObj(id2))
+			instances.push_back(obj);
+	}
+
+	garrisonsChanged(instances);
+}
+
+void CPlayerInterface::garrisonsChanged(std::vector<const CGObjectInstance *> objs)
+{
+	for (auto object : objs)
+	{
+		auto * hero = dynamic_cast<const CGHeroInstance*>(object);
+		auto * town = dynamic_cast<const CGTownInstance*>(object);
+
+		if (hero)
+		{
+			adventureInt->onHeroChanged(hero);
+
+			if(hero->inTownGarrison)
+			{
+				adventureInt->onTownChanged(hero->visitedTown);
+			}
+		}
+		if (town)
+			adventureInt->onTownChanged(town);
+	}
+
+	for (auto cgh : GH.windows().findWindows<IGarrisonHolder>())
+		cgh->updateGarrisons();
+
+	for (auto cmw : GH.windows().findWindows<CTradeWindow>())
+	{
+		if (vstd::contains(objs, cmw->hero))
+			cmw->garrisonChanged();
+	}
+
+	GH.windows().totalRedraw();
+}
+
+void CPlayerInterface::buildChanged(const CGTownInstance *town, BuildingID buildingID, int what) //what: 1 - built, 2 - demolished
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	adventureInt->onTownChanged(town);
+
+	if (castleInt)
+	{
+		castleInt->townlist->updateElement(town);
+
+		if (castleInt->town == town)
+		{
+			switch(what)
+			{
+			case 1:
+				CCS->soundh->playSound(soundBase::newBuilding);
+				castleInt->addBuilding(buildingID);
+				break;
+			case 2:
+				castleInt->removeBuilding(buildingID);
+				break;
+			}
+		}
+
+		// Perform totalRedraw in order to force redraw of updated town list icon from adventure map
+		GH.windows().totalRedraw();
+	}
+
+	for (auto cgh : GH.windows().findWindows<ITownHolder>())
+		cgh->buildChanged();
+}
+
+void CPlayerInterface::battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2)
+{
+	movementController->onBattleStarted();
+
+	//Don't wait for dialogs when we are non-active hot-seat player
+	if (LOCPLINT == this)
+		waitForAllDialogs();
+}
+
+void CPlayerInterface::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+
+	bool useQuickCombat = settings["adventure"]["quickCombat"].Bool();
+	bool forceQuickCombat = settings["adventure"]["forceQuickCombat"].Bool();
+
+	if ((replayAllowed && useQuickCombat) || forceQuickCombat)
+	{
+		autofightingAI = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String());
+
+		AutocombatPreferences autocombatPreferences = AutocombatPreferences();
+		autocombatPreferences.enableSpellsUsage = settings["battle"]["enableAutocombatSpells"].Bool();
+
+		autofightingAI->initBattleInterface(env, cb, autocombatPreferences);
+		autofightingAI->battleStart(battleID, army1, army2, tile, hero1, hero2, side, false);
+		isAutoFightOn = true;
+		cb->registerBattleInterface(autofightingAI);
+	}
+
+	//Don't wait for dialogs when we are non-active hot-seat player
+	if (LOCPLINT == this)
+		waitForAllDialogs();
+
+	BATTLE_EVENT_POSSIBLE_RETURN;
+}
+
+void CPlayerInterface::battleUnitsChanged(const BattleID & battleID, const std::vector<UnitChanges> & units)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	BATTLE_EVENT_POSSIBLE_RETURN;
+
+	for(auto & info : units)
+	{
+		switch(info.operation)
+		{
+		case UnitChanges::EOperation::RESET_STATE:
+			{
+				const CStack * stack = cb->getBattle(battleID)->battleGetStackByID(info.id );
+
+				if(!stack)
+				{
+					logGlobal->error("Invalid unit ID %d", info.id);
+					continue;
+				}
+				battleInt->stackReset(stack);
+			}
+			break;
+		case UnitChanges::EOperation::REMOVE:
+			battleInt->stackRemoved(info.id);
+			break;
+		case UnitChanges::EOperation::ADD:
+			{
+				const CStack * unit = cb->getBattle(battleID)->battleGetStackByID(info.id);
+				if(!unit)
+				{
+					logGlobal->error("Invalid unit ID %d", info.id);
+					continue;
+				}
+				battleInt->stackAdded(unit);
+			}
+			break;
+		default:
+			logGlobal->error("Unknown unit operation %d", (int)info.operation);
+			break;
+		}
+	}
+}
+
+void CPlayerInterface::battleObstaclesChanged(const BattleID & battleID, const std::vector<ObstacleChanges> & obstacles)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	BATTLE_EVENT_POSSIBLE_RETURN;
+
+	std::vector<std::shared_ptr<const CObstacleInstance>> newObstacles;
+	std::vector<ObstacleChanges> removedObstacles;
+
+	for(auto & change : obstacles)
+	{
+		if(change.operation == BattleChanges::EOperation::ADD)
+		{
+			auto instance = cb->getBattle(battleID)->battleGetObstacleByID(change.id);
+			if(instance)
+				newObstacles.push_back(instance);
+			else
+				logNetwork->error("Invalid obstacle instance %d", change.id);
+		}
+		if(change.operation == BattleChanges::EOperation::REMOVE)
+			removedObstacles.push_back(change); //Obstacles are already removed, so, show animation based on json struct
+	}
+
+	if (!newObstacles.empty())
+		battleInt->obstaclePlaced(newObstacles);
+
+	if (!removedObstacles.empty())
+		battleInt->obstacleRemoved(removedObstacles);
+
+	battleInt->fieldController->redrawBackgroundWithHexes();
+}
+
+void CPlayerInterface::battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	BATTLE_EVENT_POSSIBLE_RETURN;
+
+	battleInt->stackIsCatapulting(ca);
+}
+
+void CPlayerInterface::battleNewRound(const BattleID & battleID) //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	BATTLE_EVENT_POSSIBLE_RETURN;
+
+	battleInt->newRound();
+}
+
+void CPlayerInterface::actionStarted(const BattleID & battleID, const BattleAction &action)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	BATTLE_EVENT_POSSIBLE_RETURN;
+
+	battleInt->startAction(action);
+}
+
+void CPlayerInterface::actionFinished(const BattleID & battleID, const BattleAction &action)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	BATTLE_EVENT_POSSIBLE_RETURN;
+
+	battleInt->endAction(action);
+}
+
+void CPlayerInterface::activeStack(const BattleID & battleID, const CStack * stack) //called when it's turn of that stack
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	logGlobal->trace("Awaiting command for %s", stack->nodeName());
+
+	assert(!cb->getBattle(battleID)->battleIsFinished());
+	if (cb->getBattle(battleID)->battleIsFinished())
+	{
+		logGlobal->error("Received CPlayerInterface::activeStack after battle is finished!");
+
+		cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack));
+		return ;
+	}
+
+	if (autofightingAI)
+	{
+		if (isAutoFightOn)
+		{
+			//FIXME: we want client rendering to proceed while AI is making actions
+			// so unlock mutex while AI is busy since this might take quite a while, especially if hero has many spells
+			auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex);
+			autofightingAI->activeStack(battleID, stack);
+			return;
+		}
+
+		cb->unregisterBattleInterface(autofightingAI);
+		autofightingAI.reset();
+	}
+
+	assert(battleInt);
+	if(!battleInt)
+	{
+		// probably battle is finished already
+		cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack));
+	}
+
+	battleInt->stackActivated(stack);
+}
+
+void CPlayerInterface::battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	if(isAutoFightOn || autofightingAI)
+	{
+		isAutoFightOn = false;
+		cb->unregisterBattleInterface(autofightingAI);
+		autofightingAI.reset();
+
+		if(!battleInt)
+		{
+			bool allowManualReplay = queryID != QueryID::NONE;
+
+			auto wnd = std::make_shared<BattleResultWindow>(*br, *this, allowManualReplay);
+
+			if (allowManualReplay)
+			{
+				wnd->resultCallback = [=](ui32 selection)
+				{
+					cb->selectionMade(selection, queryID);
+				};
+			}
+			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();
+			return;
+		}
+	}
+
+	BATTLE_EVENT_POSSIBLE_RETURN;
+
+	battleInt->battleFinished(*br, queryID);
+}
+
+void CPlayerInterface::battleLogMessage(const BattleID & battleID, const std::vector<MetaString> & lines)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	BATTLE_EVENT_POSSIBLE_RETURN;
+
+	battleInt->displayBattleLog(lines);
+}
+
+void CPlayerInterface::battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	BATTLE_EVENT_POSSIBLE_RETURN;
+
+	battleInt->stackMoved(stack, dest, distance, teleport);
+}
+void CPlayerInterface::battleSpellCast(const BattleID & battleID, const BattleSpellCast * sc)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	BATTLE_EVENT_POSSIBLE_RETURN;
+
+	battleInt->spellCast(sc);
+}
+void CPlayerInterface::battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	BATTLE_EVENT_POSSIBLE_RETURN;
+
+	battleInt->battleStacksEffectsSet(sse);
+}
+void CPlayerInterface::battleTriggerEffect(const BattleID & battleID, const BattleTriggerEffect & bte)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	BATTLE_EVENT_POSSIBLE_RETURN;
+
+	battleInt->effectsController->battleTriggerEffect(bte);
+
+	if(bte.effect == vstd::to_underlying(BonusType::MANA_DRAIN))
+	{
+		const CGHeroInstance * manaDrainedHero = LOCPLINT->cb->getHero(ObjectInstanceID(bte.additionalInfo));
+		battleInt->windowObject->heroManaPointsChanged(manaDrainedHero);
+	}
+}
+void CPlayerInterface::battleStacksAttacked(const BattleID & battleID, const std::vector<BattleStackAttacked> & bsa, bool ranged)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	BATTLE_EVENT_POSSIBLE_RETURN;
+
+	std::vector<StackAttackedInfo> arg;
+	for(auto & elem : bsa)
+	{
+		const CStack * defender = cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked, false);
+		const CStack * attacker = cb->getBattle(battleID)->battleGetStackByID(elem.attackerID, false);
+
+		assert(defender);
+
+		StackAttackedInfo     info;
+		info.defender       = defender;
+		info.attacker       = attacker;
+		info.damageDealt    = elem.damageAmount;
+		info.amountKilled   = elem.killedAmount;
+		info.spellEffect    = SpellID::NONE;
+		info.indirectAttack = ranged;
+		info.killed         = elem.killed();
+		info.rebirth        = elem.willRebirth();
+		info.cloneKilled    = elem.cloneKilled();
+		info.fireShield     = elem.fireShield();
+
+		if (elem.isSpell())
+			info.spellEffect = elem.spellID;
+
+		arg.push_back(info);
+	}
+	battleInt->stacksAreAttacked(arg);
+}
+void CPlayerInterface::battleAttack(const BattleID & battleID, const BattleAttack * ba)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	BATTLE_EVENT_POSSIBLE_RETURN;
+
+	StackAttackInfo info;
+	info.attacker = cb->getBattle(battleID)->battleGetStackByID(ba->stackAttacking);
+	info.defender = nullptr;
+	info.indirectAttack = ba->shot();
+	info.lucky = ba->lucky();
+	info.unlucky = ba->unlucky();
+	info.deathBlow = ba->deathBlow();
+	info.lifeDrain = ba->lifeDrain();
+	info.tile = ba->tile;
+	info.spellEffect = SpellID::NONE;
+
+	if (ba->spellLike())
+		info.spellEffect = ba->spellID;
+
+	for(auto & elem : ba->bsa)
+	{
+		if(!elem.isSecondary())
+		{
+			assert(info.defender == nullptr);
+			info.defender = cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked);
+		}
+		else
+		{
+			info.secondaryDefender.push_back(cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked));
+		}
+	}
+	assert(info.defender != nullptr);
+	assert(info.attacker != nullptr);
+
+	battleInt->stackAttacking(info);
+}
+
+void CPlayerInterface::battleGateStateChanged(const BattleID & battleID, const EGateState state)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	BATTLE_EVENT_POSSIBLE_RETURN;
+
+	battleInt->gateStateChanged(state);
+}
+
+void CPlayerInterface::yourTacticPhase(const BattleID & battleID, int distance)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+}
+
+void CPlayerInterface::showInfoDialog(EInfoWindowMode type, const std::string &text, const std::vector<Component> & components, int soundID)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+
+	bool autoTryHover = settings["gameTweaks"]["infoBarPick"].Bool() && type == EInfoWindowMode::AUTO;
+	auto timer = type == EInfoWindowMode::INFO ? 3000 : 4500; //Implement long info windows like in HD mod
+
+	if(autoTryHover || type == EInfoWindowMode::INFO)
+	{
+		waitWhileDialog(); //Fix for mantis #98
+		adventureInt->showInfoBoxMessage(components, text, timer);
+
+		// abort movement, if any. Strictly speaking unnecessary, but prevents some edge cases, like movement sound on visiting Magi Hut with "show messages in status window" on
+		movementController->requestMovementAbort();
+
+		if (makingTurn && GH.windows().count() > 0 && LOCPLINT == this)
+			CCS->soundh->playSound(static_cast<soundBase::soundID>(soundID));
+		return;
+	}
+
+	if (settings["session"]["autoSkip"].Bool() && !GH.isKeyboardShiftDown())
+	{
+		return;
+	}
+	std::vector<Component> vect = components; //I do not know currently how to avoid copy here
+	do
+	{
+		std::vector<Component> sender = {vect.begin(), vect.begin() + std::min(vect.size(), static_cast<size_t>(8))};
+		std::vector<std::shared_ptr<CComponent>> intComps;
+		for (auto & component : sender)
+			intComps.push_back(std::make_shared<CComponent>(component));
+		showInfoDialog(text,intComps,soundID);
+		vect.erase(vect.begin(), vect.begin() + std::min(vect.size(), static_cast<size_t>(8)));
+	}
+	while(!vect.empty());
+}
+
+void CPlayerInterface::showInfoDialog(const std::string & text, std::shared_ptr<CComponent> component)
+{
+	std::vector<std::shared_ptr<CComponent>> intComps;
+	intComps.push_back(component);
+
+	showInfoDialog(text, intComps, soundBase::sound_todo);
+}
+
+void CPlayerInterface::showInfoDialog(const std::string &text, const std::vector<std::shared_ptr<CComponent>> & components, int soundID)
+{
+	LOG_TRACE_PARAMS(logGlobal, "player=%s, text=%s, is LOCPLINT=%d", playerID % text % (this==LOCPLINT));
+	waitWhileDialog();
+
+	if (settings["session"]["autoSkip"].Bool() && !GH.isKeyboardShiftDown())
+	{
+		return;
+	}
+	std::shared_ptr<CInfoWindow> temp = CInfoWindow::create(text, playerID, components);
+
+	if (makingTurn && GH.windows().count() > 0 && LOCPLINT == this)
+	{
+		CCS->soundh->playSound(static_cast<soundBase::soundID>(soundID));
+		showingDialog->set(true);
+		movementController->requestMovementAbort(); // interrupt movement to show dialog
+		GH.windows().pushWindow(temp);
+	}
+	else
+	{
+		dialogs.push_back(temp);
+	}
+}
+
+void CPlayerInterface::showInfoDialogAndWait(std::vector<Component> & components, const MetaString & text)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+
+	std::string str = text.toString();
+
+	showInfoDialog(EInfoWindowMode::MODAL, str, components, 0);
+	waitWhileDialog();
+}
+
+void CPlayerInterface::showYesNoDialog(const std::string &text, CFunctionList<void()> onYes, CFunctionList<void()> onNo, const std::vector<std::shared_ptr<CComponent>> & components)
+{
+	movementController->requestMovementAbort();
+	LOCPLINT->showingDialog->setn(true);
+	CInfoWindow::showYesNoDialog(text, components, onYes, onNo, playerID);
+}
+
+void CPlayerInterface::showBlockingDialog( const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel )
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	waitWhileDialog();
+
+	movementController->requestMovementAbort();
+	CCS->soundh->playSound(static_cast<soundBase::soundID>(soundID));
+
+	if (!selection && cancel) //simple yes/no dialog
+	{
+		std::vector<std::shared_ptr<CComponent>> intComps;
+		for (auto & component : components)
+			intComps.push_back(std::make_shared<CComponent>(component)); //will be deleted by close in window
+
+		showYesNoDialog(text, [=](){ cb->selectionMade(1, askID); }, [=](){ cb->selectionMade(0, askID); }, intComps);
+	}
+	else if (selection)
+	{
+		std::vector<std::shared_ptr<CSelectableComponent>> intComps;
+		for (auto & component : components)
+			intComps.push_back(std::make_shared<CSelectableComponent>(component)); //will be deleted by CSelWindow::close
+
+		std::vector<std::pair<AnimationPath,CFunctionList<void()> > > pom;
+		pom.push_back({ AnimationPath::builtin("IOKAY.DEF"),0});
+		if (cancel)
+		{
+			pom.push_back({AnimationPath::builtin("ICANCEL.DEF"),0});
+		}
+
+		int charperline = 35;
+		if (pom.size() > 1)
+			charperline = 50;
+		GH.windows().createAndPushWindow<CSelWindow>(text, playerID, charperline, intComps, pom, askID);
+		intComps[0]->clickPressed(GH.getCursorPosition());
+		intComps[0]->clickReleased(GH.getCursorPosition());
+	}
+}
+
+void CPlayerInterface::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	movementController->showTeleportDialog(hero, channel, exits, impassable, askID);
+}
+
+void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+
+	auto selectCallback = [=](int selection)
+	{
+		cb->sendQueryReply(selection, askID);
+	};
+
+	auto cancelCallback = [=]()
+	{
+		cb->sendQueryReply(std::nullopt, askID);
+	};
+
+	const std::string localTitle = title.toString();
+	const std::string localDescription = description.toString();
+
+	std::vector<int> tempList;
+	tempList.reserve(objects.size());
+
+	for(auto item : objects)
+		tempList.push_back(item.getNum());
+
+	CComponent localIconC(icon);
+
+	std::shared_ptr<CIntObject> localIcon = localIconC.image;
+	localIconC.removeChild(localIcon.get(), false);
+
+	std::shared_ptr<CObjectListWindow> wnd = std::make_shared<CObjectListWindow>(tempList, localIcon, localTitle, localDescription, selectCallback);
+	wnd->onExit = cancelCallback;
+	GH.windows().pushWindow(wnd);
+}
+
+void CPlayerInterface::tileRevealed(const std::unordered_set<int3> &pos)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	//FIXME: wait for dialog? Magi hut/eye would benefit from this but may break other areas
+	adventureInt->onMapTilesChanged(pos);
+}
+
+void CPlayerInterface::tileHidden(const std::unordered_set<int3> &pos)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	adventureInt->onMapTilesChanged(pos);
+}
+
+void CPlayerInterface::openHeroWindow(const CGHeroInstance *hero)
+{
+	GH.windows().createAndPushWindow<CHeroWindow>(hero);
+}
+
+void CPlayerInterface::availableCreaturesChanged( const CGDwelling *town )
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	if (const CGTownInstance * townObj = dynamic_cast<const CGTownInstance*>(town))
+	{
+		for (auto fortScreen : GH.windows().findWindows<CFortScreen>())
+			fortScreen->creaturesChangedEventHandler();
+
+		for (auto castleInterface : GH.windows().findWindows<CCastleInterface>())
+			if(castleInterface->town == town)
+				castleInterface->creaturesChangedEventHandler();
+
+		if (townObj)
+			for (auto ki : GH.windows().findWindows<CKingdomInterface>())
+				ki->townChanged(townObj);
+	}
+	else if(town && GH.windows().count() > 0 && (town->ID == Obj::CREATURE_GENERATOR1
+		||  town->ID == Obj::CREATURE_GENERATOR4  ||  town->ID == Obj::WAR_MACHINE_FACTORY))
+	{
+		for (auto crw : GH.windows().findWindows<CRecruitmentWindow>())
+			if (crw->dwelling == town)
+				crw->availableCreaturesChanged();
+	}
+}
+
+void CPlayerInterface::heroBonusChanged( const CGHeroInstance *hero, const Bonus &bonus, bool gain )
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	if (bonus.type == BonusType::NONE)
+		return;
+
+	adventureInt->onHeroChanged(hero);
+	if ((bonus.type == BonusType::FLYING_MOVEMENT || bonus.type == BonusType::WATER_WALKING) && !gain)
+	{
+		//recalculate paths because hero has lost bonus influencing pathfinding
+		localState->erasePath(hero);
+	}
+}
+
+void CPlayerInterface::saveGame( BinarySerializer & h, const int version )
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	localState->serialize(h, version);
+}
+
+void CPlayerInterface::loadGame( BinaryDeserializer & h, const int version )
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	localState->serialize(h, version);
+	firstCall = -1;
+}
+
+void CPlayerInterface::moveHero( const CGHeroInstance *h, const CGPath& path )
+{
+	assert(LOCPLINT->makingTurn);
+	assert(h);
+	assert(!showingDialog->get());
+	assert(dialogs.empty());
+
+	LOG_TRACE(logGlobal);
+	if (!LOCPLINT->makingTurn)
+		return;
+	if (!h)
+		return; //can't find hero
+
+	//It shouldn't be possible to move hero with open dialog (or dialog waiting in bg)
+	if (showingDialog->get() || !dialogs.empty())
+		return;
+
+	if (localState->isHeroSleeping(h))
+		localState->setHeroAwaken(h);
+
+	movementController->requestMovementStart(h, path);
+}
+
+void CPlayerInterface::showGarrisonDialog( const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	auto onEnd = [=](){ cb->selectionMade(0, queryID); };
+
+	if (movementController->isHeroMovingThroughGarrison(down, up))
+	{
+		onEnd();
+		return;
+	}
+
+	waitForAllDialogs();
+
+	auto cgw = std::make_shared<CGarrisonWindow>(up, down, removableUnits);
+	cgw->quit->addCallback(onEnd);
+	GH.windows().pushWindow(cgw);
+}
+
+/**
+ * Shows the dialog that appears when right-clicking an artifact that can be assembled
+ * into a combinational one on an artifact screen. Does not require the combination of
+ * artifacts to be legal.
+ */
+void CPlayerInterface::showArtifactAssemblyDialog(const Artifact * artifact, const Artifact * assembledArtifact, CFunctionList<void()> onYes)
+{
+	std::string text = artifact->getDescriptionTranslated();
+	text += "\n\n";
+	std::vector<std::shared_ptr<CComponent>> scs;
+
+	if(assembledArtifact)
+	{
+		// You possess all of the components to...
+		text += boost::str(boost::format(CGI->generaltexth->allTexts[732]) % assembledArtifact->getNameTranslated());
+
+		// Picture of assembled artifact at bottom.
+		auto sc = std::make_shared<CComponent>(CComponent::artifact, assembledArtifact->getIndex(), 0);
+		scs.push_back(sc);
+	}
+	else
+	{
+		// Do you wish to disassemble this artifact?
+		text += CGI->generaltexth->allTexts[733];
+	}
+
+	showYesNoDialog(text, onYes, nullptr, scs);
+}
+
+void CPlayerInterface::requestRealized( PackageApplied *pa )
+{
+	if(pa->packType == typeList.getTypeID<MoveHero>())
+		movementController->onMoveHeroApplied();
+
+	if(pa->packType == typeList.getTypeID<QueryReply>())
+		movementController->onQueryReplyApplied();
+}
+
+void CPlayerInterface::showHeroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2)
+{
+	heroExchangeStarted(hero1, hero2, QueryID(-1));
+}
+
+void CPlayerInterface::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	GH.windows().createAndPushWindow<CExchangeWindow>(hero1, hero2, query);
+}
+
+void CPlayerInterface::beforeObjectPropertyChanged(const SetObjectProperty * sop)
+{
+	if (sop->what == ObjProperty::OWNER)
+	{
+		const CGObjectInstance * obj = cb->getObj(sop->id);
+
+		if(obj->ID == Obj::TOWN)
+		{
+			auto town = static_cast<const CGTownInstance *>(obj);
+
+			if(obj->tempOwner == playerID)
+			{
+				localState->removeOwnedTown(town);
+				adventureInt->onTownChanged(town);
+			}
+		}
+	}
+}
+
+void CPlayerInterface::objectPropertyChanged(const SetObjectProperty * sop)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+
+	if (sop->what == ObjProperty::OWNER)
+	{
+		const CGObjectInstance * obj = cb->getObj(sop->id);
+
+		if(obj->ID == Obj::TOWN)
+		{
+			auto town = static_cast<const CGTownInstance *>(obj);
+
+			if(obj->tempOwner == playerID)
+			{
+				localState->addOwnedTown(town);
+				adventureInt->onTownChanged(town);
+			}
+		}
+
+		//redraw minimap if owner changed
+		std::set<int3> pos = obj->getBlockedPos();
+		std::unordered_set<int3> upos(pos.begin(), pos.end());
+		adventureInt->onMapTilesChanged(upos);
+
+		assert(cb->getTownsInfo().size() == localState->getOwnedTowns().size());
+	}
+}
+
+void CPlayerInterface::initializeHeroTownList()
+{
+	if(localState->getWanderingHeroes().empty())
+	{
+		for(auto & hero : cb->getHeroesInfo())
+		{
+			if(!hero->inTownGarrison)
+				localState->addWanderingHero(hero);
+		}
+	}
+
+	if(localState->getOwnedTowns().empty())
+	{
+		for(auto & town : cb->getTownsInfo())
+			localState->addOwnedTown(town);
+	}
+
+	if(adventureInt)
+		adventureInt->onHeroChanged(nullptr);
+}
+
+void CPlayerInterface::showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level, QueryID queryID)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	waitWhileDialog();
+	auto recruitCb = [=](CreatureID id, int count)
+	{
+		cb->recruitCreatures(dwelling, dst, id, count, -1);
+	};
+	auto closeCb = [=]()
+	{
+		cb->selectionMade(0, queryID);
+	};
+	GH.windows().createAndPushWindow<CRecruitmentWindow>(dwelling, level, dst, recruitCb, closeCb);
+}
+
+void CPlayerInterface::waitWhileDialog()
+{
+	if (GH.amIGuiThread())
+	{
+		logGlobal->warn("Cannot wait for dialogs in gui thread (deadlock risk)!");
+		return;
+	}
+
+	auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex);
+	boost::unique_lock<boost::mutex> un(showingDialog->mx);
+	while(showingDialog->data)
+		showingDialog->cond.wait(un);
+}
+
+void CPlayerInterface::showShipyardDialog(const IShipyard *obj)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	auto state = obj->shipyardStatus();
+	TResources cost;
+	obj->getBoatCost(cost);
+	GH.windows().createAndPushWindow<CShipyardWindow>(cost, state, obj->getBoatType(), [=](){ cb->buildBoat(obj); });
+}
+
+void CPlayerInterface::newObject( const CGObjectInstance * obj )
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	//we might have built a boat in shipyard in opened town screen
+	if (obj->ID == Obj::BOAT
+		&& LOCPLINT->castleInt
+		&&  obj->visitablePos() == LOCPLINT->castleInt->town->bestLocation())
+	{
+		CCS->soundh->playSound(soundBase::newBuilding);
+		LOCPLINT->castleInt->addBuilding(BuildingID::SHIP);
+	}
+}
+
+void CPlayerInterface::centerView (int3 pos, int focusTime)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	waitWhileDialog();
+	CCS->curh->hide();
+	adventureInt->centerOnTile(pos);
+	if (focusTime)
+	{
+		GH.windows().totalRedraw();
+		{
+			IgnoreEvents ignore(*this);
+			auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex);
+			boost::this_thread::sleep_for(boost::chrono::milliseconds(focusTime));
+		}
+	}
+	CCS->curh->show();
+}
+
+void CPlayerInterface::objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	if(playerID == initiator && obj->getRemovalSound())
+	{
+		waitWhileDialog();
+		CCS->soundh->playSound(obj->getRemovalSound().value());
+	}
+	CGI->mh->waitForOngoingAnimations();
+
+	if(obj->ID == Obj::HERO && obj->tempOwner == playerID)
+	{
+		const CGHeroInstance * h = static_cast<const CGHeroInstance *>(obj);
+		heroKilled(h);
+	}
+	GH.fakeMouseMove();
+}
+
+void CPlayerInterface::objectRemovedAfter()
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	adventureInt->onMapTilesChanged(boost::none);
+
+	// visiting or garrisoned hero removed - update window
+	if (castleInt)
+		castleInt->updateGarrisons();
+
+	for (auto ki : GH.windows().findWindows<CKingdomInterface>())
+		ki->heroRemoved();
+}
+
+void CPlayerInterface::playerBlocked(int reason, bool start)
+{
+	if(reason == PlayerBlocked::EReason::UPCOMING_BATTLE)
+	{
+		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)
+			LOCPLINT = this;
+			GH.curInt = this;
+			adventureInt->onCurrentPlayerChanged(playerID);
+			std::string msg = CGI->generaltexth->translate("vcmi.adventureMap.playerAttacked");
+			boost::replace_first(msg, "%s", cb->getStartInfo()->playerInfos.find(playerID)->second.name);
+			std::vector<std::shared_ptr<CComponent>> cmp;
+			cmp.push_back(std::make_shared<CComponent>(CComponent::flag, playerID.getNum(), 0));
+			makingTurn = true; //workaround for stiff showInfoDialog implementation
+			showInfoDialog(msg, cmp);
+			makingTurn = false;
+		}
+	}
+}
+
+void CPlayerInterface::update()
+{
+	// Make sure that gamestate won't change when GUI objects may obtain its parts on event processing or drawing request
+	boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
+
+	// While mutexes were locked away we may be have stopped being the active interface
+	if (LOCPLINT != this)
+		return;
+
+	//if there are any waiting dialogs, show them
+	if ((CSH->howManyPlayerInterfaces() <= 1 || makingTurn) && !dialogs.empty() && !showingDialog->get())
+	{
+		showingDialog->set(true);
+		GH.windows().pushWindow(dialogs.front());
+		dialogs.pop_front();
+	}
+
+	assert(adventureInt);
+
+	// Handles mouse and key input
+	GH.handleEvents();
+	GH.windows().simpleRedraw();
+}
+
+int CPlayerInterface::getLastIndex( std::string namePrefix)
+{
+	using namespace boost::filesystem;
+	using namespace boost::algorithm;
+
+	path gamesDir = VCMIDirs::get().userSavePath();
+	std::map<std::time_t, int> dates; //save number => datestamp
+
+	const directory_iterator enddir;
+	if (!exists(gamesDir))
+		create_directory(gamesDir);
+	else
+	for (directory_iterator dir(gamesDir); dir != enddir; ++dir)
+	{
+		if (is_regular_file(dir->status()))
+		{
+			std::string name = dir->path().filename().string();
+			if (starts_with(name, namePrefix) && ends_with(name, ".vcgm1"))
+			{
+				char nr = name[namePrefix.size()];
+				if (std::isdigit(nr))
+					dates[last_write_time(dir->path())] = boost::lexical_cast<int>(nr);
+			}
+		}
+	}
+
+	if (!dates.empty())
+		return (--dates.end())->second; //return latest file number
+	return 0;
+}
+
+void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult )
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+
+	if (player == playerID)
+	{
+		if (victoryLossCheckResult.loss())
+			showInfoDialog(CGI->generaltexth->allTexts[95]);
+
+		assert(GH.curInt == LOCPLINT);
+		auto previousInterface = LOCPLINT; //without multiple player interfaces some of lines below are useless, but for hotseat we wanna swap player interface temporarily
+
+		LOCPLINT = this; //this is needed for dialog to show and avoid freeze, dialog showing logic should be reworked someday
+		GH.curInt = this; //waiting for dialogs requires this to get events
+
+		if(!makingTurn)
+		{
+			makingTurn = true; //also needed for dialog to show with current implementation
+			waitForAllDialogs();
+			makingTurn = false;
+		}
+		else
+			waitForAllDialogs();
+
+		GH.curInt = previousInterface;
+		LOCPLINT = previousInterface;
+
+		if(CSH->howManyPlayerInterfaces() == 1 && !settings["session"]["spectate"].Bool()) //all human players eliminated
+		{
+			if(adventureInt)
+			{
+				GH.windows().popWindows(GH.windows().count());
+				adventureInt.reset();
+			}
+		}
+
+		if (victoryLossCheckResult.victory() && LOCPLINT == this)
+		{
+			// end game if current human player has won
+			CSH->sendClientDisconnecting();
+			requestReturningToMainMenu(true);
+		}
+		else if(CSH->howManyPlayerInterfaces() == 1 && !settings["session"]["spectate"].Bool())
+		{
+			//all human players eliminated
+			CSH->sendClientDisconnecting();
+			requestReturningToMainMenu(false);
+		}
+
+		if (GH.curInt == this)
+			GH.curInt = nullptr;
+	}
+}
+
+void CPlayerInterface::playerBonusChanged( const Bonus &bonus, bool gain )
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+}
+
+void CPlayerInterface::showPuzzleMap()
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	waitWhileDialog();
+
+	//TODO: interface should not know the real position of Grail...
+	double ratio = 0;
+	int3 grailPos = cb->getGrailPos(&ratio);
+
+	GH.windows().createAndPushWindow<CPuzzleWindow>(grailPos, ratio);
+}
+
+void CPlayerInterface::viewWorldMap()
+{
+	adventureInt->openWorldView();
+}
+
+void CPlayerInterface::advmapSpellCast(const CGHeroInstance * caster, SpellID spellID)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+
+	if(GH.windows().topWindow<CSpellWindow>())
+		GH.windows().popWindows(1);
+
+	if(spellID == SpellID::FLY || spellID == SpellID::WATER_WALK)
+		localState->erasePath(caster);
+
+	auto castSoundPath = spellID.toSpell()->getCastSound();
+	if(!castSoundPath.empty())
+		CCS->soundh->playSound(castSoundPath);
+}
+
+void CPlayerInterface::tryDigging(const CGHeroInstance * h)
+{
+	int msgToShow = -1;
+
+	const auto diggingStatus = h->diggingStatus();
+
+	switch(diggingStatus)
+	{
+	case EDiggingStatus::CAN_DIG:
+		break;
+	case EDiggingStatus::LACK_OF_MOVEMENT:
+		msgToShow = 56; //"Digging for artifacts requires a whole day, try again tomorrow."
+		break;
+	case EDiggingStatus::TILE_OCCUPIED:
+		msgToShow = 97; //Try searching on clear ground.
+		break;
+	case EDiggingStatus::WRONG_TERRAIN:
+		msgToShow = 60; ////Try looking on land!
+		break;
+	case EDiggingStatus::BACKPACK_IS_FULL:
+		msgToShow = 247; //Searching for the Grail is fruitless...
+		break;
+	default:
+		assert(0);
+	}
+
+	if(msgToShow < 0)
+		cb->dig(h);
+	else
+		showInfoDialog(CGI->generaltexth->allTexts[msgToShow]);
+}
+
+void CPlayerInterface::battleNewRoundFirst(const BattleID & battleID)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	BATTLE_EVENT_POSSIBLE_RETURN;
+
+	battleInt->newRoundFirst();
+}
+
+void CPlayerInterface::showMarketWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	auto onWindowClosed = [this, queryID](){
+		cb->selectionMade(0, queryID);
+	};
+
+	if(market->allowsTrade(EMarketMode::ARTIFACT_EXP) && visitor->getAlignment() != EAlignment::EVIL)
+		GH.windows().createAndPushWindow<CAltarWindow>(market, visitor, onWindowClosed, EMarketMode::ARTIFACT_EXP);
+	else if(market->allowsTrade(EMarketMode::CREATURE_EXP) && visitor->getAlignment() != EAlignment::GOOD)
+		GH.windows().createAndPushWindow<CAltarWindow>(market, visitor, onWindowClosed, EMarketMode::CREATURE_EXP);
+	else if(market->allowsTrade(EMarketMode::CREATURE_UNDEAD))
+		GH.windows().createAndPushWindow<CTransformerWindow>(market, visitor, onWindowClosed);
+	else if(!market->availableModes().empty())
+		GH.windows().createAndPushWindow<CMarketplaceWindow>(market, visitor, onWindowClosed, market->availableModes().front());
+}
+
+void CPlayerInterface::showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	auto onWindowClosed = [this, queryID](){
+		cb->selectionMade(0, queryID);
+	};
+	GH.windows().createAndPushWindow<CUniversityWindow>(visitor, market, onWindowClosed);
+}
+
+void CPlayerInterface::showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	GH.windows().createAndPushWindow<CHillFortWindow>(visitor, object);
+}
+
+void CPlayerInterface::availableArtifactsChanged(const CGBlackMarket * bm)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	for (auto cmw : GH.windows().findWindows<CMarketplaceWindow>())
+		cmw->artifactsChanged(false);
+}
+
+void CPlayerInterface::showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	auto onWindowClosed = [this, queryID](){
+		cb->selectionMade(0, queryID);
+	};
+	GH.windows().createAndPushWindow<CTavernWindow>(object, onWindowClosed);
+}
+
+void CPlayerInterface::showThievesGuildWindow (const CGObjectInstance * obj)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	GH.windows().createAndPushWindow<CThievesGuildWindow>(obj);
+}
+
+void CPlayerInterface::showQuestLog()
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	GH.windows().createAndPushWindow<CQuestLog>(LOCPLINT->cb->getMyQuests());
+}
+
+void CPlayerInterface::showShipyardDialogOrProblemPopup(const IShipyard *obj)
+{
+	if (obj->shipyardStatus() != IBoatGenerator::GOOD)
+	{
+		MetaString txt;
+		obj->getProblemText(txt);
+		showInfoDialog(txt.toString());
+	}
+	else
+		showShipyardDialog(obj);
+}
+
+void CPlayerInterface::requestReturningToMainMenu(bool won)
+{
+	HighScoreParameter param;
+	param.difficulty = cb->getStartInfo()->difficulty;
+	param.day = cb->getDate();
+	param.townAmount = cb->howManyTowns();
+	param.usedCheat = cb->getPlayerState(*cb->getPlayerID())->cheated;
+	param.hasGrail = false;
+	for(const CGHeroInstance * h : cb->getHeroesInfo())
+		if(h->hasArt(ArtifactID::GRAIL))
+			param.hasGrail = true;
+	for(const CGTownInstance * t : cb->getTownsInfo())
+		if(t->builtBuildings.count(BuildingID::GRAIL))
+			param.hasGrail = true;
+	param.allDefeated = true;
+	for (PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player)
+	{
+		auto ps = cb->getPlayerState(player, false);
+		if(ps && player != *cb->getPlayerID())
+			if(!ps->checkVanquished())
+				param.allDefeated = false;
+	}
+	param.scenarioName = cb->getMapHeader()->name.toString();
+	param.playerName = cb->getStartInfo()->playerInfos.find(*cb->getPlayerID())->second.name;
+	HighScoreCalculation highScoreCalc;
+	highScoreCalc.parameters.push_back(param);
+	highScoreCalc.isCampaign = false;
+
+	if(won && cb->getStartInfo()->campState)
+		CSH->startCampaignScenario(param, cb->getStartInfo()->campState);
+	else
+	{
+		GH.dispatchMainThread(
+			[won, highScoreCalc]()
+			{
+				CSH->endGameplay();
+				GH.defActionsDef = 63;
+				CMM->menu->switchToTab("main");
+				GH.windows().createAndPushWindow<CHighScoreInputScreen>(won, highScoreCalc);
+			}
+		);
+	}
+}
+
+void CPlayerInterface::askToAssembleArtifact(const ArtifactLocation &al)
+{
+	auto hero = std::visit(HeroObjectRetriever(), al.artHolder);
+	if(hero)
+	{
+		auto art = hero->getArt(al.slot);
+		if(art == nullptr)
+		{
+			logGlobal->error("artifact location %d points to nothing",
+							 al.slot.num);
+			return;
+		}
+		ArtifactUtilsClient::askToAssemble(hero, al.slot);
+	}
+}
+
+void CPlayerInterface::artifactPut(const ArtifactLocation &al)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	auto hero = std::visit(HeroObjectRetriever(), al.artHolder);
+	adventureInt->onHeroChanged(hero);
+}
+
+void CPlayerInterface::artifactRemoved(const ArtifactLocation &al)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	auto hero = std::visit(HeroObjectRetriever(), al.artHolder);
+	adventureInt->onHeroChanged(hero);
+
+	for(auto artWin : GH.windows().findWindows<CArtifactHolder>())
+		artWin->artifactRemoved(al);
+
+	waitWhileDialog();
+}
+
+void CPlayerInterface::artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	auto hero = std::visit(HeroObjectRetriever(), dst.artHolder);
+	adventureInt->onHeroChanged(hero);
+
+	bool redraw = true;
+	// If a bulk transfer has arrived, then redrawing only the last art movement.
+	if(numOfMovedArts != 0)
+	{
+		numOfMovedArts--;
+		if(numOfMovedArts != 0)
+			redraw = false;
+	}
+
+	for(auto artWin : GH.windows().findWindows<CArtifactHolder>())
+		artWin->artifactMoved(src, dst, redraw);
+
+	waitWhileDialog();
+}
+
+void CPlayerInterface::bulkArtMovementStart(size_t numOfArts)
+{
+	numOfMovedArts = numOfArts;
+}
+
+void CPlayerInterface::artifactAssembled(const ArtifactLocation &al)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	auto hero = std::visit(HeroObjectRetriever(), al.artHolder);
+	adventureInt->onHeroChanged(hero);
+
+	for(auto artWin : GH.windows().findWindows<CArtifactHolder>())
+		artWin->artifactAssembled(al);
+}
+
+void CPlayerInterface::artifactDisassembled(const ArtifactLocation &al)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	auto hero = std::visit(HeroObjectRetriever(), al.artHolder);
+	adventureInt->onHeroChanged(hero);
+
+	for(auto artWin : GH.windows().findWindows<CArtifactHolder>())
+		artWin->artifactDisassembled(al);
+}
+
+void CPlayerInterface::waitForAllDialogs()
+{
+	while(!dialogs.empty())
+	{
+		auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex);
+		boost::this_thread::sleep_for(boost::chrono::milliseconds(5));
+	}
+	waitWhileDialog();
+}
+
+void CPlayerInterface::proposeLoadingGame()
+{
+	showYesNoDialog(
+		CGI->generaltexth->allTexts[68],
+		[]()
+		{
+			GH.dispatchMainThread(
+				[]()
+				{
+					CSH->endGameplay();
+					GH.defActionsDef = 63;
+					CMM->menu->switchToTab("load");
+				}
+			);
+		},
+		nullptr
+	);
+}
+
+bool CPlayerInterface::capturedAllEvents()
+{
+	if(movementController->isHeroMoving())
+	{
+		//just inform that we are capturing events. they will be processed by heroMoved() in client thread.
+		return true;
+	}
+
+	bool needToLockAdventureMap = adventureInt && adventureInt->isActive() && CGI->mh->hasOngoingAnimations();
+	bool quickCombatOngoing = isAutoFightOn && !battleInt;
+
+	if (ignoreEvents || needToLockAdventureMap || quickCombatOngoing )
+	{
+		GH.input().ignoreEventsUntilInput();
+		return true;
+	}
+
+	return false;
+}
+
+void CPlayerInterface::showWorldViewEx(const std::vector<ObjectPosInfo>& objectPositions, bool showTerrain)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	adventureInt->openWorldView(objectPositions, showTerrain );
+}
+
+std::optional<BattleAction> CPlayerInterface::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState)
+{
+	return std::nullopt;
+}

+ 234 - 234
client/CPlayerInterface.h

@@ -1,234 +1,234 @@
-/*
- * CPlayerInterface.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/FunctionList.h"
-#include "../lib/CGameInterface.h"
-#include "gui/CIntObject.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-class Artifact;
-
-struct TryMoveHero;
-class CGHeroInstance;
-class CStack;
-class CCreature;
-struct CGPath;
-class CCreatureSet;
-class CGObjectInstance;
-struct UpgradeInfo;
-template <typename T> struct CondSh;
-struct CPathsInfo;
-
-VCMI_LIB_NAMESPACE_END
-
-class CButton;
-class AdventureMapInterface;
-class CCastleInterface;
-class BattleInterface;
-class CComponent;
-class CSelectableComponent;
-class CSlider;
-class CInGameConsole;
-class CInfoWindow;
-class IShowActivatable;
-class ClickableL;
-class ClickableR;
-class Hoverable;
-class KeyInterested;
-class MotionInterested;
-class PlayerLocalState;
-class TimeInterested;
-class HeroMovementController;
-
-namespace boost
-{
-	class mutex;
-	class recursive_mutex;
-}
-
-/// Central class for managing user interface logic
-class CPlayerInterface : public CGameInterface, public IUpdateable
-{
-	bool ignoreEvents;
-	size_t numOfMovedArts;
-
-	// -1 - just loaded game; 1 - just started game; 0 otherwise
-	int firstCall;
-	int autosaveCount;
-
-	std::list<std::shared_ptr<CInfoWindow>> dialogs; //queue of dialogs awaiting to be shown (not currently shown!)
-
-	std::unique_ptr<HeroMovementController> movementController;
-public: // TODO: make private
-	std::shared_ptr<Environment> env;
-
-	std::unique_ptr<PlayerLocalState> localState;
-
-	//minor interfaces
-	CondSh<bool> *showingDialog; //indicates if dialog box is displayed
-
-	bool makingTurn; //if player is already making his turn
-
-	CCastleInterface * castleInt; //nullptr if castle window isn't opened
-	static std::shared_ptr<BattleInterface> battleInt; //nullptr if no battle
-	CInGameConsole * cingconsole;
-
-	std::shared_ptr<CCallback> cb; //to communicate with engine
-
-	//During battle is quick combat mode is used
-	std::shared_ptr<CBattleGameInterface> autofightingAI; //AI that makes decisions
-	bool isAutoFightOn; //Flag, switch it to stop quick combat. Don't touch if there is no battle interface.
-
-protected: // Call-ins from server, should not be called directly, but only via GameInterface
-
-	void update() override;
-	void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
-
-	void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override;
-	void buildChanged(const CGTownInstance *town, BuildingID buildingID, int what) override; //what: 1 - built, 2 - demolished
-
-	void artifactPut(const ArtifactLocation &al) override;
-	void artifactRemoved(const ArtifactLocation &al) override;
-	void artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst) override;
-	void bulkArtMovementStart(size_t numOfArts) override;
-	void artifactAssembled(const ArtifactLocation &al) override;
-	void askToAssembleArtifact(const ArtifactLocation & dst) override;
-	void artifactDisassembled(const ArtifactLocation &al) override;
-
-	void heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) override;
-	void heroCreated(const CGHeroInstance* hero) override;
-	void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector<SecondarySkill> &skills, QueryID queryID) override;
-	void commanderGotLevel (const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID) override;
-	void heroInGarrisonChange(const CGTownInstance *town) override;
-	void heroMoved(const TryMoveHero & details, bool verbose = true) override;
-	void heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) override;
-	void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override;
-	void heroManaPointsChanged(const CGHeroInstance * hero) override;
-	void heroMovePointsChanged(const CGHeroInstance * hero) override;
-	void heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town) override;
-	void receivedResource() override;
-	void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector<Component> & components, int soundID) override;
-	void showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level, QueryID queryID) override;
-	void showBlockingDialog(const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID.
-	void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override;
-	void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override;
-	void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects) override;
-	void showMarketWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) override;
-	void showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) override;
-	void showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor) override;
-	void advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) override; //called when a hero casts a spell
-	void tileHidden(const std::unordered_set<int3> &pos) override; //called when given tiles become hidden under fog of war
-	void tileRevealed(const std::unordered_set<int3> &pos) override; //called when fog of war disappears from given tiles
-	void newObject(const CGObjectInstance * obj) override;
-	void availableArtifactsChanged(const CGBlackMarket *bm = nullptr) override; //bm may be nullptr, then artifacts are changed in the global pool (used by merchants in towns)
-	void yourTurn(QueryID queryID) override;
-	void availableCreaturesChanged(const CGDwelling *town) override;
-	void heroBonusChanged(const CGHeroInstance *hero, const Bonus &bonus, bool gain) override;//if gain hero received bonus, else he lost it
-	void playerBonusChanged(const Bonus &bonus, bool gain) override;
-	void requestRealized(PackageApplied *pa) override;
-	void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override;
-	void centerView (int3 pos, int focusTime) override;
-	void beforeObjectPropertyChanged(const SetObjectProperty * sop) override;
-	void objectPropertyChanged(const SetObjectProperty * sop) override;
-	void objectRemoved(const CGObjectInstance *obj, const PlayerColor & initiator) override;
-	void objectRemovedAfter() override;
-	void playerBlocked(int reason, bool start) override;
-	void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) override;
-	void playerStartsTurn(PlayerColor player) override; //called before yourTurn on active itnerface
-	void playerEndsTurn(PlayerColor player) override;
-	void saveGame(BinarySerializer & h, const int version) override; //saving
-	void loadGame(BinaryDeserializer & h, const int version) override; //loading
-	void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
-
-	//for battles
-	void actionFinished(const BattleID & battleID, const BattleAction& action) override;//occurs AFTER action taken by active stack or by the hero
-	void actionStarted(const BattleID & battleID, const BattleAction& action) override;//occurs BEFORE action taken by active stack or by the hero
-	void activeStack(const BattleID & battleID, const CStack * stack) override; //called when it's turn of that stack
-	void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; //stack performs attack
-	void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override; //end of battle
-	void battleNewRoundFirst(const BattleID & battleID) override; //called at the beginning of each turn before changes are applied; used for HP regen handling
-	void battleNewRound(const BattleID & battleID) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn
-	void battleLogMessage(const BattleID & battleID, const std::vector<MetaString> & lines) override;
-	void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport) override;
-	void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override;
-	void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override; //called when a specific effect is set to stacks
-	void battleTriggerEffect(const BattleID & battleID, const BattleTriggerEffect & bte) override; //various one-shot effect
-	void battleStacksAttacked(const BattleID & battleID, const std::vector<BattleStackAttacked> & bsa, bool ranged) override;
-	void battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) override; //called by engine just before battle starts; side=0 - left, side=1 - right
-	void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right
-	void battleUnitsChanged(const BattleID & battleID, const std::vector<UnitChanges> & units) override;
-	void battleObstaclesChanged(const BattleID & battleID, const std::vector<ObstacleChanges> & obstacles) override;
-	void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack
-	void battleGateStateChanged(const BattleID & battleID, const EGateState state) override;
-	void yourTacticPhase(const BattleID & battleID, int distance) override;
-	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override;
-
-public: // public interface for use by client via LOCPLINT access
-
-	// part of GameInterface that is also used by client code
-	void showPuzzleMap() override;
-	void viewWorldMap() override;
-	void showQuestLog() override;
-	void showThievesGuildWindow (const CGObjectInstance * obj) override;
-	void showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) override;
-	void showShipyardDialog(const IShipyard *obj) override; //obj may be town or shipyard;
-
-	void showHeroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2);
-	void showArtifactAssemblyDialog(const Artifact * artifact, const Artifact * assembledArtifact, CFunctionList<void()> onYes);
-	void waitWhileDialog();
-	void waitForAllDialogs();
-	void openTownWindow(const CGTownInstance * town); //shows townscreen
-	void openHeroWindow(const CGHeroInstance * hero); //shows hero window with given hero
-
-	void showInfoDialog(const std::string &text, std::shared_ptr<CComponent> component);
-	void showInfoDialog(const std::string &text, const std::vector<std::shared_ptr<CComponent>> & components = std::vector<std::shared_ptr<CComponent>>(), int soundID = 0);
-	void showInfoDialogAndWait(std::vector<Component> & components, const MetaString & text);
-	void showYesNoDialog(const std::string &text, CFunctionList<void()> onYes, CFunctionList<void()> onNo, const std::vector<std::shared_ptr<CComponent>> & components = std::vector<std::shared_ptr<CComponent>>());
-
-	void moveHero(const CGHeroInstance *h, const CGPath& path);
-
-	void tryDigging(const CGHeroInstance *h);
-	void showShipyardDialogOrProblemPopup(const IShipyard *obj); //obj may be town or shipyard;
-	void proposeLoadingGame();
-	void performAutosave();
-	void gamePause(bool pause);
-
-	///returns true if all events are processed internally
-	bool capturedAllEvents();
-
-	CPlayerInterface(PlayerColor Player);
-	~CPlayerInterface();
-
-private:
-	struct IgnoreEvents
-	{
-		CPlayerInterface & owner;
-		IgnoreEvents(CPlayerInterface & Owner):owner(Owner)
-		{
-			owner.ignoreEvents = true;
-		};
-		~IgnoreEvents()
-		{
-			owner.ignoreEvents = false;
-		};
-	};
-
-	void heroKilled(const CGHeroInstance* hero);
-	void garrisonsChanged(std::vector<const CGObjectInstance *> objs);
-	void requestReturningToMainMenu(bool won);
-	void acceptTurn(QueryID queryID); //used during hot seat after your turn message is close
-	void initializeHeroTownList();
-	int getLastIndex(std::string namePrefix);
-};
-
-/// Provides global access to instance of interface of currently active player
-extern CPlayerInterface * LOCPLINT;
+/*
+ * CPlayerInterface.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/FunctionList.h"
+#include "../lib/CGameInterface.h"
+#include "gui/CIntObject.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class Artifact;
+
+struct TryMoveHero;
+class CGHeroInstance;
+class CStack;
+class CCreature;
+struct CGPath;
+class CCreatureSet;
+class CGObjectInstance;
+struct UpgradeInfo;
+template <typename T> struct CondSh;
+struct CPathsInfo;
+
+VCMI_LIB_NAMESPACE_END
+
+class CButton;
+class AdventureMapInterface;
+class CCastleInterface;
+class BattleInterface;
+class CComponent;
+class CSelectableComponent;
+class CSlider;
+class CInGameConsole;
+class CInfoWindow;
+class IShowActivatable;
+class ClickableL;
+class ClickableR;
+class Hoverable;
+class KeyInterested;
+class MotionInterested;
+class PlayerLocalState;
+class TimeInterested;
+class HeroMovementController;
+
+namespace boost
+{
+	class mutex;
+	class recursive_mutex;
+}
+
+/// Central class for managing user interface logic
+class CPlayerInterface : public CGameInterface, public IUpdateable
+{
+	bool ignoreEvents;
+	size_t numOfMovedArts;
+
+	// -1 - just loaded game; 1 - just started game; 0 otherwise
+	int firstCall;
+	int autosaveCount;
+
+	std::list<std::shared_ptr<CInfoWindow>> dialogs; //queue of dialogs awaiting to be shown (not currently shown!)
+
+	std::unique_ptr<HeroMovementController> movementController;
+public: // TODO: make private
+	std::shared_ptr<Environment> env;
+
+	std::unique_ptr<PlayerLocalState> localState;
+
+	//minor interfaces
+	CondSh<bool> *showingDialog; //indicates if dialog box is displayed
+
+	bool makingTurn; //if player is already making his turn
+
+	CCastleInterface * castleInt; //nullptr if castle window isn't opened
+	static std::shared_ptr<BattleInterface> battleInt; //nullptr if no battle
+	CInGameConsole * cingconsole;
+
+	std::shared_ptr<CCallback> cb; //to communicate with engine
+
+	//During battle is quick combat mode is used
+	std::shared_ptr<CBattleGameInterface> autofightingAI; //AI that makes decisions
+	bool isAutoFightOn; //Flag, switch it to stop quick combat. Don't touch if there is no battle interface.
+
+protected: // Call-ins from server, should not be called directly, but only via GameInterface
+
+	void update() override;
+	void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
+
+	void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override;
+	void buildChanged(const CGTownInstance *town, BuildingID buildingID, int what) override; //what: 1 - built, 2 - demolished
+
+	void artifactPut(const ArtifactLocation &al) override;
+	void artifactRemoved(const ArtifactLocation &al) override;
+	void artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst) override;
+	void bulkArtMovementStart(size_t numOfArts) override;
+	void artifactAssembled(const ArtifactLocation &al) override;
+	void askToAssembleArtifact(const ArtifactLocation & dst) override;
+	void artifactDisassembled(const ArtifactLocation &al) override;
+
+	void heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) override;
+	void heroCreated(const CGHeroInstance* hero) override;
+	void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector<SecondarySkill> &skills, QueryID queryID) override;
+	void commanderGotLevel (const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID) override;
+	void heroInGarrisonChange(const CGTownInstance *town) override;
+	void heroMoved(const TryMoveHero & details, bool verbose = true) override;
+	void heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) override;
+	void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override;
+	void heroManaPointsChanged(const CGHeroInstance * hero) override;
+	void heroMovePointsChanged(const CGHeroInstance * hero) override;
+	void heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town) override;
+	void receivedResource() override;
+	void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector<Component> & components, int soundID) override;
+	void showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level, QueryID queryID) override;
+	void showBlockingDialog(const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID.
+	void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override;
+	void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override;
+	void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects) override;
+	void showMarketWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) override;
+	void showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) override;
+	void showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor) override;
+	void advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) override; //called when a hero casts a spell
+	void tileHidden(const std::unordered_set<int3> &pos) override; //called when given tiles become hidden under fog of war
+	void tileRevealed(const std::unordered_set<int3> &pos) override; //called when fog of war disappears from given tiles
+	void newObject(const CGObjectInstance * obj) override;
+	void availableArtifactsChanged(const CGBlackMarket *bm = nullptr) override; //bm may be nullptr, then artifacts are changed in the global pool (used by merchants in towns)
+	void yourTurn(QueryID queryID) override;
+	void availableCreaturesChanged(const CGDwelling *town) override;
+	void heroBonusChanged(const CGHeroInstance *hero, const Bonus &bonus, bool gain) override;//if gain hero received bonus, else he lost it
+	void playerBonusChanged(const Bonus &bonus, bool gain) override;
+	void requestRealized(PackageApplied *pa) override;
+	void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override;
+	void centerView (int3 pos, int focusTime) override;
+	void beforeObjectPropertyChanged(const SetObjectProperty * sop) override;
+	void objectPropertyChanged(const SetObjectProperty * sop) override;
+	void objectRemoved(const CGObjectInstance *obj, const PlayerColor & initiator) override;
+	void objectRemovedAfter() override;
+	void playerBlocked(int reason, bool start) override;
+	void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) override;
+	void playerStartsTurn(PlayerColor player) override; //called before yourTurn on active itnerface
+	void playerEndsTurn(PlayerColor player) override;
+	void saveGame(BinarySerializer & h, const int version) override; //saving
+	void loadGame(BinaryDeserializer & h, const int version) override; //loading
+	void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
+
+	//for battles
+	void actionFinished(const BattleID & battleID, const BattleAction& action) override;//occurs AFTER action taken by active stack or by the hero
+	void actionStarted(const BattleID & battleID, const BattleAction& action) override;//occurs BEFORE action taken by active stack or by the hero
+	void activeStack(const BattleID & battleID, const CStack * stack) override; //called when it's turn of that stack
+	void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; //stack performs attack
+	void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override; //end of battle
+	void battleNewRoundFirst(const BattleID & battleID) override; //called at the beginning of each turn before changes are applied; used for HP regen handling
+	void battleNewRound(const BattleID & battleID) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn
+	void battleLogMessage(const BattleID & battleID, const std::vector<MetaString> & lines) override;
+	void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport) override;
+	void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override;
+	void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override; //called when a specific effect is set to stacks
+	void battleTriggerEffect(const BattleID & battleID, const BattleTriggerEffect & bte) override; //various one-shot effect
+	void battleStacksAttacked(const BattleID & battleID, const std::vector<BattleStackAttacked> & bsa, bool ranged) override;
+	void battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) override; //called by engine just before battle starts; side=0 - left, side=1 - right
+	void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right
+	void battleUnitsChanged(const BattleID & battleID, const std::vector<UnitChanges> & units) override;
+	void battleObstaclesChanged(const BattleID & battleID, const std::vector<ObstacleChanges> & obstacles) override;
+	void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack
+	void battleGateStateChanged(const BattleID & battleID, const EGateState state) override;
+	void yourTacticPhase(const BattleID & battleID, int distance) override;
+	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override;
+
+public: // public interface for use by client via LOCPLINT access
+
+	// part of GameInterface that is also used by client code
+	void showPuzzleMap() override;
+	void viewWorldMap() override;
+	void showQuestLog() override;
+	void showThievesGuildWindow (const CGObjectInstance * obj) override;
+	void showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) override;
+	void showShipyardDialog(const IShipyard *obj) override; //obj may be town or shipyard;
+
+	void showHeroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2);
+	void showArtifactAssemblyDialog(const Artifact * artifact, const Artifact * assembledArtifact, CFunctionList<void()> onYes);
+	void waitWhileDialog();
+	void waitForAllDialogs();
+	void openTownWindow(const CGTownInstance * town); //shows townscreen
+	void openHeroWindow(const CGHeroInstance * hero); //shows hero window with given hero
+
+	void showInfoDialog(const std::string &text, std::shared_ptr<CComponent> component);
+	void showInfoDialog(const std::string &text, const std::vector<std::shared_ptr<CComponent>> & components = std::vector<std::shared_ptr<CComponent>>(), int soundID = 0);
+	void showInfoDialogAndWait(std::vector<Component> & components, const MetaString & text);
+	void showYesNoDialog(const std::string &text, CFunctionList<void()> onYes, CFunctionList<void()> onNo, const std::vector<std::shared_ptr<CComponent>> & components = std::vector<std::shared_ptr<CComponent>>());
+
+	void moveHero(const CGHeroInstance *h, const CGPath& path);
+
+	void tryDigging(const CGHeroInstance *h);
+	void showShipyardDialogOrProblemPopup(const IShipyard *obj); //obj may be town or shipyard;
+	void proposeLoadingGame();
+	void performAutosave();
+	void gamePause(bool pause);
+
+	///returns true if all events are processed internally
+	bool capturedAllEvents();
+
+	CPlayerInterface(PlayerColor Player);
+	~CPlayerInterface();
+
+private:
+	struct IgnoreEvents
+	{
+		CPlayerInterface & owner;
+		IgnoreEvents(CPlayerInterface & Owner):owner(Owner)
+		{
+			owner.ignoreEvents = true;
+		};
+		~IgnoreEvents()
+		{
+			owner.ignoreEvents = false;
+		};
+	};
+
+	void heroKilled(const CGHeroInstance* hero);
+	void garrisonsChanged(std::vector<const CGObjectInstance *> objs);
+	void requestReturningToMainMenu(bool won);
+	void acceptTurn(QueryID queryID); //used during hot seat after your turn message is close
+	void initializeHeroTownList();
+	int getLastIndex(std::string namePrefix);
+};
+
+/// Provides global access to instance of interface of currently active player
+extern CPlayerInterface * LOCPLINT;

+ 665 - 665
client/CVideoHandler.cpp

@@ -1,665 +1,665 @@
-/*
- * CVideoHandler.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 "CVideoHandler.h"
-
-#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 "../lib/filesystem/CInputStream.h"
-
-#include <SDL_render.h>
-
-#ifndef DISABLE_VIDEO
-
-extern "C" {
-#include <libavformat/avformat.h>
-#include <libavcodec/avcodec.h>
-#include <libavutil/imgutils.h>
-#include <libswscale/swscale.h>
-}
-
-#ifdef _MSC_VER
-#pragma comment(lib, "avcodec.lib")
-#pragma comment(lib, "avutil.lib")
-#pragma comment(lib, "avformat.lib")
-#pragma comment(lib, "swscale.lib")
-#endif // _MSC_VER
-
-// Define a set of functions to read data
-static int lodRead(void* opaque, uint8_t* buf, int size)
-{
-	auto video = reinterpret_cast<CVideoPlayer *>(opaque);
-
-	return static_cast<int>(video->data->read(buf, size));
-}
-
-static si64 lodSeek(void * opaque, si64 pos, int whence)
-{
-	auto video = reinterpret_cast<CVideoPlayer *>(opaque);
-
-	if (whence & AVSEEK_SIZE)
-		return video->data->getSize();
-
-	return video->data->seek(pos);
-}
-
-// Define a set of functions to read data
-static int lodReadAudio(void* opaque, uint8_t* buf, int size)
-{
-	auto video = reinterpret_cast<CVideoPlayer *>(opaque);
-
-	return static_cast<int>(video->dataAudio->read(buf, size));
-}
-
-static si64 lodSeekAudio(void * opaque, si64 pos, int whence)
-{
-	auto video = reinterpret_cast<CVideoPlayer *>(opaque);
-
-	if (whence & AVSEEK_SIZE)
-		return video->dataAudio->getSize();
-
-	return video->dataAudio->seek(pos);
-}
-
-CVideoPlayer::CVideoPlayer()
-	: stream(-1)
-	, format (nullptr)
-	, codecContext(nullptr)
-	, codec(nullptr)
-	, frame(nullptr)
-	, sws(nullptr)
-	, context(nullptr)
-	, texture(nullptr)
-	, dest(nullptr)
-	, destRect(0,0,0,0)
-	, pos(0,0,0,0)
-	, frameTime(0)
-	, doLoop(false)
-{}
-
-bool CVideoPlayer::open(const VideoPath & fname, bool scale)
-{
-	return open(fname, true, false);
-}
-
-// loop = to loop through the video
-// useOverlay = directly write to the screen.
-bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverlay, bool scale)
-{
-	close();
-
-	doLoop = loop;
-	frameTime = 0;
-
-	if (CResourceHandler::get()->existsResource(videoToOpen))
-		fname = videoToOpen;
-	else
-		fname = videoToOpen.addPrefix("VIDEO/");
-
-	if (!CResourceHandler::get()->existsResource(fname))
-	{
-		logGlobal->error("Error: video %s was not found", fname.getName());
-		return false;
-	}
-
-	data = CResourceHandler::get()->load(fname);
-
-	static const int BUFFER_SIZE = 4096;
-
-	unsigned char * buffer  = (unsigned char *)av_malloc(BUFFER_SIZE);// will be freed by ffmpeg
-	context = avio_alloc_context( buffer, BUFFER_SIZE, 0, (void *)this, lodRead, nullptr, lodSeek);
-
-	format = avformat_alloc_context();
-	format->pb = context;
-	// filename is not needed - file was already open and stored in this->data;
-	int avfopen = avformat_open_input(&format, "dummyFilename", nullptr, nullptr);
-
-	if (avfopen != 0)
-	{
-		return false;
-	}
-	// Retrieve stream information
-	if (avformat_find_stream_info(format, nullptr) < 0)
-		return false;
-
-	// Find the first video stream
-	stream = -1;
-	for(ui32 i=0; i<format->nb_streams; i++)
-	{
-		if (format->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
-		{
-			stream = i;
-			break;
-		}
-	}
-
-	if (stream < 0)
-		// No video stream in that file
-		return false;
-
-	// Find the decoder for the video stream
-	codec = avcodec_find_decoder(format->streams[stream]->codecpar->codec_id);
-
-	if (codec == nullptr)
-	{
-		// Unsupported codec
-		return false;
-	}
-
-	codecContext = avcodec_alloc_context3(codec);
-	if(!codecContext)
-		return false;
-	// Get a pointer to the codec context for the video stream
-	int ret = avcodec_parameters_to_context(codecContext, format->streams[stream]->codecpar);
-	if (ret < 0)
-	{
-		//We cannot get codec from parameters
-		avcodec_free_context(&codecContext);
-		return false;
-	}
-
-	// Open codec
-	if ( avcodec_open2(codecContext, codec, nullptr) < 0 )
-	{
-		// Could not open codec
-		codec = nullptr;
-		return false;
-	}
-	// Allocate video frame
-	frame = av_frame_alloc();
-
-	//setup scaling
-	if(scale)
-	{
-		pos.w = screen->w;
-		pos.h = screen->h;
-	}
-	else
-	{
-		pos.w  = codecContext->width;
-		pos.h = codecContext->height;
-	}
-
-	// Allocate a place to put our YUV image on that screen
-	if (useOverlay)
-	{
-		texture = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STATIC, pos.w, pos.h);
-	}
-	else
-	{
-		dest = CSDL_Ext::newSurface(pos.w, pos.h);
-		destRect.x = destRect.y = 0;
-		destRect.w = pos.w;
-		destRect.h = pos.h;
-	}
-
-	if (texture == nullptr && dest == nullptr)
-		return false;
-
-	if (texture)
-	{ // Convert the image into YUV format that SDL uses
-		sws = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt,
-							 pos.w, pos.h,
-							 AV_PIX_FMT_YUV420P,
-							 SWS_BICUBIC, nullptr, nullptr, nullptr);
-	}
-	else
-	{
-		AVPixelFormat screenFormat = AV_PIX_FMT_NONE;
-		if (screen->format->Bshift > screen->format->Rshift)
-		{
-			// this a BGR surface
-			switch (screen->format->BytesPerPixel)
-			{
-				case 2: screenFormat = AV_PIX_FMT_BGR565; break;
-				case 3: screenFormat = AV_PIX_FMT_BGR24; break;
-				case 4: screenFormat = AV_PIX_FMT_BGR32; break;
-				default: return false;
-			}
-		}
-		else
-		{
-			// this a RGB surface
-			switch (screen->format->BytesPerPixel)
-			{
-				case 2: screenFormat = AV_PIX_FMT_RGB565; break;
-				case 3: screenFormat = AV_PIX_FMT_RGB24; break;
-				case 4: screenFormat = AV_PIX_FMT_RGB32; break;
-				default: return false;
-			}
-		}
-
-		sws = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt,
-							 pos.w, pos.h, screenFormat,
-							 SWS_BICUBIC, nullptr, nullptr, nullptr);
-	}
-
-	if (sws == nullptr)
-		return false;
-
-	return true;
-}
-
-// Read the next frame. Return false on error/end of file.
-bool CVideoPlayer::nextFrame()
-{
-	AVPacket packet;
-	int frameFinished = 0;
-	bool gotError = false;
-
-	if (sws == nullptr)
-		return false;
-
-	while(!frameFinished)
-	{
-		int ret = av_read_frame(format, &packet);
-		if (ret < 0)
-		{
-			// Error. It's probably an end of file.
-			if (doLoop && !gotError)
-			{
-				// Rewind
-				frameTime = 0;
-				if (av_seek_frame(format, stream, 0, AVSEEK_FLAG_BYTE) < 0)
-					break;
-				gotError = true;
-			}
-			else
-			{
-				break;
-			}
-		}
-		else
-		{
-			// Is this a packet from the video stream?
-			if (packet.stream_index == stream)
-			{
-				// Decode video frame
-				int rc = avcodec_send_packet(codecContext, &packet);
-				if (rc >=0)
-					packet.size = 0;
-				rc = avcodec_receive_frame(codecContext, frame);
-				if (rc >= 0)
-					frameFinished = 1;
-				// Did we get a video frame?
-				if (frameFinished)
-				{
-					uint8_t *data[4];
-					int linesize[4];
-
-					if (texture) {
-						av_image_alloc(data, linesize, pos.w, pos.h, AV_PIX_FMT_YUV420P, 1);
-
-						sws_scale(sws, frame->data, frame->linesize,
-								  0, codecContext->height, data, linesize);
-
-						SDL_UpdateYUVTexture(texture, NULL, data[0], linesize[0],
-								data[1], linesize[1],
-								data[2], linesize[2]);
-						av_freep(&data[0]);
-					}
-					else
-					{
-						/* Avoid buffer overflow caused by sws_scale():
-						 *     http://trac.ffmpeg.org/ticket/9254
-						 * Currently (ffmpeg-4.4 with SSE3 enabled) sws_scale()
-						 * has a few requirements for target data buffers on rescaling:
-						 * 1. buffer has to be aligned to be usable for SIMD instructions
-						 * 2. buffer has to be padded to allow small overflow by SIMD instructions
-						 * Unfortunately SDL_Surface does not provide these guarantees.
-						 * This means that atempt to rescale directly into SDL surface causes
-						 * memory corruption. Usually it happens on campaign selection screen
-						 * where short video moves start spinning on mouse hover.
-						 *
-						 * To fix [1.] we use av_malloc() for memory allocation.
-						 * To fix [2.] we add an `ffmpeg_pad` that provides plenty of space.
-						 * We have to use intermdiate buffer and then use memcpy() to land it
-						 * to SDL_Surface.
-						 */
-						size_t pic_bytes = dest->pitch * dest->h;
-						size_t ffmped_pad = 1024; /* a few bytes of overflow will go here */
-						void * for_sws = av_malloc (pic_bytes + ffmped_pad);
-						data[0] = (ui8 *)for_sws;
-						linesize[0] = dest->pitch;
-
-						sws_scale(sws, frame->data, frame->linesize,
-								  0, codecContext->height, data, linesize);
-						memcpy(dest->pixels, for_sws, pic_bytes);
-						av_free(for_sws);
-					}
-				}
-			}
-
-			av_packet_unref(&packet);
-		}
-	}
-
-	return frameFinished != 0;
-}
-
-void CVideoPlayer::show( int x, int y, SDL_Surface *dst, bool update )
-{
-	if (sws == nullptr)
-		return;
-
-	pos.x = x;
-	pos.y = y;
-	CSDL_Ext::blitSurface(dest, destRect, dst, pos.topLeft());
-
-	if (update)
-		CSDL_Ext::updateRect(dst, pos);
-}
-
-void CVideoPlayer::redraw( int x, int y, SDL_Surface *dst, bool update )
-{
-	show(x, y, dst, update);
-}
-
-void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, bool update, std::function<void()> onVideoRestart)
-{
-	if (sws == nullptr)
-		return;
-
-#if (LIBAVUTIL_VERSION_MAJOR < 58)   
-	auto packet_duration = frame->pkt_duration;
-#else
-	auto packet_duration = frame->duration;
-#endif
-	double frameEndTime = (frame->pts + packet_duration) * av_q2d(format->streams[stream]->time_base);
-	frameTime += GH.framerate().getElapsedMilliseconds() / 1000.0;
-
-	if (frameTime >= frameEndTime )
-	{
-		if (nextFrame())
-			show(x,y,dst,update);
-		else
-		{
-			if(onVideoRestart)
-				onVideoRestart();
-			VideoPath filenameToReopen = fname; // create copy to backup this->fname
-			open(filenameToReopen);
-			nextFrame();
-
-			// The y position is wrong at the first frame.
-			// Note: either the windows player or the linux player is
-			// broken. Compensate here until the bug is found.
-			show(x, y--, dst, update);
-		}
-	}
-	else
-	{
-		redraw(x, y, dst, update);
-	}
-}
-
-void CVideoPlayer::close()
-{
-	fname = VideoPath();
-
-	if (sws)
-	{
-		sws_freeContext(sws);
-		sws = nullptr;
-	}
-
-	if (texture)
-	{
-		SDL_DestroyTexture(texture);
-		texture = nullptr;
-	}
-
-	if (dest)
-	{
-		SDL_FreeSurface(dest);
-		dest = nullptr;
-	}
-
-	if (frame)
-	{
-		av_frame_free(&frame);//will be set to null
-	}
-
-	if (codec)
-	{
-		avcodec_close(codecContext);
-		codec = nullptr;
-	}
-	if (codecContext)
-	{
-		avcodec_free_context(&codecContext);
-	}
-
-	if (format)
-	{
-		avformat_close_input(&format);
-	}
-
-	if (context)
-	{
-		av_free(context);
-		context = nullptr;
-	}
-}
-
-std::pair<std::unique_ptr<ui8 []>, si64> CVideoPlayer::getAudio(const VideoPath & videoToOpen)
-{
-	std::pair<std::unique_ptr<ui8 []>, si64> dat(std::make_pair(nullptr, 0));
-
-	VideoPath fnameAudio;
-
-	if (CResourceHandler::get()->existsResource(videoToOpen))
-		fnameAudio = videoToOpen;
-	else
-		fnameAudio = videoToOpen.addPrefix("VIDEO/");
-
-	if (!CResourceHandler::get()->existsResource(fnameAudio))
-	{
-		logGlobal->error("Error: video %s was not found", fnameAudio.getName());
-		return dat;
-	}
-
-	dataAudio = CResourceHandler::get()->load(fnameAudio);
-
-	static const int BUFFER_SIZE = 4096;
-
-	unsigned char * bufferAudio  = (unsigned char *)av_malloc(BUFFER_SIZE);// will be freed by ffmpeg
-	AVIOContext * contextAudio = avio_alloc_context( bufferAudio, BUFFER_SIZE, 0, (void *)this, lodReadAudio, nullptr, lodSeekAudio);
-
-	AVFormatContext * formatAudio = avformat_alloc_context();
-	formatAudio->pb = contextAudio;
-	// filename is not needed - file was already open and stored in this->data;
-	int avfopen = avformat_open_input(&formatAudio, "dummyFilename", nullptr, nullptr);
-
-	if (avfopen != 0)
-	{
-		return dat;
-	}
-	// Retrieve stream information
-	if (avformat_find_stream_info(formatAudio, nullptr) < 0)
-		return dat;
-
-	// Find the first audio stream
-	int streamAudio = -1;
-	for(ui32 i = 0; i < formatAudio->nb_streams; i++)
-	{
-		if (formatAudio->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
-		{
-			streamAudio = i;
-			break;
-		}
-	}
-
-	if(streamAudio < 0)
-		return dat;
-
-	const AVCodec *codecAudio = avcodec_find_decoder(formatAudio->streams[streamAudio]->codecpar->codec_id);
-		
-	AVCodecContext *codecContextAudio;
-	if (codecAudio != nullptr)
-		codecContextAudio = avcodec_alloc_context3(codecAudio);
-
-	// Get a pointer to the codec context for the audio stream
-	if (streamAudio > -1)
-	{
-		int ret = avcodec_parameters_to_context(codecContextAudio, formatAudio->streams[streamAudio]->codecpar);
-		if (ret < 0)
-		{
-			//We cannot get codec from parameters
-			avcodec_free_context(&codecContextAudio);
-		}
-	}
-	
-	// Open codec
-	AVFrame *frameAudio;
-	if (codecAudio != nullptr)
-	{
-		if ( avcodec_open2(codecContextAudio, codecAudio, nullptr) < 0 )
-		{
-			// Could not open codec
-			codecAudio = nullptr;
-		}
-		// Allocate audio frame
-		frameAudio = av_frame_alloc();
-	}
-		
-	AVPacket packet;
-
-	std::vector<ui8> samples;
-
-	while (av_read_frame(formatAudio, &packet) >= 0)
-	{
-		if(packet.stream_index == streamAudio)
-		{
-			int rc = avcodec_send_packet(codecContextAudio, &packet);
-			if (rc >= 0)
-				packet.size = 0;
-			rc = avcodec_receive_frame(codecContextAudio, frameAudio);
-			int bytesToRead = (frameAudio->nb_samples * 2 * (formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample / 8));
-			if (rc >= 0)
-				for (int s = 0; s < bytesToRead; s += sizeof(ui8))
-				{
-					ui8 value;
-					memcpy(&value, &frameAudio->data[0][s], sizeof(ui8));
-					samples.push_back(value);
-				}
-		}
-
-		av_packet_unref(&packet);
-	}
-
-	typedef struct WAV_HEADER {
-		ui8 RIFF[4] = {'R', 'I', 'F', 'F'};
-		ui32 ChunkSize;
-		ui8 WAVE[4] = {'W', 'A', 'V', 'E'};
-		ui8 fmt[4] = {'f', 'm', 't', ' '};
-		ui32 Subchunk1Size = 16;
-		ui16 AudioFormat = 1;
-		ui16 NumOfChan = 2;
-		ui32 SamplesPerSec = 22050;
-		ui32 bytesPerSec = 22050 * 2;
-		ui16 blockAlign = 2;
-		ui16 bitsPerSample = 16;
-		ui8 Subchunk2ID[4] = {'d', 'a', 't', 'a'};
-		ui32 Subchunk2Size;
-	} wav_hdr;
-
-	wav_hdr wav;
-	wav.ChunkSize = samples.size() + sizeof(wav_hdr) - 8;
-  	wav.Subchunk2Size = samples.size() + sizeof(wav_hdr) - 44;
-	wav.SamplesPerSec = formatAudio->streams[streamAudio]->codecpar->sample_rate;
-	wav.bitsPerSample = formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample;
-	auto wavPtr = reinterpret_cast<ui8*>(&wav);
-
-	dat = std::make_pair(std::make_unique<ui8[]>(samples.size() + sizeof(wav_hdr)), samples.size() + sizeof(wav_hdr));
-	std::copy(wavPtr, wavPtr + sizeof(wav_hdr), dat.first.get());
-	std::copy(samples.begin(), samples.end(), dat.first.get() + sizeof(wav_hdr));
-
-	if (frameAudio)
-		av_frame_free(&frameAudio);
-
-	if (codecAudio)
-	{
-		avcodec_close(codecContextAudio);
-		codecAudio = nullptr;
-	}
-	if (codecContextAudio)
-		avcodec_free_context(&codecContextAudio);
-
-	if (formatAudio)
-		avformat_close_input(&formatAudio);
-
-	if (contextAudio)
-	{
-		av_free(contextAudio);
-		contextAudio = nullptr;
-	}
-
-	return dat;
-}
-
-// Plays a video. Only works for overlays.
-bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey)
-{
-	// Note: either the windows player or the linux player is
-	// broken. Compensate here until the bug is found.
-	y--;
-
-	pos.x = x;
-	pos.y = y;
-	frameTime = 0.0;
-
-	while(nextFrame())
-	{
-		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);
-
-#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_for(boost::chrono::milliseconds(timeToSleepMillisec));
-	}
-
-	return true;
-}
-
-bool CVideoPlayer::openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey, bool scale)
-{
-	open(name, false, true, scale);
-	bool ret = playVideo(x, y,  stopOnKey);
-	close();
-	return ret;
-}
-
-CVideoPlayer::~CVideoPlayer()
-{
-	close();
-}
-
-#endif
-
+/*
+ * CVideoHandler.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 "CVideoHandler.h"
+
+#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 "../lib/filesystem/CInputStream.h"
+
+#include <SDL_render.h>
+
+#ifndef DISABLE_VIDEO
+
+extern "C" {
+#include <libavformat/avformat.h>
+#include <libavcodec/avcodec.h>
+#include <libavutil/imgutils.h>
+#include <libswscale/swscale.h>
+}
+
+#ifdef _MSC_VER
+#pragma comment(lib, "avcodec.lib")
+#pragma comment(lib, "avutil.lib")
+#pragma comment(lib, "avformat.lib")
+#pragma comment(lib, "swscale.lib")
+#endif // _MSC_VER
+
+// Define a set of functions to read data
+static int lodRead(void* opaque, uint8_t* buf, int size)
+{
+	auto video = reinterpret_cast<CVideoPlayer *>(opaque);
+
+	return static_cast<int>(video->data->read(buf, size));
+}
+
+static si64 lodSeek(void * opaque, si64 pos, int whence)
+{
+	auto video = reinterpret_cast<CVideoPlayer *>(opaque);
+
+	if (whence & AVSEEK_SIZE)
+		return video->data->getSize();
+
+	return video->data->seek(pos);
+}
+
+// Define a set of functions to read data
+static int lodReadAudio(void* opaque, uint8_t* buf, int size)
+{
+	auto video = reinterpret_cast<CVideoPlayer *>(opaque);
+
+	return static_cast<int>(video->dataAudio->read(buf, size));
+}
+
+static si64 lodSeekAudio(void * opaque, si64 pos, int whence)
+{
+	auto video = reinterpret_cast<CVideoPlayer *>(opaque);
+
+	if (whence & AVSEEK_SIZE)
+		return video->dataAudio->getSize();
+
+	return video->dataAudio->seek(pos);
+}
+
+CVideoPlayer::CVideoPlayer()
+	: stream(-1)
+	, format (nullptr)
+	, codecContext(nullptr)
+	, codec(nullptr)
+	, frame(nullptr)
+	, sws(nullptr)
+	, context(nullptr)
+	, texture(nullptr)
+	, dest(nullptr)
+	, destRect(0,0,0,0)
+	, pos(0,0,0,0)
+	, frameTime(0)
+	, doLoop(false)
+{}
+
+bool CVideoPlayer::open(const VideoPath & fname, bool scale)
+{
+	return open(fname, true, false);
+}
+
+// loop = to loop through the video
+// useOverlay = directly write to the screen.
+bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverlay, bool scale)
+{
+	close();
+
+	doLoop = loop;
+	frameTime = 0;
+
+	if (CResourceHandler::get()->existsResource(videoToOpen))
+		fname = videoToOpen;
+	else
+		fname = videoToOpen.addPrefix("VIDEO/");
+
+	if (!CResourceHandler::get()->existsResource(fname))
+	{
+		logGlobal->error("Error: video %s was not found", fname.getName());
+		return false;
+	}
+
+	data = CResourceHandler::get()->load(fname);
+
+	static const int BUFFER_SIZE = 4096;
+
+	unsigned char * buffer  = (unsigned char *)av_malloc(BUFFER_SIZE);// will be freed by ffmpeg
+	context = avio_alloc_context( buffer, BUFFER_SIZE, 0, (void *)this, lodRead, nullptr, lodSeek);
+
+	format = avformat_alloc_context();
+	format->pb = context;
+	// filename is not needed - file was already open and stored in this->data;
+	int avfopen = avformat_open_input(&format, "dummyFilename", nullptr, nullptr);
+
+	if (avfopen != 0)
+	{
+		return false;
+	}
+	// Retrieve stream information
+	if (avformat_find_stream_info(format, nullptr) < 0)
+		return false;
+
+	// Find the first video stream
+	stream = -1;
+	for(ui32 i=0; i<format->nb_streams; i++)
+	{
+		if (format->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
+		{
+			stream = i;
+			break;
+		}
+	}
+
+	if (stream < 0)
+		// No video stream in that file
+		return false;
+
+	// Find the decoder for the video stream
+	codec = avcodec_find_decoder(format->streams[stream]->codecpar->codec_id);
+
+	if (codec == nullptr)
+	{
+		// Unsupported codec
+		return false;
+	}
+
+	codecContext = avcodec_alloc_context3(codec);
+	if(!codecContext)
+		return false;
+	// Get a pointer to the codec context for the video stream
+	int ret = avcodec_parameters_to_context(codecContext, format->streams[stream]->codecpar);
+	if (ret < 0)
+	{
+		//We cannot get codec from parameters
+		avcodec_free_context(&codecContext);
+		return false;
+	}
+
+	// Open codec
+	if ( avcodec_open2(codecContext, codec, nullptr) < 0 )
+	{
+		// Could not open codec
+		codec = nullptr;
+		return false;
+	}
+	// Allocate video frame
+	frame = av_frame_alloc();
+
+	//setup scaling
+	if(scale)
+	{
+		pos.w = screen->w;
+		pos.h = screen->h;
+	}
+	else
+	{
+		pos.w  = codecContext->width;
+		pos.h = codecContext->height;
+	}
+
+	// Allocate a place to put our YUV image on that screen
+	if (useOverlay)
+	{
+		texture = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STATIC, pos.w, pos.h);
+	}
+	else
+	{
+		dest = CSDL_Ext::newSurface(pos.w, pos.h);
+		destRect.x = destRect.y = 0;
+		destRect.w = pos.w;
+		destRect.h = pos.h;
+	}
+
+	if (texture == nullptr && dest == nullptr)
+		return false;
+
+	if (texture)
+	{ // Convert the image into YUV format that SDL uses
+		sws = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt,
+							 pos.w, pos.h,
+							 AV_PIX_FMT_YUV420P,
+							 SWS_BICUBIC, nullptr, nullptr, nullptr);
+	}
+	else
+	{
+		AVPixelFormat screenFormat = AV_PIX_FMT_NONE;
+		if (screen->format->Bshift > screen->format->Rshift)
+		{
+			// this a BGR surface
+			switch (screen->format->BytesPerPixel)
+			{
+				case 2: screenFormat = AV_PIX_FMT_BGR565; break;
+				case 3: screenFormat = AV_PIX_FMT_BGR24; break;
+				case 4: screenFormat = AV_PIX_FMT_BGR32; break;
+				default: return false;
+			}
+		}
+		else
+		{
+			// this a RGB surface
+			switch (screen->format->BytesPerPixel)
+			{
+				case 2: screenFormat = AV_PIX_FMT_RGB565; break;
+				case 3: screenFormat = AV_PIX_FMT_RGB24; break;
+				case 4: screenFormat = AV_PIX_FMT_RGB32; break;
+				default: return false;
+			}
+		}
+
+		sws = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt,
+							 pos.w, pos.h, screenFormat,
+							 SWS_BICUBIC, nullptr, nullptr, nullptr);
+	}
+
+	if (sws == nullptr)
+		return false;
+
+	return true;
+}
+
+// Read the next frame. Return false on error/end of file.
+bool CVideoPlayer::nextFrame()
+{
+	AVPacket packet;
+	int frameFinished = 0;
+	bool gotError = false;
+
+	if (sws == nullptr)
+		return false;
+
+	while(!frameFinished)
+	{
+		int ret = av_read_frame(format, &packet);
+		if (ret < 0)
+		{
+			// Error. It's probably an end of file.
+			if (doLoop && !gotError)
+			{
+				// Rewind
+				frameTime = 0;
+				if (av_seek_frame(format, stream, 0, AVSEEK_FLAG_BYTE) < 0)
+					break;
+				gotError = true;
+			}
+			else
+			{
+				break;
+			}
+		}
+		else
+		{
+			// Is this a packet from the video stream?
+			if (packet.stream_index == stream)
+			{
+				// Decode video frame
+				int rc = avcodec_send_packet(codecContext, &packet);
+				if (rc >=0)
+					packet.size = 0;
+				rc = avcodec_receive_frame(codecContext, frame);
+				if (rc >= 0)
+					frameFinished = 1;
+				// Did we get a video frame?
+				if (frameFinished)
+				{
+					uint8_t *data[4];
+					int linesize[4];
+
+					if (texture) {
+						av_image_alloc(data, linesize, pos.w, pos.h, AV_PIX_FMT_YUV420P, 1);
+
+						sws_scale(sws, frame->data, frame->linesize,
+								  0, codecContext->height, data, linesize);
+
+						SDL_UpdateYUVTexture(texture, NULL, data[0], linesize[0],
+								data[1], linesize[1],
+								data[2], linesize[2]);
+						av_freep(&data[0]);
+					}
+					else
+					{
+						/* Avoid buffer overflow caused by sws_scale():
+						 *     http://trac.ffmpeg.org/ticket/9254
+						 * Currently (ffmpeg-4.4 with SSE3 enabled) sws_scale()
+						 * has a few requirements for target data buffers on rescaling:
+						 * 1. buffer has to be aligned to be usable for SIMD instructions
+						 * 2. buffer has to be padded to allow small overflow by SIMD instructions
+						 * Unfortunately SDL_Surface does not provide these guarantees.
+						 * This means that atempt to rescale directly into SDL surface causes
+						 * memory corruption. Usually it happens on campaign selection screen
+						 * where short video moves start spinning on mouse hover.
+						 *
+						 * To fix [1.] we use av_malloc() for memory allocation.
+						 * To fix [2.] we add an `ffmpeg_pad` that provides plenty of space.
+						 * We have to use intermdiate buffer and then use memcpy() to land it
+						 * to SDL_Surface.
+						 */
+						size_t pic_bytes = dest->pitch * dest->h;
+						size_t ffmped_pad = 1024; /* a few bytes of overflow will go here */
+						void * for_sws = av_malloc (pic_bytes + ffmped_pad);
+						data[0] = (ui8 *)for_sws;
+						linesize[0] = dest->pitch;
+
+						sws_scale(sws, frame->data, frame->linesize,
+								  0, codecContext->height, data, linesize);
+						memcpy(dest->pixels, for_sws, pic_bytes);
+						av_free(for_sws);
+					}
+				}
+			}
+
+			av_packet_unref(&packet);
+		}
+	}
+
+	return frameFinished != 0;
+}
+
+void CVideoPlayer::show( int x, int y, SDL_Surface *dst, bool update )
+{
+	if (sws == nullptr)
+		return;
+
+	pos.x = x;
+	pos.y = y;
+	CSDL_Ext::blitSurface(dest, destRect, dst, pos.topLeft());
+
+	if (update)
+		CSDL_Ext::updateRect(dst, pos);
+}
+
+void CVideoPlayer::redraw( int x, int y, SDL_Surface *dst, bool update )
+{
+	show(x, y, dst, update);
+}
+
+void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, bool update, std::function<void()> onVideoRestart)
+{
+	if (sws == nullptr)
+		return;
+
+#if (LIBAVUTIL_VERSION_MAJOR < 58)   
+	auto packet_duration = frame->pkt_duration;
+#else
+	auto packet_duration = frame->duration;
+#endif
+	double frameEndTime = (frame->pts + packet_duration) * av_q2d(format->streams[stream]->time_base);
+	frameTime += GH.framerate().getElapsedMilliseconds() / 1000.0;
+
+	if (frameTime >= frameEndTime )
+	{
+		if (nextFrame())
+			show(x,y,dst,update);
+		else
+		{
+			if(onVideoRestart)
+				onVideoRestart();
+			VideoPath filenameToReopen = fname; // create copy to backup this->fname
+			open(filenameToReopen);
+			nextFrame();
+
+			// The y position is wrong at the first frame.
+			// Note: either the windows player or the linux player is
+			// broken. Compensate here until the bug is found.
+			show(x, y--, dst, update);
+		}
+	}
+	else
+	{
+		redraw(x, y, dst, update);
+	}
+}
+
+void CVideoPlayer::close()
+{
+	fname = VideoPath();
+
+	if (sws)
+	{
+		sws_freeContext(sws);
+		sws = nullptr;
+	}
+
+	if (texture)
+	{
+		SDL_DestroyTexture(texture);
+		texture = nullptr;
+	}
+
+	if (dest)
+	{
+		SDL_FreeSurface(dest);
+		dest = nullptr;
+	}
+
+	if (frame)
+	{
+		av_frame_free(&frame);//will be set to null
+	}
+
+	if (codec)
+	{
+		avcodec_close(codecContext);
+		codec = nullptr;
+	}
+	if (codecContext)
+	{
+		avcodec_free_context(&codecContext);
+	}
+
+	if (format)
+	{
+		avformat_close_input(&format);
+	}
+
+	if (context)
+	{
+		av_free(context);
+		context = nullptr;
+	}
+}
+
+std::pair<std::unique_ptr<ui8 []>, si64> CVideoPlayer::getAudio(const VideoPath & videoToOpen)
+{
+	std::pair<std::unique_ptr<ui8 []>, si64> dat(std::make_pair(nullptr, 0));
+
+	VideoPath fnameAudio;
+
+	if (CResourceHandler::get()->existsResource(videoToOpen))
+		fnameAudio = videoToOpen;
+	else
+		fnameAudio = videoToOpen.addPrefix("VIDEO/");
+
+	if (!CResourceHandler::get()->existsResource(fnameAudio))
+	{
+		logGlobal->error("Error: video %s was not found", fnameAudio.getName());
+		return dat;
+	}
+
+	dataAudio = CResourceHandler::get()->load(fnameAudio);
+
+	static const int BUFFER_SIZE = 4096;
+
+	unsigned char * bufferAudio  = (unsigned char *)av_malloc(BUFFER_SIZE);// will be freed by ffmpeg
+	AVIOContext * contextAudio = avio_alloc_context( bufferAudio, BUFFER_SIZE, 0, (void *)this, lodReadAudio, nullptr, lodSeekAudio);
+
+	AVFormatContext * formatAudio = avformat_alloc_context();
+	formatAudio->pb = contextAudio;
+	// filename is not needed - file was already open and stored in this->data;
+	int avfopen = avformat_open_input(&formatAudio, "dummyFilename", nullptr, nullptr);
+
+	if (avfopen != 0)
+	{
+		return dat;
+	}
+	// Retrieve stream information
+	if (avformat_find_stream_info(formatAudio, nullptr) < 0)
+		return dat;
+
+	// Find the first audio stream
+	int streamAudio = -1;
+	for(ui32 i = 0; i < formatAudio->nb_streams; i++)
+	{
+		if (formatAudio->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
+		{
+			streamAudio = i;
+			break;
+		}
+	}
+
+	if(streamAudio < 0)
+		return dat;
+
+	const AVCodec *codecAudio = avcodec_find_decoder(formatAudio->streams[streamAudio]->codecpar->codec_id);
+		
+	AVCodecContext *codecContextAudio;
+	if (codecAudio != nullptr)
+		codecContextAudio = avcodec_alloc_context3(codecAudio);
+
+	// Get a pointer to the codec context for the audio stream
+	if (streamAudio > -1)
+	{
+		int ret = avcodec_parameters_to_context(codecContextAudio, formatAudio->streams[streamAudio]->codecpar);
+		if (ret < 0)
+		{
+			//We cannot get codec from parameters
+			avcodec_free_context(&codecContextAudio);
+		}
+	}
+	
+	// Open codec
+	AVFrame *frameAudio;
+	if (codecAudio != nullptr)
+	{
+		if ( avcodec_open2(codecContextAudio, codecAudio, nullptr) < 0 )
+		{
+			// Could not open codec
+			codecAudio = nullptr;
+		}
+		// Allocate audio frame
+		frameAudio = av_frame_alloc();
+	}
+		
+	AVPacket packet;
+
+	std::vector<ui8> samples;
+
+	while (av_read_frame(formatAudio, &packet) >= 0)
+	{
+		if(packet.stream_index == streamAudio)
+		{
+			int rc = avcodec_send_packet(codecContextAudio, &packet);
+			if (rc >= 0)
+				packet.size = 0;
+			rc = avcodec_receive_frame(codecContextAudio, frameAudio);
+			int bytesToRead = (frameAudio->nb_samples * 2 * (formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample / 8));
+			if (rc >= 0)
+				for (int s = 0; s < bytesToRead; s += sizeof(ui8))
+				{
+					ui8 value;
+					memcpy(&value, &frameAudio->data[0][s], sizeof(ui8));
+					samples.push_back(value);
+				}
+		}
+
+		av_packet_unref(&packet);
+	}
+
+	typedef struct WAV_HEADER {
+		ui8 RIFF[4] = {'R', 'I', 'F', 'F'};
+		ui32 ChunkSize;
+		ui8 WAVE[4] = {'W', 'A', 'V', 'E'};
+		ui8 fmt[4] = {'f', 'm', 't', ' '};
+		ui32 Subchunk1Size = 16;
+		ui16 AudioFormat = 1;
+		ui16 NumOfChan = 2;
+		ui32 SamplesPerSec = 22050;
+		ui32 bytesPerSec = 22050 * 2;
+		ui16 blockAlign = 2;
+		ui16 bitsPerSample = 16;
+		ui8 Subchunk2ID[4] = {'d', 'a', 't', 'a'};
+		ui32 Subchunk2Size;
+	} wav_hdr;
+
+	wav_hdr wav;
+	wav.ChunkSize = samples.size() + sizeof(wav_hdr) - 8;
+  	wav.Subchunk2Size = samples.size() + sizeof(wav_hdr) - 44;
+	wav.SamplesPerSec = formatAudio->streams[streamAudio]->codecpar->sample_rate;
+	wav.bitsPerSample = formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample;
+	auto wavPtr = reinterpret_cast<ui8*>(&wav);
+
+	dat = std::make_pair(std::make_unique<ui8[]>(samples.size() + sizeof(wav_hdr)), samples.size() + sizeof(wav_hdr));
+	std::copy(wavPtr, wavPtr + sizeof(wav_hdr), dat.first.get());
+	std::copy(samples.begin(), samples.end(), dat.first.get() + sizeof(wav_hdr));
+
+	if (frameAudio)
+		av_frame_free(&frameAudio);
+
+	if (codecAudio)
+	{
+		avcodec_close(codecContextAudio);
+		codecAudio = nullptr;
+	}
+	if (codecContextAudio)
+		avcodec_free_context(&codecContextAudio);
+
+	if (formatAudio)
+		avformat_close_input(&formatAudio);
+
+	if (contextAudio)
+	{
+		av_free(contextAudio);
+		contextAudio = nullptr;
+	}
+
+	return dat;
+}
+
+// Plays a video. Only works for overlays.
+bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey)
+{
+	// Note: either the windows player or the linux player is
+	// broken. Compensate here until the bug is found.
+	y--;
+
+	pos.x = x;
+	pos.y = y;
+	frameTime = 0.0;
+
+	while(nextFrame())
+	{
+		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);
+
+#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_for(boost::chrono::milliseconds(timeToSleepMillisec));
+	}
+
+	return true;
+}
+
+bool CVideoPlayer::openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey, bool scale)
+{
+	open(name, false, true, scale);
+	bool ret = playVideo(x, y,  stopOnKey);
+	close();
+	return ret;
+}
+
+CVideoPlayer::~CVideoPlayer()
+{
+	close();
+}
+
+#endif
+

+ 121 - 121
client/CVideoHandler.h

@@ -1,121 +1,121 @@
-/*
- * CVideoHandler.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"
-#include "../lib/filesystem/ResourcePath.h"
-
-struct SDL_Surface;
-struct SDL_Texture;
-
-class IVideoPlayer : boost::noncopyable
-{
-public:
-	virtual bool open(const VideoPath & name, bool scale = false)=0; //true - succes
-	virtual void close()=0;
-	virtual bool nextFrame()=0;
-	virtual void show(int x, int y, SDL_Surface *dst, bool update = true)=0;
-	virtual void redraw(int x, int y, SDL_Surface *dst, bool update = true)=0; //reblits buffer
-	virtual bool wait()=0;
-	virtual int curFrame() const =0;
-	virtual int frameCount() const =0;
-};
-
-class IMainVideoPlayer : public IVideoPlayer
-{
-public:
-	virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function<void()> restart = 0){}
-	virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false)
-	{
-		return false;
-	}
-	virtual std::pair<std::unique_ptr<ui8 []>, si64> getAudio(const VideoPath & videoToOpen) { return std::make_pair(nullptr, 0); };
-};
-
-class CEmptyVideoPlayer : public IMainVideoPlayer
-{
-public:
-	int curFrame() const override {return -1;};
-	int frameCount() const override {return -1;};
-	void redraw( int x, int y, SDL_Surface *dst, bool update = true ) override {};
-	void show( int x, int y, SDL_Surface *dst, bool update = true ) override {};
-	bool nextFrame() override {return false;};
-	void close() override {};
-	bool wait() override {return false;};
-	bool open(const VideoPath & name, bool scale = false) override {return false;};
-};
-
-#ifndef DISABLE_VIDEO
-
-struct AVFormatContext;
-struct AVCodecContext;
-struct AVCodec;
-struct AVFrame;
-struct AVIOContext;
-
-VCMI_LIB_NAMESPACE_BEGIN
-class CInputStream;
-VCMI_LIB_NAMESPACE_END
-
-class CVideoPlayer : public IMainVideoPlayer
-{
-	int stream;					// stream index in video
-	AVFormatContext *format;
-	AVCodecContext *codecContext; // codec context for stream
-	const AVCodec *codec;
-	AVFrame *frame;
-	struct SwsContext *sws;
-
-	AVIOContext * context;
-
-	VideoPath fname;  //name of current video file (empty if idle)
-
-	// Destination. Either overlay or dest.
-
-	SDL_Texture *texture;
-	SDL_Surface *dest;
-	Rect destRect;			// valid when dest is used
-	Rect pos;				// destination on screen
-
-	/// video playback currnet progress, in seconds
-	double frameTime;
-	bool doLoop;				// loop through video
-
-	bool playVideo(int x, int y, bool stopOnKey);
-	bool open(const VideoPath & fname, bool loop, bool useOverlay = false, bool scale = false);
-public:
-	CVideoPlayer();
-	~CVideoPlayer();
-
-	bool init();
-	bool open(const VideoPath & fname, bool scale = false) override;
-	void close() override;
-	bool nextFrame() override;			// display next frame
-
-	void show(int x, int y, SDL_Surface *dst, bool update = true) override; //blit current frame
-	void redraw(int x, int y, SDL_Surface *dst, bool update = true) override; //reblits buffer
-	void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function<void()> onVideoRestart = nullptr) override; //moves to next frame if appropriate, and blits it or blits only if redraw parameter is set true
-
-	// Opens video, calls playVideo, closes video; returns playVideo result (if whole video has been played)
-	bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) override;
-
-	std::pair<std::unique_ptr<ui8 []>, si64> getAudio(const VideoPath & videoToOpen) override;
-
-	//TODO:
-	bool wait() override {return false;};
-	int curFrame() const override {return -1;};
-	int frameCount() const override {return -1;};
-
-	// public to allow access from ffmpeg IO functions
-	std::unique_ptr<CInputStream> data;
-	std::unique_ptr<CInputStream> dataAudio;
-};
-
-#endif
+/*
+ * CVideoHandler.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"
+#include "../lib/filesystem/ResourcePath.h"
+
+struct SDL_Surface;
+struct SDL_Texture;
+
+class IVideoPlayer : boost::noncopyable
+{
+public:
+	virtual bool open(const VideoPath & name, bool scale = false)=0; //true - succes
+	virtual void close()=0;
+	virtual bool nextFrame()=0;
+	virtual void show(int x, int y, SDL_Surface *dst, bool update = true)=0;
+	virtual void redraw(int x, int y, SDL_Surface *dst, bool update = true)=0; //reblits buffer
+	virtual bool wait()=0;
+	virtual int curFrame() const =0;
+	virtual int frameCount() const =0;
+};
+
+class IMainVideoPlayer : public IVideoPlayer
+{
+public:
+	virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function<void()> restart = 0){}
+	virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false)
+	{
+		return false;
+	}
+	virtual std::pair<std::unique_ptr<ui8 []>, si64> getAudio(const VideoPath & videoToOpen) { return std::make_pair(nullptr, 0); };
+};
+
+class CEmptyVideoPlayer : public IMainVideoPlayer
+{
+public:
+	int curFrame() const override {return -1;};
+	int frameCount() const override {return -1;};
+	void redraw( int x, int y, SDL_Surface *dst, bool update = true ) override {};
+	void show( int x, int y, SDL_Surface *dst, bool update = true ) override {};
+	bool nextFrame() override {return false;};
+	void close() override {};
+	bool wait() override {return false;};
+	bool open(const VideoPath & name, bool scale = false) override {return false;};
+};
+
+#ifndef DISABLE_VIDEO
+
+struct AVFormatContext;
+struct AVCodecContext;
+struct AVCodec;
+struct AVFrame;
+struct AVIOContext;
+
+VCMI_LIB_NAMESPACE_BEGIN
+class CInputStream;
+VCMI_LIB_NAMESPACE_END
+
+class CVideoPlayer : public IMainVideoPlayer
+{
+	int stream;					// stream index in video
+	AVFormatContext *format;
+	AVCodecContext *codecContext; // codec context for stream
+	const AVCodec *codec;
+	AVFrame *frame;
+	struct SwsContext *sws;
+
+	AVIOContext * context;
+
+	VideoPath fname;  //name of current video file (empty if idle)
+
+	// Destination. Either overlay or dest.
+
+	SDL_Texture *texture;
+	SDL_Surface *dest;
+	Rect destRect;			// valid when dest is used
+	Rect pos;				// destination on screen
+
+	/// video playback currnet progress, in seconds
+	double frameTime;
+	bool doLoop;				// loop through video
+
+	bool playVideo(int x, int y, bool stopOnKey);
+	bool open(const VideoPath & fname, bool loop, bool useOverlay = false, bool scale = false);
+public:
+	CVideoPlayer();
+	~CVideoPlayer();
+
+	bool init();
+	bool open(const VideoPath & fname, bool scale = false) override;
+	void close() override;
+	bool nextFrame() override;			// display next frame
+
+	void show(int x, int y, SDL_Surface *dst, bool update = true) override; //blit current frame
+	void redraw(int x, int y, SDL_Surface *dst, bool update = true) override; //reblits buffer
+	void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function<void()> onVideoRestart = nullptr) override; //moves to next frame if appropriate, and blits it or blits only if redraw parameter is set true
+
+	// Opens video, calls playVideo, closes video; returns playVideo result (if whole video has been played)
+	bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) override;
+
+	std::pair<std::unique_ptr<ui8 []>, si64> getAudio(const VideoPath & videoToOpen) override;
+
+	//TODO:
+	bool wait() override {return false;};
+	int curFrame() const override {return -1;};
+	int frameCount() const override {return -1;};
+
+	// public to allow access from ffmpeg IO functions
+	std::unique_ptr<CInputStream> data;
+	std::unique_ptr<CInputStream> dataAudio;
+};
+
+#endif

+ 740 - 740
client/Client.cpp

@@ -1,740 +1,740 @@
-/*
- * Client.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 "Global.h"
-#include "StdInc.h"
-#include "Client.h"
-
-#include "CGameInfo.h"
-#include "CPlayerInterface.h"
-#include "CServerHandler.h"
-#include "ClientNetPackVisitors.h"
-#include "adventureMap/AdventureMapInterface.h"
-#include "battle/BattleInterface.h"
-#include "gui/CGuiHandler.h"
-#include "gui/WindowHandler.h"
-#include "mapView/mapHandler.h"
-
-#include "../CCallback.h"
-#include "../lib/CConfigHandler.h"
-#include "../lib/gameState/CGameState.h"
-#include "../lib/CThreadHelper.h"
-#include "../lib/VCMIDirs.h"
-#include "../lib/UnlockGuard.h"
-#include "../lib/battle/BattleInfo.h"
-#include "../lib/serializer/BinaryDeserializer.h"
-#include "../lib/mapping/CMapService.h"
-#include "../lib/pathfinder/CGPathNode.h"
-#include "../lib/filesystem/Filesystem.h"
-#include "../lib/registerTypes/RegisterTypes.h"
-#include "../lib/serializer/Connection.h"
-
-#include <memory>
-#include <vcmi/events/EventBus.h>
-
-#if SCRIPTING_ENABLED
-#include "../lib/ScriptHandler.h"
-#endif
-
-#ifdef VCMI_ANDROID
-#include "lib/CAndroidVMHelper.h"
-
-#ifndef SINGLE_PROCESS_APP
-std::atomic_bool androidTestServerReadyFlag;
-#endif
-#endif
-
-ThreadSafeVector<int> CClient::waitingRequest;
-
-template<typename T> class CApplyOnCL;
-
-class CBaseForCLApply
-{
-public:
-	virtual void applyOnClAfter(CClient * cl, void * pack) const =0;
-	virtual void applyOnClBefore(CClient * cl, void * pack) const =0;
-	virtual ~CBaseForCLApply(){}
-
-	template<typename U> static CBaseForCLApply * getApplier(const U * t = nullptr)
-	{
-		return new CApplyOnCL<U>();
-	}
-};
-
-template<typename T> class CApplyOnCL : public CBaseForCLApply
-{
-public:
-	void applyOnClAfter(CClient * cl, void * pack) const override
-	{
-		T * ptr = static_cast<T *>(pack);
-		ApplyClientNetPackVisitor visitor(*cl, *cl->gameState());
-		ptr->visit(visitor);
-	}
-	void applyOnClBefore(CClient * cl, void * pack) const override
-	{
-		T * ptr = static_cast<T *>(pack);
-		ApplyFirstClientNetPackVisitor visitor(*cl, *cl->gameState());
-		ptr->visit(visitor);
-	}
-};
-
-template<> class CApplyOnCL<CPack>: public CBaseForCLApply
-{
-public:
-	void applyOnClAfter(CClient * cl, void * pack) const override
-	{
-		logGlobal->error("Cannot apply on CL plain CPack!");
-		assert(0);
-	}
-	void applyOnClBefore(CClient * cl, void * pack) const override
-	{
-		logGlobal->error("Cannot apply on CL plain CPack!");
-		assert(0);
-	}
-};
-
-CPlayerEnvironment::CPlayerEnvironment(PlayerColor player_, CClient * cl_, std::shared_ptr<CCallback> mainCallback_)
-	: player(player_),
-	cl(cl_),
-	mainCallback(mainCallback_)
-{
-
-}
-
-const Services * CPlayerEnvironment::services() const
-{
-	return VLC;
-}
-
-vstd::CLoggerBase * CPlayerEnvironment::logger() const
-{
-	return logGlobal;
-}
-
-events::EventBus * CPlayerEnvironment::eventBus() const
-{
-	return cl->eventBus();//always get actual value
-}
-
-const CPlayerEnvironment::BattleCb * CPlayerEnvironment::battle(const BattleID & battleID) const
-{
-	return mainCallback->getBattle(battleID).get();
-}
-
-const CPlayerEnvironment::GameCb * CPlayerEnvironment::game() const
-{
-	return mainCallback.get();
-}
-
-
-CClient::CClient()
-{
-	waitingRequest.clear();
-	applier = std::make_shared<CApplier<CBaseForCLApply>>();
-	registerTypesClientPacks1(*applier);
-	registerTypesClientPacks2(*applier);
-	IObjectInterface::cb = this;
-	gs = nullptr;
-}
-
-CClient::~CClient()
-{
-	IObjectInterface::cb = nullptr;
-}
-
-const Services * CClient::services() const
-{
-	return VLC; //todo: this should be CGI
-}
-
-const CClient::BattleCb * CClient::battle(const BattleID & battleID) const
-{
-	return nullptr; //todo?
-}
-
-const CClient::GameCb * CClient::game() const
-{
-	return this;
-}
-
-vstd::CLoggerBase * CClient::logger() const
-{
-	return logGlobal;
-}
-
-events::EventBus * CClient::eventBus() const
-{
-	return clientEventBus.get();
-}
-
-void CClient::newGame(CGameState * initializedGameState)
-{
-	CSH->th->update();
-	CMapService mapService;
-	gs = initializedGameState ? initializedGameState : new CGameState();
-	gs->preInit(VLC);
-	logNetwork->trace("\tCreating gamestate: %i", CSH->th->getDiff());
-	if(!initializedGameState)
-	{
-		Load::ProgressAccumulator progressTracking;
-		gs->init(&mapService, CSH->si.get(), progressTracking, settings["general"]["saveRandomMaps"].Bool());
-	}
-	logNetwork->trace("Initializing GameState (together): %d ms", CSH->th->getDiff());
-
-	initMapHandler();
-	reinitScripting();
-	initPlayerEnvironments();
-	initPlayerInterfaces();
-}
-
-void CClient::loadGame(CGameState * initializedGameState)
-{
-	logNetwork->info("Loading procedure started!");
-
-	logNetwork->info("Game state was transferred over network, loading.");
-	gs = initializedGameState;
-
-	gs->preInit(VLC);
-	gs->updateOnLoad(CSH->si.get());
-	logNetwork->info("Game loaded, initialize interfaces.");
-
-	initMapHandler();
-
-	reinitScripting();
-
-	initPlayerEnvironments();
-	
-	// Loading of client state - disabled for now
-	// Since client no longer writes or loads its own state and instead receives it from server
-	// client state serializer will serialize its own copies of all pointers, e.g. heroes/towns/objects
-	// and on deserialize will create its own copies (instead of using copies from state received from server)
-	// Potential solutions:
-	// 1) Use server gamestate to deserialize pointers, so any pointer to same object will point to server instance and not our copy
-	// 2) Remove all serialization of pointers with instance ID's and restore them on load (including AI deserializer code)
-	// 3) Completely remove client savegame and send all information, like hero paths and sleeping status to server (either in form of hero properties or as some generic "client options" message
-#ifdef BROKEN_CLIENT_STATE_SERIALIZATION_HAS_BEEN_FIXED
-	// try to deserialize client data including sleepingHeroes
-	try
-	{
-		boost::filesystem::path clientSaveName = *CResourceHandler::get()->getResourceName(ResourcePath(CSH->si->mapname, EResType::CLIENT_SAVEGAME));
-
-		if(clientSaveName.empty())
-			throw std::runtime_error("Cannot open client part of " + CSH->si->mapname);
-
-		std::unique_ptr<CLoadFile> loader (new CLoadFile(clientSaveName));
-		serialize(loader->serializer, loader->serializer.fileVersion);
-
-		logNetwork->info("Client data loaded.");
-	}
-	catch(std::exception & e)
-	{
-		logGlobal->info("Cannot load client data for game %s. Error: %s", CSH->si->mapname, e.what());
-	}
-#endif
-
-	initPlayerInterfaces();
-}
-
-void CClient::serialize(BinarySerializer & h, const int version)
-{
-	assert(h.saving);
-	ui8 players = static_cast<ui8>(playerint.size());
-	h & players;
-
-	for(auto i = playerint.begin(); i != playerint.end(); i++)
-	{
-		logGlobal->trace("Saving player %s interface", i->first);
-		assert(i->first == i->second->playerID);
-		h & i->first;
-		h & i->second->dllName;
-		h & i->second->human;
-		i->second->saveGame(h, version);
-	}
-
-#if SCRIPTING_ENABLED
-	if(version >= 800)
-	{
-		JsonNode scriptsState;
-		clientScripts->serializeState(h.saving, scriptsState);
-		h & scriptsState;
-	}
-#endif
-}
-
-void CClient::serialize(BinaryDeserializer & h, const int version)
-{
-	assert(!h.saving);
-	ui8 players = 0;
-	h & players;
-
-	for(int i = 0; i < players; i++)
-	{
-		std::string dllname;
-		PlayerColor pid;
-		bool isHuman = false;
-		auto prevInt = LOCPLINT;
-
-		h & pid;
-		h & dllname;
-		h & isHuman;
-		assert(dllname.length() == 0 || !isHuman);
-		if(pid == PlayerColor::NEUTRAL)
-		{
-			logGlobal->trace("Neutral battle interfaces are not serialized.");
-			continue;
-		}
-
-		logGlobal->trace("Loading player %s interface", pid);
-		std::shared_ptr<CGameInterface> nInt;
-		if(dllname.length())
-			nInt = CDynLibHandler::getNewAI(dllname);
-		else
-			nInt = std::make_shared<CPlayerInterface>(pid);
-
-		nInt->dllName = dllname;
-		nInt->human = isHuman;
-		nInt->playerID = pid;
-
-		bool shouldResetInterface = true;
-		// Client no longer handle this player at all
-		if(!vstd::contains(CSH->getAllClientPlayers(CSH->c->connectionID), pid))
-		{
-			logGlobal->trace("Player %s is not belong to this client. Destroying interface", pid);
-		}
-		else if(isHuman && !vstd::contains(CSH->getHumanColors(), pid))
-		{
-			logGlobal->trace("Player %s is no longer controlled by human. Destroying interface", pid);
-		}
-		else if(!isHuman && vstd::contains(CSH->getHumanColors(), pid))
-		{
-			logGlobal->trace("Player %s is no longer controlled by AI. Destroying interface", pid);
-		}
-		else
-		{
-			installNewPlayerInterface(nInt, pid);
-			shouldResetInterface = false;
-		}
-
-		// loadGame needs to be called after initGameInterface to load paths correctly
-		// initGameInterface is called in installNewPlayerInterface
-		nInt->loadGame(h, version);
-
-		if (shouldResetInterface)
-		{
-			nInt.reset();
-			LOCPLINT = prevInt;
-		}
-	}
-
-#if SCRIPTING_ENABLED
-	{
-		JsonNode scriptsState;
-		h & scriptsState;
-		clientScripts->serializeState(h.saving, scriptsState);
-	}
-#endif
-
-	logNetwork->trace("Loaded client part of save %d ms", CSH->th->getDiff());
-}
-
-void CClient::save(const std::string & fname)
-{
-	if(!gs->currentBattles.empty())
-	{
-		logNetwork->error("Game cannot be saved during battle!");
-		return;
-	}
-
-	SaveGame save_game(fname);
-	sendRequest(&save_game, PlayerColor::NEUTRAL);
-}
-
-void CClient::endGame()
-{
-#if SCRIPTING_ENABLED
-	clientScripts.reset();
-#endif
-
-	//suggest interfaces to finish their stuff (AI should interrupt any bg working threads)
-	for(auto & i : playerint)
-		i.second->finish();
-
-	GH.curInt = nullptr;
-	{
-		logNetwork->info("Ending current game!");
-		removeGUI();
-
-		vstd::clear_pointer(const_cast<CGameInfo *>(CGI)->mh);
-		vstd::clear_pointer(gs);
-
-		logNetwork->info("Deleted mapHandler and gameState.");
-	}
-
-	CPlayerInterface::battleInt.reset();
-	playerint.clear();
-	battleints.clear();
-	battleCallbacks.clear();
-	playerEnvironments.clear();
-	logNetwork->info("Deleted playerInts.");
-	logNetwork->info("Client stopped.");
-}
-
-void CClient::initMapHandler()
-{
-	// TODO: CMapHandler initialization can probably go somewhere else
-	// It's can't be before initialization of interfaces
-	// During loading CPlayerInterface from serialized state it's depend on MH
-	if(!settings["session"]["headless"].Bool())
-	{
-		const_cast<CGameInfo *>(CGI)->mh = new CMapHandler(gs->map);
-		logNetwork->trace("Creating mapHandler: %d ms", CSH->th->getDiff());
-	}
-
-	pathCache.clear();
-}
-
-void CClient::initPlayerEnvironments()
-{
-	playerEnvironments.clear();
-
-	auto allPlayers = CSH->getAllClientPlayers(CSH->c->connectionID);
-	bool hasHumanPlayer = false;
-	for(auto & color : allPlayers)
-	{
-		logNetwork->info("Preparing environment for player %s", color.toString());
-		playerEnvironments[color] = std::make_shared<CPlayerEnvironment>(color, this, std::make_shared<CCallback>(gs, color, this));
-		
-		if(!hasHumanPlayer && gs->players[color].isHuman())
-			hasHumanPlayer = true;
-	}
-
-	if(!hasHumanPlayer)
-	{
-		Settings session = settings.write["session"];
-		session["spectate"].Bool() = true;
-		session["spectate-skip-battle-result"].Bool() = true;
-		session["spectate-ignore-hero"].Bool() = true;
-	}
-	
-	if(settings["session"]["spectate"].Bool())
-	{
-		playerEnvironments[PlayerColor::SPECTATOR] = std::make_shared<CPlayerEnvironment>(PlayerColor::SPECTATOR, this, std::make_shared<CCallback>(gs, std::nullopt, this));
-	}
-}
-
-void CClient::initPlayerInterfaces()
-{
-	for(auto & playerInfo : gs->scenarioOps->playerInfos)
-	{
-		PlayerColor color = playerInfo.first;
-		if(!vstd::contains(CSH->getAllClientPlayers(CSH->c->connectionID), color))
-			continue;
-
-		if(!vstd::contains(playerint, color))
-		{
-			logNetwork->info("Preparing interface for player %s", color.toString());
-			if(playerInfo.second.isControlledByAI())
-			{
-				bool alliedToHuman = false;
-				for(auto & allyInfo : gs->scenarioOps->playerInfos)
-					if (gs->getPlayerTeam(allyInfo.first) == gs->getPlayerTeam(playerInfo.first) && allyInfo.second.isControlledByHuman())
-						alliedToHuman = true;
-
-				auto AiToGive = aiNameForPlayer(playerInfo.second, false, alliedToHuman);
-				logNetwork->info("Player %s will be lead by %s", color.toString(), AiToGive);
-				installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), color);
-			}
-			else
-			{
-				logNetwork->info("Player %s will be lead by human", color.toString());
-				installNewPlayerInterface(std::make_shared<CPlayerInterface>(color), color);
-			}
-		}
-	}
-
-	if(settings["session"]["spectate"].Bool())
-	{
-		installNewPlayerInterface(std::make_shared<CPlayerInterface>(PlayerColor::SPECTATOR), PlayerColor::SPECTATOR, true);
-	}
-
-	if(CSH->getAllClientPlayers(CSH->c->connectionID).count(PlayerColor::NEUTRAL))
-		installNewBattleInterface(CDynLibHandler::getNewBattleAI(settings["server"]["neutralAI"].String()), PlayerColor::NEUTRAL);
-
-	logNetwork->trace("Initialized player interfaces %d ms", CSH->th->getDiff());
-}
-
-std::string CClient::aiNameForPlayer(const PlayerSettings & ps, bool battleAI, bool alliedToHuman)
-{
-	if(ps.name.size())
-	{
-		const boost::filesystem::path aiPath = VCMIDirs::get().fullLibraryPath("AI", ps.name);
-		if(boost::filesystem::exists(aiPath))
-			return ps.name;
-	}
-
-	return aiNameForPlayer(battleAI, alliedToHuman);
-}
-
-std::string CClient::aiNameForPlayer(bool battleAI, bool alliedToHuman)
-{
-	const int sensibleAILimit = settings["session"]["oneGoodAI"].Bool() ? 1 : PlayerColor::PLAYER_LIMIT_I;
-	std::string goodAdventureAI = alliedToHuman ? settings["server"]["alliedAI"].String() : settings["server"]["playerAI"].String();
-	std::string goodBattleAI = settings["server"]["neutralAI"].String();
-	std::string goodAI = battleAI ? goodBattleAI : goodAdventureAI;
-	std::string badAI = battleAI ? "StupidAI" : "EmptyAI";
-
-	//TODO what about human players
-	if(battleints.size() >= sensibleAILimit)
-		return badAI;
-
-	return goodAI;
-}
-
-void CClient::installNewPlayerInterface(std::shared_ptr<CGameInterface> gameInterface, PlayerColor color, bool battlecb)
-{
-	playerint[color] = gameInterface;
-
-	logGlobal->trace("\tInitializing the interface for player %s", color.toString());
-	auto cb = std::make_shared<CCallback>(gs, color, this);
-	battleCallbacks[color] = cb;
-	gameInterface->initGameInterface(playerEnvironments.at(color), cb);
-
-	installNewBattleInterface(gameInterface, color, battlecb);
-}
-
-void CClient::installNewBattleInterface(std::shared_ptr<CBattleGameInterface> battleInterface, PlayerColor color, bool needCallback)
-{
-	battleints[color] = battleInterface;
-
-	if(needCallback)
-	{
-		logGlobal->trace("\tInitializing the battle interface for player %s", color.toString());
-		auto cbc = std::make_shared<CBattleCallback>(color, this);
-		battleCallbacks[color] = cbc;
-		battleInterface->initBattleInterface(playerEnvironments.at(color), cbc);
-	}
-}
-
-void CClient::handlePack(CPack * pack)
-{
-	CBaseForCLApply * apply = applier->getApplier(typeList.getTypeID(pack)); //find the applier
-	if(apply)
-	{
-		boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
-		apply->applyOnClBefore(this, pack);
-		logNetwork->trace("\tMade first apply on cl: %s", typeList.getTypeInfo(pack)->name());
-		gs->apply(pack);
-		logNetwork->trace("\tApplied on gs: %s", typeList.getTypeInfo(pack)->name());
-		apply->applyOnClAfter(this, pack);
-		logNetwork->trace("\tMade second apply on cl: %s", typeList.getTypeInfo(pack)->name());
-	}
-	else
-	{
-		logNetwork->error("Message %s cannot be applied, cannot find applier!", typeList.getTypeInfo(pack)->name());
-	}
-	delete pack;
-}
-
-int CClient::sendRequest(const CPackForServer * request, PlayerColor player)
-{
-	static ui32 requestCounter = 0;
-
-	ui32 requestID = requestCounter++;
-	logNetwork->trace("Sending a request \"%s\". It'll have an ID=%d.", typeid(*request).name(), requestID);
-
-	waitingRequest.pushBack(requestID);
-	request->requestID = requestID;
-	request->player = player;
-	CSH->c->sendPack(request);
-	if(vstd::contains(playerint, player))
-		playerint[player]->requestSent(request, requestID);
-
-	return requestID;
-}
-
-void CClient::battleStarted(const BattleInfo * info)
-{
-	for(auto & battleCb : battleCallbacks)
-	{
-		if(vstd::contains_if(info->sides, [&](const SideInBattle& side) {return side.color == battleCb.first; })
-			|| !battleCb.first.isValidPlayer())
-		{
-			battleCb.second->onBattleStarted(info);
-		}
-	}
-
-	std::shared_ptr<CPlayerInterface> att, def;
-	auto & leftSide = info->sides[0], & rightSide = info->sides[1];
-
-	//If quick combat is not, do not prepare interfaces for battleint
-	auto callBattleStart = [&](PlayerColor color, ui8 side)
-	{
-		if(vstd::contains(battleints, color))
-			battleints[color]->battleStart(info->battleID, leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side, info->replayAllowed);
-	};
-	
-	callBattleStart(leftSide.color, 0);
-	callBattleStart(rightSide.color, 1);
-	callBattleStart(PlayerColor::UNFLAGGABLE, 1);
-	if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool())
-		callBattleStart(PlayerColor::SPECTATOR, 1);
-	
-	if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human)
-		att = std::dynamic_pointer_cast<CPlayerInterface>(playerint[leftSide.color]);
-
-	if(vstd::contains(playerint, rightSide.color) && playerint[rightSide.color]->human)
-		def = std::dynamic_pointer_cast<CPlayerInterface>(playerint[rightSide.color]);
-	
-	//Remove player interfaces for auto battle (quickCombat option)
-	if(att && att->isAutoFightOn)
-	{
-		if (att->cb->getBattle(info->battleID)->battleGetTacticDist())
-		{
-			auto side = att->cb->getBattle(info->battleID)->playerToSide(att->playerID);
-			auto action = BattleAction::makeEndOFTacticPhase(*side);
-			att->cb->battleMakeTacticAction(info->battleID, action);
-		}
-
-		att.reset();
-		def.reset();
-	}
-
-	if(!settings["session"]["headless"].Bool())
-	{
-		if(att || def)
-		{
-			CPlayerInterface::battleInt = std::make_shared<BattleInterface>(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def);
-		}
-		else if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool())
-		{
-			//TODO: This certainly need improvement
-			auto spectratorInt = std::dynamic_pointer_cast<CPlayerInterface>(playerint[PlayerColor::SPECTATOR]);
-			spectratorInt->cb->onBattleStarted(info);
-			CPlayerInterface::battleInt = std::make_shared<BattleInterface>(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def, spectratorInt);
-		}
-	}
-
-	if(info->tacticDistance)
-	{
-		auto tacticianColor = info->sides[info->tacticsSide].color;
-
-		if (vstd::contains(battleints, tacticianColor))
-			battleints[tacticianColor]->yourTacticPhase(info->battleID, info->tacticDistance);
-	}
-}
-
-void CClient::battleFinished(const BattleID & battleID)
-{
-	for(auto & side : gs->getBattle(battleID)->sides)
-		if(battleCallbacks.count(side.color))
-			battleCallbacks[side.color]->onBattleEnded(battleID);
-
-	if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool())
-		battleCallbacks[PlayerColor::SPECTATOR]->onBattleEnded(battleID);
-}
-
-void CClient::startPlayerBattleAction(const BattleID & battleID, PlayerColor color)
-{
-	auto battleint = battleints.at(color);
-
-	if (!battleint->human)
-	{
-		// we want to avoid locking gamestate and causing UI to freeze while AI is making turn
-		auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex);
-		battleint->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false));
-	}
-	else
-	{
-		battleint->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false));
-	}
-}
-
-void CClient::invalidatePaths()
-{
-	boost::unique_lock<boost::mutex> pathLock(pathCacheMutex);
-	pathCache.clear();
-}
-
-std::shared_ptr<const CPathsInfo> CClient::getPathsInfo(const CGHeroInstance * h)
-{
-	assert(h);
-	boost::unique_lock<boost::mutex> pathLock(pathCacheMutex);
-
-	auto iter = pathCache.find(h);
-
-	if(iter == std::end(pathCache))
-	{
-		std::shared_ptr<CPathsInfo> paths = std::make_shared<CPathsInfo>(getMapSize(), h);
-
-		gs->calculatePaths(h, *paths.get());
-
-		pathCache[h] = paths;
-		return paths;
-	}
-	else
-	{
-		return iter->second;
-	}
-}
-
-#if SCRIPTING_ENABLED
-scripting::Pool * CClient::getGlobalContextPool() const
-{
-	return clientScripts.get();
-}
-#endif
-
-void CClient::reinitScripting()
-{
-	clientEventBus = std::make_unique<events::EventBus>();
-#if SCRIPTING_ENABLED
-	clientScripts.reset(new scripting::PoolImpl(this));
-#endif
-}
-
-void CClient::removeGUI()
-{
-	// CClient::endGame
-	GH.curInt = nullptr;
-	GH.windows().clear();
-	adventureInt.reset();
-	logGlobal->info("Removed GUI.");
-
-	LOCPLINT = nullptr;
-}
-
-#ifdef VCMI_ANDROID
-#ifndef SINGLE_PROCESS_APP
-extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_notifyServerClosed(JNIEnv * env, jclass cls)
-{
-	logNetwork->info("Received server closed signal");
-	if (CSH) {
-		CSH->campaignServerRestartLock.setn(false);
-	}
-}
-
-extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_notifyServerReady(JNIEnv * env, jclass cls)
-{
-	logNetwork->info("Received server ready signal");
-	androidTestServerReadyFlag.store(true);
-}
-#endif
-
-extern "C" JNIEXPORT jboolean JNICALL Java_eu_vcmi_vcmi_NativeMethods_tryToSaveTheGame(JNIEnv * env, jclass cls)
-{
-	logGlobal->info("Received emergency save game request");
-	if(!LOCPLINT || !LOCPLINT->cb)
-	{
-		return false;
-	}
-
-	LOCPLINT->cb->save("Saves/_Android_Autosave");
-	return true;
-}
-#endif
+/*
+ * Client.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 "Global.h"
+#include "StdInc.h"
+#include "Client.h"
+
+#include "CGameInfo.h"
+#include "CPlayerInterface.h"
+#include "CServerHandler.h"
+#include "ClientNetPackVisitors.h"
+#include "adventureMap/AdventureMapInterface.h"
+#include "battle/BattleInterface.h"
+#include "gui/CGuiHandler.h"
+#include "gui/WindowHandler.h"
+#include "mapView/mapHandler.h"
+
+#include "../CCallback.h"
+#include "../lib/CConfigHandler.h"
+#include "../lib/gameState/CGameState.h"
+#include "../lib/CThreadHelper.h"
+#include "../lib/VCMIDirs.h"
+#include "../lib/UnlockGuard.h"
+#include "../lib/battle/BattleInfo.h"
+#include "../lib/serializer/BinaryDeserializer.h"
+#include "../lib/mapping/CMapService.h"
+#include "../lib/pathfinder/CGPathNode.h"
+#include "../lib/filesystem/Filesystem.h"
+#include "../lib/registerTypes/RegisterTypes.h"
+#include "../lib/serializer/Connection.h"
+
+#include <memory>
+#include <vcmi/events/EventBus.h>
+
+#if SCRIPTING_ENABLED
+#include "../lib/ScriptHandler.h"
+#endif
+
+#ifdef VCMI_ANDROID
+#include "lib/CAndroidVMHelper.h"
+
+#ifndef SINGLE_PROCESS_APP
+std::atomic_bool androidTestServerReadyFlag;
+#endif
+#endif
+
+ThreadSafeVector<int> CClient::waitingRequest;
+
+template<typename T> class CApplyOnCL;
+
+class CBaseForCLApply
+{
+public:
+	virtual void applyOnClAfter(CClient * cl, void * pack) const =0;
+	virtual void applyOnClBefore(CClient * cl, void * pack) const =0;
+	virtual ~CBaseForCLApply(){}
+
+	template<typename U> static CBaseForCLApply * getApplier(const U * t = nullptr)
+	{
+		return new CApplyOnCL<U>();
+	}
+};
+
+template<typename T> class CApplyOnCL : public CBaseForCLApply
+{
+public:
+	void applyOnClAfter(CClient * cl, void * pack) const override
+	{
+		T * ptr = static_cast<T *>(pack);
+		ApplyClientNetPackVisitor visitor(*cl, *cl->gameState());
+		ptr->visit(visitor);
+	}
+	void applyOnClBefore(CClient * cl, void * pack) const override
+	{
+		T * ptr = static_cast<T *>(pack);
+		ApplyFirstClientNetPackVisitor visitor(*cl, *cl->gameState());
+		ptr->visit(visitor);
+	}
+};
+
+template<> class CApplyOnCL<CPack>: public CBaseForCLApply
+{
+public:
+	void applyOnClAfter(CClient * cl, void * pack) const override
+	{
+		logGlobal->error("Cannot apply on CL plain CPack!");
+		assert(0);
+	}
+	void applyOnClBefore(CClient * cl, void * pack) const override
+	{
+		logGlobal->error("Cannot apply on CL plain CPack!");
+		assert(0);
+	}
+};
+
+CPlayerEnvironment::CPlayerEnvironment(PlayerColor player_, CClient * cl_, std::shared_ptr<CCallback> mainCallback_)
+	: player(player_),
+	cl(cl_),
+	mainCallback(mainCallback_)
+{
+
+}
+
+const Services * CPlayerEnvironment::services() const
+{
+	return VLC;
+}
+
+vstd::CLoggerBase * CPlayerEnvironment::logger() const
+{
+	return logGlobal;
+}
+
+events::EventBus * CPlayerEnvironment::eventBus() const
+{
+	return cl->eventBus();//always get actual value
+}
+
+const CPlayerEnvironment::BattleCb * CPlayerEnvironment::battle(const BattleID & battleID) const
+{
+	return mainCallback->getBattle(battleID).get();
+}
+
+const CPlayerEnvironment::GameCb * CPlayerEnvironment::game() const
+{
+	return mainCallback.get();
+}
+
+
+CClient::CClient()
+{
+	waitingRequest.clear();
+	applier = std::make_shared<CApplier<CBaseForCLApply>>();
+	registerTypesClientPacks1(*applier);
+	registerTypesClientPacks2(*applier);
+	IObjectInterface::cb = this;
+	gs = nullptr;
+}
+
+CClient::~CClient()
+{
+	IObjectInterface::cb = nullptr;
+}
+
+const Services * CClient::services() const
+{
+	return VLC; //todo: this should be CGI
+}
+
+const CClient::BattleCb * CClient::battle(const BattleID & battleID) const
+{
+	return nullptr; //todo?
+}
+
+const CClient::GameCb * CClient::game() const
+{
+	return this;
+}
+
+vstd::CLoggerBase * CClient::logger() const
+{
+	return logGlobal;
+}
+
+events::EventBus * CClient::eventBus() const
+{
+	return clientEventBus.get();
+}
+
+void CClient::newGame(CGameState * initializedGameState)
+{
+	CSH->th->update();
+	CMapService mapService;
+	gs = initializedGameState ? initializedGameState : new CGameState();
+	gs->preInit(VLC);
+	logNetwork->trace("\tCreating gamestate: %i", CSH->th->getDiff());
+	if(!initializedGameState)
+	{
+		Load::ProgressAccumulator progressTracking;
+		gs->init(&mapService, CSH->si.get(), progressTracking, settings["general"]["saveRandomMaps"].Bool());
+	}
+	logNetwork->trace("Initializing GameState (together): %d ms", CSH->th->getDiff());
+
+	initMapHandler();
+	reinitScripting();
+	initPlayerEnvironments();
+	initPlayerInterfaces();
+}
+
+void CClient::loadGame(CGameState * initializedGameState)
+{
+	logNetwork->info("Loading procedure started!");
+
+	logNetwork->info("Game state was transferred over network, loading.");
+	gs = initializedGameState;
+
+	gs->preInit(VLC);
+	gs->updateOnLoad(CSH->si.get());
+	logNetwork->info("Game loaded, initialize interfaces.");
+
+	initMapHandler();
+
+	reinitScripting();
+
+	initPlayerEnvironments();
+	
+	// Loading of client state - disabled for now
+	// Since client no longer writes or loads its own state and instead receives it from server
+	// client state serializer will serialize its own copies of all pointers, e.g. heroes/towns/objects
+	// and on deserialize will create its own copies (instead of using copies from state received from server)
+	// Potential solutions:
+	// 1) Use server gamestate to deserialize pointers, so any pointer to same object will point to server instance and not our copy
+	// 2) Remove all serialization of pointers with instance ID's and restore them on load (including AI deserializer code)
+	// 3) Completely remove client savegame and send all information, like hero paths and sleeping status to server (either in form of hero properties or as some generic "client options" message
+#ifdef BROKEN_CLIENT_STATE_SERIALIZATION_HAS_BEEN_FIXED
+	// try to deserialize client data including sleepingHeroes
+	try
+	{
+		boost::filesystem::path clientSaveName = *CResourceHandler::get()->getResourceName(ResourcePath(CSH->si->mapname, EResType::CLIENT_SAVEGAME));
+
+		if(clientSaveName.empty())
+			throw std::runtime_error("Cannot open client part of " + CSH->si->mapname);
+
+		std::unique_ptr<CLoadFile> loader (new CLoadFile(clientSaveName));
+		serialize(loader->serializer, loader->serializer.fileVersion);
+
+		logNetwork->info("Client data loaded.");
+	}
+	catch(std::exception & e)
+	{
+		logGlobal->info("Cannot load client data for game %s. Error: %s", CSH->si->mapname, e.what());
+	}
+#endif
+
+	initPlayerInterfaces();
+}
+
+void CClient::serialize(BinarySerializer & h, const int version)
+{
+	assert(h.saving);
+	ui8 players = static_cast<ui8>(playerint.size());
+	h & players;
+
+	for(auto i = playerint.begin(); i != playerint.end(); i++)
+	{
+		logGlobal->trace("Saving player %s interface", i->first);
+		assert(i->first == i->second->playerID);
+		h & i->first;
+		h & i->second->dllName;
+		h & i->second->human;
+		i->second->saveGame(h, version);
+	}
+
+#if SCRIPTING_ENABLED
+	if(version >= 800)
+	{
+		JsonNode scriptsState;
+		clientScripts->serializeState(h.saving, scriptsState);
+		h & scriptsState;
+	}
+#endif
+}
+
+void CClient::serialize(BinaryDeserializer & h, const int version)
+{
+	assert(!h.saving);
+	ui8 players = 0;
+	h & players;
+
+	for(int i = 0; i < players; i++)
+	{
+		std::string dllname;
+		PlayerColor pid;
+		bool isHuman = false;
+		auto prevInt = LOCPLINT;
+
+		h & pid;
+		h & dllname;
+		h & isHuman;
+		assert(dllname.length() == 0 || !isHuman);
+		if(pid == PlayerColor::NEUTRAL)
+		{
+			logGlobal->trace("Neutral battle interfaces are not serialized.");
+			continue;
+		}
+
+		logGlobal->trace("Loading player %s interface", pid);
+		std::shared_ptr<CGameInterface> nInt;
+		if(dllname.length())
+			nInt = CDynLibHandler::getNewAI(dllname);
+		else
+			nInt = std::make_shared<CPlayerInterface>(pid);
+
+		nInt->dllName = dllname;
+		nInt->human = isHuman;
+		nInt->playerID = pid;
+
+		bool shouldResetInterface = true;
+		// Client no longer handle this player at all
+		if(!vstd::contains(CSH->getAllClientPlayers(CSH->c->connectionID), pid))
+		{
+			logGlobal->trace("Player %s is not belong to this client. Destroying interface", pid);
+		}
+		else if(isHuman && !vstd::contains(CSH->getHumanColors(), pid))
+		{
+			logGlobal->trace("Player %s is no longer controlled by human. Destroying interface", pid);
+		}
+		else if(!isHuman && vstd::contains(CSH->getHumanColors(), pid))
+		{
+			logGlobal->trace("Player %s is no longer controlled by AI. Destroying interface", pid);
+		}
+		else
+		{
+			installNewPlayerInterface(nInt, pid);
+			shouldResetInterface = false;
+		}
+
+		// loadGame needs to be called after initGameInterface to load paths correctly
+		// initGameInterface is called in installNewPlayerInterface
+		nInt->loadGame(h, version);
+
+		if (shouldResetInterface)
+		{
+			nInt.reset();
+			LOCPLINT = prevInt;
+		}
+	}
+
+#if SCRIPTING_ENABLED
+	{
+		JsonNode scriptsState;
+		h & scriptsState;
+		clientScripts->serializeState(h.saving, scriptsState);
+	}
+#endif
+
+	logNetwork->trace("Loaded client part of save %d ms", CSH->th->getDiff());
+}
+
+void CClient::save(const std::string & fname)
+{
+	if(!gs->currentBattles.empty())
+	{
+		logNetwork->error("Game cannot be saved during battle!");
+		return;
+	}
+
+	SaveGame save_game(fname);
+	sendRequest(&save_game, PlayerColor::NEUTRAL);
+}
+
+void CClient::endGame()
+{
+#if SCRIPTING_ENABLED
+	clientScripts.reset();
+#endif
+
+	//suggest interfaces to finish their stuff (AI should interrupt any bg working threads)
+	for(auto & i : playerint)
+		i.second->finish();
+
+	GH.curInt = nullptr;
+	{
+		logNetwork->info("Ending current game!");
+		removeGUI();
+
+		vstd::clear_pointer(const_cast<CGameInfo *>(CGI)->mh);
+		vstd::clear_pointer(gs);
+
+		logNetwork->info("Deleted mapHandler and gameState.");
+	}
+
+	CPlayerInterface::battleInt.reset();
+	playerint.clear();
+	battleints.clear();
+	battleCallbacks.clear();
+	playerEnvironments.clear();
+	logNetwork->info("Deleted playerInts.");
+	logNetwork->info("Client stopped.");
+}
+
+void CClient::initMapHandler()
+{
+	// TODO: CMapHandler initialization can probably go somewhere else
+	// It's can't be before initialization of interfaces
+	// During loading CPlayerInterface from serialized state it's depend on MH
+	if(!settings["session"]["headless"].Bool())
+	{
+		const_cast<CGameInfo *>(CGI)->mh = new CMapHandler(gs->map);
+		logNetwork->trace("Creating mapHandler: %d ms", CSH->th->getDiff());
+	}
+
+	pathCache.clear();
+}
+
+void CClient::initPlayerEnvironments()
+{
+	playerEnvironments.clear();
+
+	auto allPlayers = CSH->getAllClientPlayers(CSH->c->connectionID);
+	bool hasHumanPlayer = false;
+	for(auto & color : allPlayers)
+	{
+		logNetwork->info("Preparing environment for player %s", color.toString());
+		playerEnvironments[color] = std::make_shared<CPlayerEnvironment>(color, this, std::make_shared<CCallback>(gs, color, this));
+		
+		if(!hasHumanPlayer && gs->players[color].isHuman())
+			hasHumanPlayer = true;
+	}
+
+	if(!hasHumanPlayer)
+	{
+		Settings session = settings.write["session"];
+		session["spectate"].Bool() = true;
+		session["spectate-skip-battle-result"].Bool() = true;
+		session["spectate-ignore-hero"].Bool() = true;
+	}
+	
+	if(settings["session"]["spectate"].Bool())
+	{
+		playerEnvironments[PlayerColor::SPECTATOR] = std::make_shared<CPlayerEnvironment>(PlayerColor::SPECTATOR, this, std::make_shared<CCallback>(gs, std::nullopt, this));
+	}
+}
+
+void CClient::initPlayerInterfaces()
+{
+	for(auto & playerInfo : gs->scenarioOps->playerInfos)
+	{
+		PlayerColor color = playerInfo.first;
+		if(!vstd::contains(CSH->getAllClientPlayers(CSH->c->connectionID), color))
+			continue;
+
+		if(!vstd::contains(playerint, color))
+		{
+			logNetwork->info("Preparing interface for player %s", color.toString());
+			if(playerInfo.second.isControlledByAI())
+			{
+				bool alliedToHuman = false;
+				for(auto & allyInfo : gs->scenarioOps->playerInfos)
+					if (gs->getPlayerTeam(allyInfo.first) == gs->getPlayerTeam(playerInfo.first) && allyInfo.second.isControlledByHuman())
+						alliedToHuman = true;
+
+				auto AiToGive = aiNameForPlayer(playerInfo.second, false, alliedToHuman);
+				logNetwork->info("Player %s will be lead by %s", color.toString(), AiToGive);
+				installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), color);
+			}
+			else
+			{
+				logNetwork->info("Player %s will be lead by human", color.toString());
+				installNewPlayerInterface(std::make_shared<CPlayerInterface>(color), color);
+			}
+		}
+	}
+
+	if(settings["session"]["spectate"].Bool())
+	{
+		installNewPlayerInterface(std::make_shared<CPlayerInterface>(PlayerColor::SPECTATOR), PlayerColor::SPECTATOR, true);
+	}
+
+	if(CSH->getAllClientPlayers(CSH->c->connectionID).count(PlayerColor::NEUTRAL))
+		installNewBattleInterface(CDynLibHandler::getNewBattleAI(settings["server"]["neutralAI"].String()), PlayerColor::NEUTRAL);
+
+	logNetwork->trace("Initialized player interfaces %d ms", CSH->th->getDiff());
+}
+
+std::string CClient::aiNameForPlayer(const PlayerSettings & ps, bool battleAI, bool alliedToHuman)
+{
+	if(ps.name.size())
+	{
+		const boost::filesystem::path aiPath = VCMIDirs::get().fullLibraryPath("AI", ps.name);
+		if(boost::filesystem::exists(aiPath))
+			return ps.name;
+	}
+
+	return aiNameForPlayer(battleAI, alliedToHuman);
+}
+
+std::string CClient::aiNameForPlayer(bool battleAI, bool alliedToHuman)
+{
+	const int sensibleAILimit = settings["session"]["oneGoodAI"].Bool() ? 1 : PlayerColor::PLAYER_LIMIT_I;
+	std::string goodAdventureAI = alliedToHuman ? settings["server"]["alliedAI"].String() : settings["server"]["playerAI"].String();
+	std::string goodBattleAI = settings["server"]["neutralAI"].String();
+	std::string goodAI = battleAI ? goodBattleAI : goodAdventureAI;
+	std::string badAI = battleAI ? "StupidAI" : "EmptyAI";
+
+	//TODO what about human players
+	if(battleints.size() >= sensibleAILimit)
+		return badAI;
+
+	return goodAI;
+}
+
+void CClient::installNewPlayerInterface(std::shared_ptr<CGameInterface> gameInterface, PlayerColor color, bool battlecb)
+{
+	playerint[color] = gameInterface;
+
+	logGlobal->trace("\tInitializing the interface for player %s", color.toString());
+	auto cb = std::make_shared<CCallback>(gs, color, this);
+	battleCallbacks[color] = cb;
+	gameInterface->initGameInterface(playerEnvironments.at(color), cb);
+
+	installNewBattleInterface(gameInterface, color, battlecb);
+}
+
+void CClient::installNewBattleInterface(std::shared_ptr<CBattleGameInterface> battleInterface, PlayerColor color, bool needCallback)
+{
+	battleints[color] = battleInterface;
+
+	if(needCallback)
+	{
+		logGlobal->trace("\tInitializing the battle interface for player %s", color.toString());
+		auto cbc = std::make_shared<CBattleCallback>(color, this);
+		battleCallbacks[color] = cbc;
+		battleInterface->initBattleInterface(playerEnvironments.at(color), cbc);
+	}
+}
+
+void CClient::handlePack(CPack * pack)
+{
+	CBaseForCLApply * apply = applier->getApplier(typeList.getTypeID(pack)); //find the applier
+	if(apply)
+	{
+		boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
+		apply->applyOnClBefore(this, pack);
+		logNetwork->trace("\tMade first apply on cl: %s", typeList.getTypeInfo(pack)->name());
+		gs->apply(pack);
+		logNetwork->trace("\tApplied on gs: %s", typeList.getTypeInfo(pack)->name());
+		apply->applyOnClAfter(this, pack);
+		logNetwork->trace("\tMade second apply on cl: %s", typeList.getTypeInfo(pack)->name());
+	}
+	else
+	{
+		logNetwork->error("Message %s cannot be applied, cannot find applier!", typeList.getTypeInfo(pack)->name());
+	}
+	delete pack;
+}
+
+int CClient::sendRequest(const CPackForServer * request, PlayerColor player)
+{
+	static ui32 requestCounter = 0;
+
+	ui32 requestID = requestCounter++;
+	logNetwork->trace("Sending a request \"%s\". It'll have an ID=%d.", typeid(*request).name(), requestID);
+
+	waitingRequest.pushBack(requestID);
+	request->requestID = requestID;
+	request->player = player;
+	CSH->c->sendPack(request);
+	if(vstd::contains(playerint, player))
+		playerint[player]->requestSent(request, requestID);
+
+	return requestID;
+}
+
+void CClient::battleStarted(const BattleInfo * info)
+{
+	for(auto & battleCb : battleCallbacks)
+	{
+		if(vstd::contains_if(info->sides, [&](const SideInBattle& side) {return side.color == battleCb.first; })
+			|| !battleCb.first.isValidPlayer())
+		{
+			battleCb.second->onBattleStarted(info);
+		}
+	}
+
+	std::shared_ptr<CPlayerInterface> att, def;
+	auto & leftSide = info->sides[0], & rightSide = info->sides[1];
+
+	//If quick combat is not, do not prepare interfaces for battleint
+	auto callBattleStart = [&](PlayerColor color, ui8 side)
+	{
+		if(vstd::contains(battleints, color))
+			battleints[color]->battleStart(info->battleID, leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side, info->replayAllowed);
+	};
+	
+	callBattleStart(leftSide.color, 0);
+	callBattleStart(rightSide.color, 1);
+	callBattleStart(PlayerColor::UNFLAGGABLE, 1);
+	if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool())
+		callBattleStart(PlayerColor::SPECTATOR, 1);
+	
+	if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human)
+		att = std::dynamic_pointer_cast<CPlayerInterface>(playerint[leftSide.color]);
+
+	if(vstd::contains(playerint, rightSide.color) && playerint[rightSide.color]->human)
+		def = std::dynamic_pointer_cast<CPlayerInterface>(playerint[rightSide.color]);
+	
+	//Remove player interfaces for auto battle (quickCombat option)
+	if(att && att->isAutoFightOn)
+	{
+		if (att->cb->getBattle(info->battleID)->battleGetTacticDist())
+		{
+			auto side = att->cb->getBattle(info->battleID)->playerToSide(att->playerID);
+			auto action = BattleAction::makeEndOFTacticPhase(*side);
+			att->cb->battleMakeTacticAction(info->battleID, action);
+		}
+
+		att.reset();
+		def.reset();
+	}
+
+	if(!settings["session"]["headless"].Bool())
+	{
+		if(att || def)
+		{
+			CPlayerInterface::battleInt = std::make_shared<BattleInterface>(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def);
+		}
+		else if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool())
+		{
+			//TODO: This certainly need improvement
+			auto spectratorInt = std::dynamic_pointer_cast<CPlayerInterface>(playerint[PlayerColor::SPECTATOR]);
+			spectratorInt->cb->onBattleStarted(info);
+			CPlayerInterface::battleInt = std::make_shared<BattleInterface>(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def, spectratorInt);
+		}
+	}
+
+	if(info->tacticDistance)
+	{
+		auto tacticianColor = info->sides[info->tacticsSide].color;
+
+		if (vstd::contains(battleints, tacticianColor))
+			battleints[tacticianColor]->yourTacticPhase(info->battleID, info->tacticDistance);
+	}
+}
+
+void CClient::battleFinished(const BattleID & battleID)
+{
+	for(auto & side : gs->getBattle(battleID)->sides)
+		if(battleCallbacks.count(side.color))
+			battleCallbacks[side.color]->onBattleEnded(battleID);
+
+	if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool())
+		battleCallbacks[PlayerColor::SPECTATOR]->onBattleEnded(battleID);
+}
+
+void CClient::startPlayerBattleAction(const BattleID & battleID, PlayerColor color)
+{
+	auto battleint = battleints.at(color);
+
+	if (!battleint->human)
+	{
+		// we want to avoid locking gamestate and causing UI to freeze while AI is making turn
+		auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex);
+		battleint->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false));
+	}
+	else
+	{
+		battleint->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false));
+	}
+}
+
+void CClient::invalidatePaths()
+{
+	boost::unique_lock<boost::mutex> pathLock(pathCacheMutex);
+	pathCache.clear();
+}
+
+std::shared_ptr<const CPathsInfo> CClient::getPathsInfo(const CGHeroInstance * h)
+{
+	assert(h);
+	boost::unique_lock<boost::mutex> pathLock(pathCacheMutex);
+
+	auto iter = pathCache.find(h);
+
+	if(iter == std::end(pathCache))
+	{
+		std::shared_ptr<CPathsInfo> paths = std::make_shared<CPathsInfo>(getMapSize(), h);
+
+		gs->calculatePaths(h, *paths.get());
+
+		pathCache[h] = paths;
+		return paths;
+	}
+	else
+	{
+		return iter->second;
+	}
+}
+
+#if SCRIPTING_ENABLED
+scripting::Pool * CClient::getGlobalContextPool() const
+{
+	return clientScripts.get();
+}
+#endif
+
+void CClient::reinitScripting()
+{
+	clientEventBus = std::make_unique<events::EventBus>();
+#if SCRIPTING_ENABLED
+	clientScripts.reset(new scripting::PoolImpl(this));
+#endif
+}
+
+void CClient::removeGUI()
+{
+	// CClient::endGame
+	GH.curInt = nullptr;
+	GH.windows().clear();
+	adventureInt.reset();
+	logGlobal->info("Removed GUI.");
+
+	LOCPLINT = nullptr;
+}
+
+#ifdef VCMI_ANDROID
+#ifndef SINGLE_PROCESS_APP
+extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_notifyServerClosed(JNIEnv * env, jclass cls)
+{
+	logNetwork->info("Received server closed signal");
+	if (CSH) {
+		CSH->campaignServerRestartLock.setn(false);
+	}
+}
+
+extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_notifyServerReady(JNIEnv * env, jclass cls)
+{
+	logNetwork->info("Received server ready signal");
+	androidTestServerReadyFlag.store(true);
+}
+#endif
+
+extern "C" JNIEXPORT jboolean JNICALL Java_eu_vcmi_vcmi_NativeMethods_tryToSaveTheGame(JNIEnv * env, jclass cls)
+{
+	logGlobal->info("Received emergency save game request");
+	if(!LOCPLINT || !LOCPLINT->cb)
+	{
+		return false;
+	}
+
+	LOCPLINT->cb->save("Saves/_Android_Autosave");
+	return true;
+}
+#endif

+ 240 - 240
client/Client.h

@@ -1,240 +1,240 @@
-/*
- * Client.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 <memory>
-#include <vcmi/Environment.h>
-
-#include "../lib/IGameCallback.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-struct CPack;
-struct CPackForServer;
-class IBattleEventsReceiver;
-class CBattleGameInterface;
-class CGameInterface;
-class BinaryDeserializer;
-class BinarySerializer;
-class BattleAction;
-class BattleInfo;
-
-template<typename T> class CApplier;
-
-#if SCRIPTING_ENABLED
-namespace scripting
-{
-	class PoolImpl;
-}
-#endif
-
-namespace events
-{
-	class EventBus;
-}
-
-VCMI_LIB_NAMESPACE_END
-
-class CBattleCallback;
-class CCallback;
-class CClient;
-class CBaseForCLApply;
-
-namespace boost { class thread; }
-
-template<typename T>
-class ThreadSafeVector
-{
-	using TLock = boost::unique_lock<boost::mutex>;
-	std::vector<T> items;
-	boost::mutex mx;
-	boost::condition_variable cond;
-
-public:
-	void clear()
-	{
-		TLock lock(mx);
-		items.clear();
-		cond.notify_all();
-	}
-
-	void pushBack(const T & item)
-	{
-		TLock lock(mx);
-		items.push_back(item);
-		cond.notify_all();
-	}
-
-	void waitWhileContains(const T & item)
-	{
-		TLock lock(mx);
-		while(vstd::contains(items, item))
-			cond.wait(lock);
-	}
-
-	bool tryRemovingElement(const T & item) //returns false if element was not present
-	{
-		TLock lock(mx);
-		auto itr = vstd::find(items, item);
-		if(itr == items.end()) //not in container
-		{
-			return false;
-		}
-
-		items.erase(itr);
-		cond.notify_all();
-		return true;
-	}
-};
-
-class CPlayerEnvironment : public Environment
-{
-public:
-	PlayerColor player;
-	CClient * cl;
-	std::shared_ptr<CCallback> mainCallback;
-
-	CPlayerEnvironment(PlayerColor player_, CClient * cl_, std::shared_ptr<CCallback> mainCallback_);
-	const Services * services() const override;
-	vstd::CLoggerBase * logger() const override;
-	events::EventBus * eventBus() const override;
-	const BattleCb * battle(const BattleID & battle) const override;
-	const GameCb * game() const override;
-};
-
-/// Class which handles client - server logic
-class CClient : public IGameCallback, public Environment
-{
-public:
-	std::map<PlayerColor, std::shared_ptr<CGameInterface>> playerint;
-	std::map<PlayerColor, std::shared_ptr<CBattleGameInterface>> battleints;
-
-	std::map<PlayerColor, std::vector<std::shared_ptr<IBattleEventsReceiver>>> additionalBattleInts;
-
-	std::unique_ptr<BattleAction> currentBattleAction;
-
-	CClient();
-	~CClient();
-
-	const Services * services() const override;
-	const BattleCb * battle(const BattleID & battle) const override;
-	const GameCb * game() const override;
-	vstd::CLoggerBase * logger() const override;
-	events::EventBus * eventBus() const override;
-
-	void newGame(CGameState * gameState);
-	void loadGame(CGameState * gameState);
-	void serialize(BinarySerializer & h, const int version);
-	void serialize(BinaryDeserializer & h, const int version);
-
-	void save(const std::string & fname);
-	void endGame();
-
-	void initMapHandler();
-	void initPlayerEnvironments();
-	void initPlayerInterfaces();
-	std::string aiNameForPlayer(const PlayerSettings & ps, bool battleAI, bool alliedToHuman); //empty means no AI -> human
-	std::string aiNameForPlayer(bool battleAI, bool alliedToHuman);
-	void installNewPlayerInterface(std::shared_ptr<CGameInterface> gameInterface, PlayerColor color, bool battlecb = false);
-	void installNewBattleInterface(std::shared_ptr<CBattleGameInterface> battleInterface, PlayerColor color, bool needCallback = true);
-
-	static ThreadSafeVector<int> waitingRequest; //FIXME: make this normal field (need to join all threads before client destruction)
-
-	void handlePack(CPack * pack); //applies the given pack and deletes it
-	int sendRequest(const CPackForServer * request, PlayerColor player); //returns ID given to that request
-
-	void battleStarted(const BattleInfo * info);
-	void battleFinished(const BattleID & battleID);
-	void startPlayerBattleAction(const BattleID & battleID, PlayerColor color);
-
-	void invalidatePaths();
-	std::shared_ptr<const CPathsInfo> getPathsInfo(const CGHeroInstance * h);
-
-	friend class CCallback; //handling players actions
-	friend class CBattleCallback; //handling players actions
-
-	void changeSpells(const CGHeroInstance * hero, bool give, const std::set<SpellID> & spells) override {};
-	bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override {return false;};
-	void createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype ) override {};
-	void setOwner(const CGObjectInstance * obj, PlayerColor owner) override {};
-	void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs = false) override {};
-	void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs = false) override {};
-
-	void showBlockingDialog(BlockingDialog * iw) override {};
-	void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override {};
-	void showTeleportDialog(TeleportDialog * iw) override {};
-	void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) override {};
-	void giveResource(PlayerColor player, GameResID which, int val) override {};
-	virtual void giveResources(PlayerColor player, TResources resources) override {};
-
-	void giveCreatures(const CArmedInstance * objid, const CGHeroInstance * h, const CCreatureSet & creatures, bool remove) override {};
-	void takeCreatures(ObjectInstanceID objid, const std::vector<CStackBasicDescriptor> & creatures) override {};
-	bool changeStackType(const StackLocation & sl, const CCreature * c) override {return false;};
-	bool changeStackCount(const StackLocation & sl, TQuantity count, bool absoluteValue = false) override {return false;};
-	bool insertNewStack(const StackLocation & sl, const CCreature * c, TQuantity count) override {return false;};
-	bool eraseStack(const StackLocation & sl, bool forceRemoval = false) override {return false;};
-	bool swapStacks(const StackLocation & sl1, const StackLocation & sl2) override {return false;}
-	bool addToSlot(const StackLocation & sl, const CCreature * c, TQuantity count) override {return false;}
-	void tryJoiningArmy(const CArmedInstance * src, const CArmedInstance * dst, bool removeObjWhenFinished, bool allowMerging) override {}
-	bool moveStack(const StackLocation & src, const StackLocation & dst, TQuantity count = -1) override {return false;}
-
-	void removeAfterVisit(const CGObjectInstance * object) override {};
-	bool swapGarrisonOnSiege(ObjectInstanceID tid) override {return false;};
-	bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) override {return false;}
-	bool giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) override {return false;}
-	void putArtifact(const ArtifactLocation & al, const CArtifactInstance * a) override {};
-	void removeArtifact(const ArtifactLocation & al) override {};
-	bool moveArtifact(const ArtifactLocation & al1, const ArtifactLocation & al2) override {return false;};
-
-	void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {};
-	void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override {};
-	void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {};
-	void startBattlePrimary(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool creatureBank = false, const CGTownInstance * town = nullptr) override {}; //use hero=nullptr for no hero
-	void startBattleI(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used
-	void startBattleI(const CArmedInstance * army1, const CArmedInstance * army2, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle
-	bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;};
-	void giveHeroBonus(GiveBonus * bonus) override {};
-	void setMovePoints(SetMovePoints * smp) override {};
-	void setManaPoints(ObjectInstanceID hid, int val) override {};
-	void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override {};
-	void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {};
-	void sendAndApply(CPackForClient * pack) override {};
-	void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {};
-	void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {};
-
-	void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override {}
-	void changeFogOfWar(std::unordered_set<int3> & tiles, PlayerColor player, ETileVisibility mode) override {}
-
-	void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override {}
-
-	void showInfoDialog(InfoWindow * iw) override {};
-	void showInfoDialog(const std::string & msg, PlayerColor player) override {};
-	void removeGUI();
-
-#if SCRIPTING_ENABLED
-	scripting::Pool * getGlobalContextPool() const override;
-#endif
-
-private:
-	std::map<PlayerColor, std::shared_ptr<CBattleCallback>> battleCallbacks; //callbacks given to player interfaces
-	std::map<PlayerColor, std::shared_ptr<CPlayerEnvironment>> playerEnvironments;
-
-#if SCRIPTING_ENABLED
-	std::shared_ptr<scripting::PoolImpl> clientScripts;
-#endif
-	std::unique_ptr<events::EventBus> clientEventBus;
-
-	std::shared_ptr<CApplier<CBaseForCLApply>> applier;
-
-	mutable boost::mutex pathCacheMutex;
-	std::map<const CGHeroInstance *, std::shared_ptr<CPathsInfo>> pathCache;
-
-	void reinitScripting();
-};
+/*
+ * Client.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 <memory>
+#include <vcmi/Environment.h>
+
+#include "../lib/IGameCallback.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+struct CPack;
+struct CPackForServer;
+class IBattleEventsReceiver;
+class CBattleGameInterface;
+class CGameInterface;
+class BinaryDeserializer;
+class BinarySerializer;
+class BattleAction;
+class BattleInfo;
+
+template<typename T> class CApplier;
+
+#if SCRIPTING_ENABLED
+namespace scripting
+{
+	class PoolImpl;
+}
+#endif
+
+namespace events
+{
+	class EventBus;
+}
+
+VCMI_LIB_NAMESPACE_END
+
+class CBattleCallback;
+class CCallback;
+class CClient;
+class CBaseForCLApply;
+
+namespace boost { class thread; }
+
+template<typename T>
+class ThreadSafeVector
+{
+	using TLock = boost::unique_lock<boost::mutex>;
+	std::vector<T> items;
+	boost::mutex mx;
+	boost::condition_variable cond;
+
+public:
+	void clear()
+	{
+		TLock lock(mx);
+		items.clear();
+		cond.notify_all();
+	}
+
+	void pushBack(const T & item)
+	{
+		TLock lock(mx);
+		items.push_back(item);
+		cond.notify_all();
+	}
+
+	void waitWhileContains(const T & item)
+	{
+		TLock lock(mx);
+		while(vstd::contains(items, item))
+			cond.wait(lock);
+	}
+
+	bool tryRemovingElement(const T & item) //returns false if element was not present
+	{
+		TLock lock(mx);
+		auto itr = vstd::find(items, item);
+		if(itr == items.end()) //not in container
+		{
+			return false;
+		}
+
+		items.erase(itr);
+		cond.notify_all();
+		return true;
+	}
+};
+
+class CPlayerEnvironment : public Environment
+{
+public:
+	PlayerColor player;
+	CClient * cl;
+	std::shared_ptr<CCallback> mainCallback;
+
+	CPlayerEnvironment(PlayerColor player_, CClient * cl_, std::shared_ptr<CCallback> mainCallback_);
+	const Services * services() const override;
+	vstd::CLoggerBase * logger() const override;
+	events::EventBus * eventBus() const override;
+	const BattleCb * battle(const BattleID & battle) const override;
+	const GameCb * game() const override;
+};
+
+/// Class which handles client - server logic
+class CClient : public IGameCallback, public Environment
+{
+public:
+	std::map<PlayerColor, std::shared_ptr<CGameInterface>> playerint;
+	std::map<PlayerColor, std::shared_ptr<CBattleGameInterface>> battleints;
+
+	std::map<PlayerColor, std::vector<std::shared_ptr<IBattleEventsReceiver>>> additionalBattleInts;
+
+	std::unique_ptr<BattleAction> currentBattleAction;
+
+	CClient();
+	~CClient();
+
+	const Services * services() const override;
+	const BattleCb * battle(const BattleID & battle) const override;
+	const GameCb * game() const override;
+	vstd::CLoggerBase * logger() const override;
+	events::EventBus * eventBus() const override;
+
+	void newGame(CGameState * gameState);
+	void loadGame(CGameState * gameState);
+	void serialize(BinarySerializer & h, const int version);
+	void serialize(BinaryDeserializer & h, const int version);
+
+	void save(const std::string & fname);
+	void endGame();
+
+	void initMapHandler();
+	void initPlayerEnvironments();
+	void initPlayerInterfaces();
+	std::string aiNameForPlayer(const PlayerSettings & ps, bool battleAI, bool alliedToHuman); //empty means no AI -> human
+	std::string aiNameForPlayer(bool battleAI, bool alliedToHuman);
+	void installNewPlayerInterface(std::shared_ptr<CGameInterface> gameInterface, PlayerColor color, bool battlecb = false);
+	void installNewBattleInterface(std::shared_ptr<CBattleGameInterface> battleInterface, PlayerColor color, bool needCallback = true);
+
+	static ThreadSafeVector<int> waitingRequest; //FIXME: make this normal field (need to join all threads before client destruction)
+
+	void handlePack(CPack * pack); //applies the given pack and deletes it
+	int sendRequest(const CPackForServer * request, PlayerColor player); //returns ID given to that request
+
+	void battleStarted(const BattleInfo * info);
+	void battleFinished(const BattleID & battleID);
+	void startPlayerBattleAction(const BattleID & battleID, PlayerColor color);
+
+	void invalidatePaths();
+	std::shared_ptr<const CPathsInfo> getPathsInfo(const CGHeroInstance * h);
+
+	friend class CCallback; //handling players actions
+	friend class CBattleCallback; //handling players actions
+
+	void changeSpells(const CGHeroInstance * hero, bool give, const std::set<SpellID> & spells) override {};
+	bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override {return false;};
+	void createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype ) override {};
+	void setOwner(const CGObjectInstance * obj, PlayerColor owner) override {};
+	void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs = false) override {};
+	void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs = false) override {};
+
+	void showBlockingDialog(BlockingDialog * iw) override {};
+	void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override {};
+	void showTeleportDialog(TeleportDialog * iw) override {};
+	void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) override {};
+	void giveResource(PlayerColor player, GameResID which, int val) override {};
+	virtual void giveResources(PlayerColor player, TResources resources) override {};
+
+	void giveCreatures(const CArmedInstance * objid, const CGHeroInstance * h, const CCreatureSet & creatures, bool remove) override {};
+	void takeCreatures(ObjectInstanceID objid, const std::vector<CStackBasicDescriptor> & creatures) override {};
+	bool changeStackType(const StackLocation & sl, const CCreature * c) override {return false;};
+	bool changeStackCount(const StackLocation & sl, TQuantity count, bool absoluteValue = false) override {return false;};
+	bool insertNewStack(const StackLocation & sl, const CCreature * c, TQuantity count) override {return false;};
+	bool eraseStack(const StackLocation & sl, bool forceRemoval = false) override {return false;};
+	bool swapStacks(const StackLocation & sl1, const StackLocation & sl2) override {return false;}
+	bool addToSlot(const StackLocation & sl, const CCreature * c, TQuantity count) override {return false;}
+	void tryJoiningArmy(const CArmedInstance * src, const CArmedInstance * dst, bool removeObjWhenFinished, bool allowMerging) override {}
+	bool moveStack(const StackLocation & src, const StackLocation & dst, TQuantity count = -1) override {return false;}
+
+	void removeAfterVisit(const CGObjectInstance * object) override {};
+	bool swapGarrisonOnSiege(ObjectInstanceID tid) override {return false;};
+	bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) override {return false;}
+	bool giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) override {return false;}
+	void putArtifact(const ArtifactLocation & al, const CArtifactInstance * a) override {};
+	void removeArtifact(const ArtifactLocation & al) override {};
+	bool moveArtifact(const ArtifactLocation & al1, const ArtifactLocation & al2) override {return false;};
+
+	void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {};
+	void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override {};
+	void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {};
+	void startBattlePrimary(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool creatureBank = false, const CGTownInstance * town = nullptr) override {}; //use hero=nullptr for no hero
+	void startBattleI(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used
+	void startBattleI(const CArmedInstance * army1, const CArmedInstance * army2, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle
+	bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;};
+	void giveHeroBonus(GiveBonus * bonus) override {};
+	void setMovePoints(SetMovePoints * smp) override {};
+	void setManaPoints(ObjectInstanceID hid, int val) override {};
+	void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override {};
+	void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {};
+	void sendAndApply(CPackForClient * pack) override {};
+	void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {};
+	void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {};
+
+	void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override {}
+	void changeFogOfWar(std::unordered_set<int3> & tiles, PlayerColor player, ETileVisibility mode) override {}
+
+	void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override {}
+
+	void showInfoDialog(InfoWindow * iw) override {};
+	void showInfoDialog(const std::string & msg, PlayerColor player) override {};
+	void removeGUI();
+
+#if SCRIPTING_ENABLED
+	scripting::Pool * getGlobalContextPool() const override;
+#endif
+
+private:
+	std::map<PlayerColor, std::shared_ptr<CBattleCallback>> battleCallbacks; //callbacks given to player interfaces
+	std::map<PlayerColor, std::shared_ptr<CPlayerEnvironment>> playerEnvironments;
+
+#if SCRIPTING_ENABLED
+	std::shared_ptr<scripting::PoolImpl> clientScripts;
+#endif
+	std::unique_ptr<events::EventBus> clientEventBus;
+
+	std::shared_ptr<CApplier<CBaseForCLApply>> applier;
+
+	mutable boost::mutex pathCacheMutex;
+	std::map<const CGHeroInstance *, std::shared_ptr<CPathsInfo>> pathCache;
+
+	void reinitScripting();
+};

+ 1042 - 1042
client/NetPacksClient.cpp

@@ -1,1042 +1,1042 @@
-/*
- * NetPacksClient.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 "ClientNetPackVisitors.h"
-
-#include "Client.h"
-#include "CPlayerInterface.h"
-#include "CGameInfo.h"
-#include "windows/GUIClasses.h"
-#include "mapView/mapHandler.h"
-#include "adventureMap/CInGameConsole.h"
-#include "battle/BattleInterface.h"
-#include "battle/BattleWindow.h"
-#include "gui/CGuiHandler.h"
-#include "gui/WindowHandler.h"
-#include "widgets/MiscWidgets.h"
-#include "CMT.h"
-#include "CServerHandler.h"
-
-#include "../CCallback.h"
-#include "../lib/filesystem/Filesystem.h"
-#include "../lib/filesystem/FileInfo.h"
-#include "../lib/serializer/Connection.h"
-#include "../lib/serializer/BinarySerializer.h"
-#include "../lib/CGeneralTextHandler.h"
-#include "../lib/CHeroHandler.h"
-#include "../lib/VCMI_Lib.h"
-#include "../lib/mapping/CMap.h"
-#include "../lib/VCMIDirs.h"
-#include "../lib/spells/CSpellHandler.h"
-#include "../lib/CSoundBase.h"
-#include "../lib/StartInfo.h"
-#include "../lib/CConfigHandler.h"
-#include "../lib/mapObjects/CGMarket.h"
-#include "../lib/gameState/CGameState.h"
-#include "../lib/CStack.h"
-#include "../lib/battle/BattleInfo.h"
-#include "../lib/GameConstants.h"
-#include "../lib/CPlayerState.h"
-
-// TODO: as Tow suggested these template should all be part of CClient
-// This will require rework spectator interface properly though
-
-template<typename T, typename ... Args, typename ... Args2>
-bool callOnlyThatInterface(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args)
-{
-	if(vstd::contains(cl.playerint, player))
-	{
-		((*cl.playerint[player]).*ptr)(std::forward<Args2>(args)...);
-		return true;
-	}
-	return false;
-}
-
-template<typename T, typename ... Args, typename ... Args2>
-bool callInterfaceIfPresent(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args)
-{
-	bool called = callOnlyThatInterface(cl, player, ptr, std::forward<Args2>(args)...);
-	return called;
-}
-
-template<typename T, typename ... Args, typename ... Args2>
-void callOnlyThatBattleInterface(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args)
-{
-	if(vstd::contains(cl.battleints,player))
-		((*cl.battleints[player]).*ptr)(std::forward<Args2>(args)...);
-
-	if(cl.additionalBattleInts.count(player))
-	{
-		for(auto bInt : cl.additionalBattleInts[player])
-			((*bInt).*ptr)(std::forward<Args2>(args)...);
-	}
-}
-
-template<typename T, typename ... Args, typename ... Args2>
-void callBattleInterfaceIfPresent(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args)
-{
-	callOnlyThatInterface(cl, player, ptr, std::forward<Args2>(args)...);
-}
-
-//calls all normal interfaces and privileged ones, playerints may be updated when iterating over it, so we need a copy
-template<typename T, typename ... Args, typename ... Args2>
-void callAllInterfaces(CClient & cl, void (T::*ptr)(Args...), Args2 && ...args)
-{
-	for(auto pInt : cl.playerint)
-		((*pInt.second).*ptr)(std::forward<Args2>(args)...);
-}
-
-//calls all normal interfaces and privileged ones, playerints may be updated when iterating over it, so we need a copy
-template<typename T, typename ... Args, typename ... Args2>
-void callBattleInterfaceIfPresentForBothSides(CClient & cl, const BattleID & battleID, void (T::*ptr)(Args...), Args2 && ...args)
-{
-	assert(cl.gameState()->getBattle(battleID));
-
-	if (!cl.gameState()->getBattle(battleID))
-	{
-		logGlobal->error("Attempt to call battle interface without ongoing battle!");
-		return;
-	}
-
-	callOnlyThatBattleInterface(cl, cl.gameState()->getBattle(battleID)->sides[0].color, ptr, std::forward<Args2>(args)...);
-	callOnlyThatBattleInterface(cl, cl.gameState()->getBattle(battleID)->sides[1].color, ptr, std::forward<Args2>(args)...);
-	if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool() && LOCPLINT->battleInt)
-	{
-		callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, ptr, std::forward<Args2>(args)...);
-	}
-}
-
-void ApplyClientNetPackVisitor::visitSetResources(SetResources & pack)
-{
-	//todo: inform on actual resource set transfered
-	callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::receivedResource);
-}
-
-void ApplyClientNetPackVisitor::visitSetPrimSkill(SetPrimSkill & pack)
-{
-	const CGHeroInstance * h = cl.getHero(pack.id);
-	if(!h)
-	{
-		logNetwork->error("Cannot find hero with pack.id %d", pack.id.getNum());
-		return;
-	}
-	callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroPrimarySkillChanged, h, pack.which, pack.val);
-}
-
-void ApplyClientNetPackVisitor::visitSetSecSkill(SetSecSkill & pack)
-{
-	const CGHeroInstance *h = cl.getHero(pack.id);
-	if(!h)
-	{
-		logNetwork->error("Cannot find hero with pack.id %d", pack.id.getNum());
-		return;
-	}
-	callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroSecondarySkillChanged, h, pack.which, pack.val);
-}
-
-void ApplyClientNetPackVisitor::visitHeroVisitCastle(HeroVisitCastle & pack)
-{
-	const CGHeroInstance *h = cl.getHero(pack.hid);
-	
-	if(pack.start())
-	{
-		callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroVisitsTown, h, gs.getTown(pack.tid));
-	}
-}
-
-void ApplyClientNetPackVisitor::visitSetMana(SetMana & pack)
-{
-	const CGHeroInstance *h = cl.getHero(pack.hid);
-	callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroManaPointsChanged, h);
-
-	for (auto window : GH.windows().findWindows<BattleWindow>())
-		window->heroManaPointsChanged(h);
-}
-
-void ApplyClientNetPackVisitor::visitSetMovePoints(SetMovePoints & pack)
-{
-	const CGHeroInstance *h = cl.getHero(pack.hid);
-	cl.invalidatePaths();
-	callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroMovePointsChanged, h);
-}
-
-void ApplyClientNetPackVisitor::visitFoWChange(FoWChange & pack)
-{
-	for(auto &i : cl.playerint)
-	{
-		if(cl.getPlayerRelations(i.first, pack.player) == PlayerRelations::SAME_PLAYER && pack.waitForDialogs && LOCPLINT == i.second.get())
-		{
-			LOCPLINT->waitWhileDialog();
-		}
-		if(cl.getPlayerRelations(i.first, pack.player) != PlayerRelations::ENEMIES)
-		{
-			if(pack.mode == ETileVisibility::REVEALED)
-				i.second->tileRevealed(pack.tiles);
-			else
-				i.second->tileHidden(pack.tiles);
-		}
-	}
-	cl.invalidatePaths();
-}
-
-static void dispatchGarrisonChange(CClient & cl, ObjectInstanceID army1, ObjectInstanceID army2)
-{
-	auto obj1 = cl.getObj(army1);
-	if(!obj1)
-	{
-		logNetwork->error("Cannot find army with pack.id %d", army1.getNum());
-		return;
-	}
-
-	callInterfaceIfPresent(cl, obj1->tempOwner, &IGameEventsReceiver::garrisonsChanged, army1, army2);
-
-	if(army2 != ObjectInstanceID() && army2 != army1)
-	{
-		auto obj2 = cl.getObj(army2);
-		if(!obj2)
-		{
-			logNetwork->error("Cannot find army with pack.id %d", army2.getNum());
-			return;
-		}
-
-		if(obj1->tempOwner != obj2->tempOwner)
-			callInterfaceIfPresent(cl, obj2->tempOwner, &IGameEventsReceiver::garrisonsChanged, army1, army2);
-	}
-}
-
-void ApplyClientNetPackVisitor::visitChangeStackCount(ChangeStackCount & pack)
-{
-	dispatchGarrisonChange(cl, pack.army, ObjectInstanceID());
-}
-
-void ApplyClientNetPackVisitor::visitSetStackType(SetStackType & pack)
-{
-	dispatchGarrisonChange(cl, pack.army, ObjectInstanceID());
-}
-
-void ApplyClientNetPackVisitor::visitEraseStack(EraseStack & pack)
-{
-	dispatchGarrisonChange(cl, pack.army, ObjectInstanceID());
-}
-
-void ApplyClientNetPackVisitor::visitSwapStacks(SwapStacks & pack)
-{
-	dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy);
-}
-
-void ApplyClientNetPackVisitor::visitInsertNewStack(InsertNewStack & pack)
-{
-	dispatchGarrisonChange(cl, pack.army, ObjectInstanceID());
-}
-
-void ApplyClientNetPackVisitor::visitRebalanceStacks(RebalanceStacks & pack)
-{
-	dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy);
-}
-
-void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & pack)
-{
-	if(!pack.moves.empty())
-	{
-		auto destArmy = pack.moves[0].srcArmy == pack.moves[0].dstArmy
-			? ObjectInstanceID()
-			: pack.moves[0].dstArmy;
-		dispatchGarrisonChange(cl, pack.moves[0].srcArmy, destArmy);
-	}
-}
-
-void ApplyClientNetPackVisitor::visitBulkSmartRebalanceStacks(BulkSmartRebalanceStacks & pack)
-{
-	if(!pack.moves.empty())
-	{
-		assert(pack.moves[0].srcArmy == pack.moves[0].dstArmy);
-		dispatchGarrisonChange(cl, pack.moves[0].srcArmy, ObjectInstanceID());
-	}
-	else if(!pack.changes.empty())
-	{
-		dispatchGarrisonChange(cl, pack.changes[0].army, ObjectInstanceID());
-	}
-}
-
-void ApplyClientNetPackVisitor::visitPutArtifact(PutArtifact & pack)
-{
-	callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactPut, pack.al);
-	if(pack.askAssemble)
-		callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::askToAssembleArtifact, pack.al);
-}
-
-void ApplyClientNetPackVisitor::visitEraseArtifact(EraseArtifact & pack)
-{
-	callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactRemoved, pack.al);
-}
-
-void ApplyClientNetPackVisitor::visitMoveArtifact(MoveArtifact & pack)
-{
-	auto moveArtifact = [this, &pack](PlayerColor player) -> void
-	{
-		callInterfaceIfPresent(cl, player, &IGameEventsReceiver::artifactMoved, pack.src, pack.dst);
-		if(pack.askAssemble)
-			callInterfaceIfPresent(cl, player, &IGameEventsReceiver::askToAssembleArtifact, pack.dst);
-	};
-
-	moveArtifact(pack.src.owningPlayer());
-	if(pack.src.owningPlayer() != pack.dst.owningPlayer())
-		moveArtifact(pack.dst.owningPlayer());
-
-	cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings
-}
-
-void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack)
-{
-	auto applyMove = [this, &pack](std::vector<BulkMoveArtifacts::LinkedSlots> & artsPack) -> void
-	{
-		for(auto & slotToMove : artsPack)
-		{
-			auto srcLoc = ArtifactLocation(pack.srcArtHolder, slotToMove.srcPos);
-			auto dstLoc = ArtifactLocation(pack.dstArtHolder, slotToMove.dstPos);
-			MoveArtifact ma(&srcLoc, &dstLoc, false);
-			visitMoveArtifact(ma);
-		}
-	};
-
-	auto srcOwner = std::get<ConstTransitivePtr<CGHeroInstance>>(pack.srcArtHolder)->tempOwner;
-	auto dstOwner = std::get<ConstTransitivePtr<CGHeroInstance>>(pack.dstArtHolder)->tempOwner;
-
-	// Begin a session of bulk movement of arts. It is not necessary but useful for the client optimization.
-	callInterfaceIfPresent(cl, srcOwner, &IGameEventsReceiver::bulkArtMovementStart, pack.artsPack0.size() + pack.artsPack1.size());
-	if(srcOwner != dstOwner)
-		callInterfaceIfPresent(cl, dstOwner, &IGameEventsReceiver::bulkArtMovementStart, pack.artsPack0.size() + pack.artsPack1.size());
-
-	applyMove(pack.artsPack0);
-	if(pack.swap)
-		applyMove(pack.artsPack1);
-}
-
-void ApplyClientNetPackVisitor::visitAssembledArtifact(AssembledArtifact & pack)
-{
-	callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactAssembled, pack.al);
-
-	cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings
-}
-
-void ApplyClientNetPackVisitor::visitDisassembledArtifact(DisassembledArtifact & pack)
-{
-	callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactDisassembled, pack.al);
-
-	cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings
-}
-
-void ApplyClientNetPackVisitor::visitHeroVisit(HeroVisit & pack)
-{
-	auto hero = cl.getHero(pack.heroId);
-	auto obj = cl.getObj(pack.objId, false);
-	callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::heroVisit, hero, obj, pack.starting);
-}
-
-void ApplyClientNetPackVisitor::visitNewTurn(NewTurn & pack)
-{
-	cl.invalidatePaths();
-}
-
-void ApplyClientNetPackVisitor::visitGiveBonus(GiveBonus & pack)
-{
-	cl.invalidatePaths();
-	switch(pack.who)
-	{
-	case GiveBonus::ETarget::HERO:
-		{
-			const CGHeroInstance *h = gs.getHero(ObjectInstanceID(pack.id));
-			callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, true);
-		}
-		break;
-	case GiveBonus::ETarget::PLAYER:
-		{
-			callInterfaceIfPresent(cl, PlayerColor(pack.id), &IGameEventsReceiver::playerBonusChanged, pack.bonus, true);
-		}
-		break;
-	}
-}
-
-void ApplyFirstClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack)
-{
-	CGObjectInstance *obj = gs.getObjInstance(pack.objid);
-	if(CGI->mh)
-		CGI->mh->onObjectFadeOut(obj, pack.initiator);
-
-	CGI->mh->waitForOngoingAnimations();
-}
-
-void ApplyClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack)
-{
-	CGObjectInstance *obj = gs.getObjInstance(pack.objid);
-	if(CGI->mh)
-		CGI->mh->onObjectFadeIn(obj, pack.initiator);
-
-	CGI->mh->waitForOngoingAnimations();
-	cl.invalidatePaths();
-}
-
-void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack)
-{
-	callAllInterfaces(cl, &IGameEventsReceiver::gameOver, pack.player, pack.victoryLossCheckResult);
-
-	// In auto testing pack.mode we always close client if red pack.player won or lose
-	if(!settings["session"]["testmap"].isNull() && pack.player == PlayerColor(0))
-		handleQuit(settings["session"]["spectate"].Bool()); // if spectator is active ask to close client or not
-}
-
-void ApplyClientNetPackVisitor::visitPlayerReinitInterface(PlayerReinitInterface & pack)
-{
-	auto initInterfaces = [this]()
-	{
-		cl.initPlayerInterfaces();
-
-		for (PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player)
-		{
-			if (cl.gameState()->isPlayerMakingTurn(player))
-			{
-				callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, player);
-				callOnlyThatInterface(cl, player, &CGameInterface::yourTurn, QueryID::NONE);
-			}
-		}
-	};
-	
-	for(auto player : pack.players)
-	{
-		auto & plSettings = CSH->si->getIthPlayersSettings(player);
-		if(pack.playerConnectionId == PlayerSettings::PLAYER_AI)
-		{
-			plSettings.connectedPlayerIDs.clear();
-			cl.initPlayerEnvironments();
-			initInterfaces();
-		}
-		else if(pack.playerConnectionId == CSH->c->connectionID)
-		{
-			plSettings.connectedPlayerIDs.insert(pack.playerConnectionId);
-			cl.playerint.clear();
-			initInterfaces();
-		}
-	}
-}
-
-void ApplyClientNetPackVisitor::visitRemoveBonus(RemoveBonus & pack)
-{
-	cl.invalidatePaths();
-	switch(pack.who)
-	{
-	case GiveBonus::ETarget::HERO:
-		{
-			const CGHeroInstance *h = gs.getHero(ObjectInstanceID(pack.whoID));
-			callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, false);
-		}
-		break;
-	case GiveBonus::ETarget::PLAYER:
-		{
-			//const PlayerState *p = gs.getPlayerState(pack.id);
-			callInterfaceIfPresent(cl, PlayerColor(pack.whoID), &IGameEventsReceiver::playerBonusChanged, pack.bonus, false);
-		}
-		break;
-	}
-}
-
-void ApplyFirstClientNetPackVisitor::visitRemoveObject(RemoveObject & pack)
-{
-	const CGObjectInstance *o = cl.getObj(pack.objectID);
-
-	if(CGI->mh)
-		CGI->mh->onObjectFadeOut(o, pack.initiator);
-
-	//notify interfaces about removal
-	for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++)
-	{
-		//below line contains little cheat for AI so it will be aware of deletion of enemy heroes that moved or got re-covered by FoW
-		//TODO: loose requirements as next AI related crashes appear, for example another pack.player collects object that got re-covered by FoW, unsure if AI code workarounds this
-		if(gs.isVisible(o, i->first) || (!cl.getPlayerState(i->first)->human && o->ID == Obj::HERO && o->tempOwner != i->first))
-			i->second->objectRemoved(o, pack.initiator);
-	}
-
-	CGI->mh->waitForOngoingAnimations();
-}
-
-void ApplyClientNetPackVisitor::visitRemoveObject(RemoveObject & pack)
-{
-	cl.invalidatePaths();
-	for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++)
-		i->second->objectRemovedAfter();
-}
-
-void ApplyFirstClientNetPackVisitor::visitTryMoveHero(TryMoveHero & pack)
-{
-	CGHeroInstance *h = gs.getHero(pack.id);
-
-	if(CGI->mh)
-	{
-		switch (pack.result)
-		{
-			case TryMoveHero::EMBARK:
-				CGI->mh->onBeforeHeroEmbark(h, pack.start, pack.end);
-				break;
-			case TryMoveHero::TELEPORTATION:
-				CGI->mh->onBeforeHeroTeleported(h, pack.start, pack.end);
-				break;
-			case TryMoveHero::DISEMBARK:
-				CGI->mh->onBeforeHeroDisembark(h, pack.start, pack.end);
-				break;
-		}
-		CGI->mh->waitForOngoingAnimations();
-	}
-}
-
-void ApplyClientNetPackVisitor::visitTryMoveHero(TryMoveHero & pack)
-{
-	const CGHeroInstance *h = cl.getHero(pack.id);
-	cl.invalidatePaths();
-
-	if(CGI->mh)
-	{
-		switch(pack.result)
-		{
-			case TryMoveHero::SUCCESS:
-				CGI->mh->onHeroMoved(h, pack.start, pack.end);
-				break;
-			case TryMoveHero::EMBARK:
-				CGI->mh->onAfterHeroEmbark(h, pack.start, pack.end);
-				break;
-			case TryMoveHero::TELEPORTATION:
-				CGI->mh->onAfterHeroTeleported(h, pack.start, pack.end);
-				break;
-			case TryMoveHero::DISEMBARK:
-				CGI->mh->onAfterHeroDisembark(h, pack.start, pack.end);
-				break;
-		}
-	}
-
-	PlayerColor player = h->tempOwner;
-
-	for(auto &i : cl.playerint)
-		if(cl.getPlayerRelations(i.first, player) != PlayerRelations::ENEMIES)
-			i.second->tileRevealed(pack.fowRevealed);
-
-	for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++)
-	{
-		if(i->first != PlayerColor::SPECTATOR && gs.checkForStandardLoss(i->first)) // Do not notify vanquished pack.player's interface
-			continue;
-
-		if(gs.isVisible(h->convertToVisitablePos(pack.start), i->first)
-			|| gs.isVisible(h->convertToVisitablePos(pack.end), i->first))
-		{
-			// pack.src and pack.dst of enemy hero move may be not visible => 'verbose' should be false
-			const bool verbose = cl.getPlayerRelations(i->first, player) != PlayerRelations::ENEMIES;
-			i->second->heroMoved(pack, verbose);
-		}
-	}
-}
-
-void ApplyClientNetPackVisitor::visitNewStructures(NewStructures & pack)
-{
-	CGTownInstance *town = gs.getTown(pack.tid);
-	for(const auto & id : pack.bid)
-	{
-		callInterfaceIfPresent(cl, town->getOwner(), &IGameEventsReceiver::buildChanged, town, id, 1);
-	}
-
-	// invalidate section of map view with our object and force an update
-	CGI->mh->onObjectInstantRemove(town, town->getOwner());
-	CGI->mh->onObjectInstantAdd(town, town->getOwner());
-
-}
-void ApplyClientNetPackVisitor::visitRazeStructures(RazeStructures & pack)
-{
-	CGTownInstance * town = gs.getTown(pack.tid);
-	for(const auto & id : pack.bid)
-	{
-		callInterfaceIfPresent(cl, town->getOwner(), &IGameEventsReceiver::buildChanged, town, id, 2);
-	}
-
-	// invalidate section of map view with our object and force an update
-	CGI->mh->onObjectInstantRemove(town, town->getOwner());
-	CGI->mh->onObjectInstantAdd(town, town->getOwner());
-}
-
-void ApplyClientNetPackVisitor::visitSetAvailableCreatures(SetAvailableCreatures & pack)
-{
-	const CGDwelling * dw = static_cast<const CGDwelling*>(cl.getObj(pack.tid));
-
-	PlayerColor p;
-	if(dw->ID == Obj::WAR_MACHINE_FACTORY) //War Machines Factory is not flaggable, it's "owned" by visitor
-		p = cl.getTile(dw->visitablePos())->visitableObjects.back()->tempOwner;
-	else
-		p = dw->tempOwner;
-
-	callInterfaceIfPresent(cl, p, &IGameEventsReceiver::availableCreaturesChanged, dw);
-}
-
-void ApplyClientNetPackVisitor::visitSetHeroesInTown(SetHeroesInTown & pack)
-{
-	CGTownInstance * t = gs.getTown(pack.tid);
-	CGHeroInstance * hGarr  = gs.getHero(pack.garrison);
-	CGHeroInstance * hVisit = gs.getHero(pack.visiting);
-
-	//inform all players that see this object
-	for(auto i = cl.playerint.cbegin(); i != cl.playerint.cend(); ++i)
-	{
-		if(!i->first.isValidPlayer())
-			continue;
-
-		if(gs.isVisible(t, i->first) ||
-			(hGarr && gs.isVisible(hGarr, i->first)) ||
-			(hVisit && gs.isVisible(hVisit, i->first)))
-		{
-			cl.playerint[i->first]->heroInGarrisonChange(t);
-		}
-	}
-}
-
-void ApplyClientNetPackVisitor::visitHeroRecruited(HeroRecruited & pack)
-{
-	CGHeroInstance *h = gs.map->heroesOnMap.back();
-	if(h->subID != pack.hid)
-	{
-		logNetwork->error("Something wrong with hero recruited!");
-	}
-
-	if(callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroCreated, h))
-	{
-		if(const CGTownInstance *t = gs.getTown(pack.tid))
-			callInterfaceIfPresent(cl, h->getOwner(), &IGameEventsReceiver::heroInGarrisonChange, t);
-	}
-	if(CGI->mh)
-		CGI->mh->onObjectInstantAdd(h, h->getOwner());
-}
-
-void ApplyClientNetPackVisitor::visitGiveHero(GiveHero & pack)
-{
-	CGHeroInstance *h = gs.getHero(pack.id);
-	if(CGI->mh)
-		CGI->mh->onObjectInstantAdd(h, h->getOwner());
-	callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroCreated, h);
-}
-
-void ApplyFirstClientNetPackVisitor::visitGiveHero(GiveHero & pack)
-{
-}
-
-void ApplyClientNetPackVisitor::visitInfoWindow(InfoWindow & pack)
-{
-	std::string str = pack.text.toString();
-
-	if(!callInterfaceIfPresent(cl, pack.player, &CGameInterface::showInfoDialog, pack.type, str, pack.components,(soundBase::soundID)pack.soundID))
-		logNetwork->warn("We received InfoWindow for not our player...");
-}
-
-void ApplyFirstClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & pack)
-{
-	//inform all players that see this object
-	for(auto it = cl.playerint.cbegin(); it != cl.playerint.cend(); ++it)
-	{
-		if(gs.isVisible(gs.getObjInstance(pack.id), it->first))
-			callInterfaceIfPresent(cl, it->first, &IGameEventsReceiver::beforeObjectPropertyChanged, &pack);
-	}
-
-	// invalidate section of map view with our object and force an update with new flag color
-	if (pack.what == ObjProperty::OWNER)
-	{
-		auto object = gs.getObjInstance(pack.id);
-		CGI->mh->onObjectInstantRemove(object, object->getOwner());
-	}
-}
-
-void ApplyClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & pack)
-{
-	//inform all players that see this object
-	for(auto it = cl.playerint.cbegin(); it != cl.playerint.cend(); ++it)
-	{
-		if(gs.isVisible(gs.getObjInstance(pack.id), it->first))
-			callInterfaceIfPresent(cl, it->first, &IGameEventsReceiver::objectPropertyChanged, &pack);
-	}
-
-	// invalidate section of map view with our object and force an update with new flag color
-	if (pack.what == ObjProperty::OWNER)
-	{
-		auto object = gs.getObjInstance(pack.id);
-		CGI->mh->onObjectInstantAdd(object, object->getOwner());
-	}
-}
-
-void ApplyClientNetPackVisitor::visitHeroLevelUp(HeroLevelUp & pack)
-{
-	const CGHeroInstance * hero = cl.getHero(pack.heroId);
-	assert(hero);
-	callOnlyThatInterface(cl, pack.player, &CGameInterface::heroGotLevel, hero, pack.primskill, pack.skills, pack.queryID);
-}
-
-void ApplyClientNetPackVisitor::visitCommanderLevelUp(CommanderLevelUp & pack)
-{
-	const CGHeroInstance * hero = cl.getHero(pack.heroId);
-	assert(hero);
-	const CCommanderInstance * commander = hero->commander;
-	assert(commander);
-	assert(commander->armyObj); //is it possible for Commander to exist beyond armed instance?
-	callOnlyThatInterface(cl, pack.player, &CGameInterface::commanderGotLevel, commander, pack.skills, pack.queryID);
-}
-
-void ApplyClientNetPackVisitor::visitBlockingDialog(BlockingDialog & pack)
-{
-	std::string str = pack.text.toString();
-
-	if(!callOnlyThatInterface(cl, pack.player, &CGameInterface::showBlockingDialog, str, pack.components, pack.queryID, (soundBase::soundID)pack.soundID, pack.selection(), pack.cancel()))
-		logNetwork->warn("We received YesNoDialog for not our player...");
-}
-
-void ApplyClientNetPackVisitor::visitGarrisonDialog(GarrisonDialog & pack)
-{
-	const CGHeroInstance *h = cl.getHero(pack.hid);
-	const CArmedInstance *obj = static_cast<const CArmedInstance*>(cl.getObj(pack.objid));
-
-	callOnlyThatInterface(cl, h->getOwner(), &CGameInterface::showGarrisonDialog, obj, h, pack.removableUnits, pack.queryID);
-}
-
-void ApplyClientNetPackVisitor::visitExchangeDialog(ExchangeDialog & pack)
-{
-	callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::heroExchangeStarted, pack.hero1, pack.hero2, pack.queryID);
-}
-
-void ApplyClientNetPackVisitor::visitTeleportDialog(TeleportDialog & pack)
-{
-	const CGHeroInstance *h = cl.getHero(pack.hero);
-	callOnlyThatInterface(cl, h->getOwner(), &CGameInterface::showTeleportDialog, h, pack.channel, pack.exits, pack.impassable, pack.queryID);
-}
-
-void ApplyClientNetPackVisitor::visitMapObjectSelectDialog(MapObjectSelectDialog & pack)
-{
-	callOnlyThatInterface(cl, pack.player, &CGameInterface::showMapObjectSelectDialog, pack.queryID, pack.icon, pack.title, pack.description, pack.objects);
-}
-
-void ApplyFirstClientNetPackVisitor::visitBattleStart(BattleStart & pack)
-{
-	// Cannot use the usual code because curB is not set yet
-	callOnlyThatBattleInterface(cl, pack.info->sides[0].color, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject,
-		pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero);
-	callOnlyThatBattleInterface(cl, pack.info->sides[1].color, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject,
-		pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero);
-	callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject,
-		pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero);
-}
-
-void ApplyClientNetPackVisitor::visitBattleStart(BattleStart & pack)
-{
-	cl.battleStarted(pack.info);
-}
-
-void ApplyFirstClientNetPackVisitor::visitBattleNextRound(BattleNextRound & pack)
-{
-	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleNewRoundFirst, pack.battleID);
-}
-
-void ApplyClientNetPackVisitor::visitBattleNextRound(BattleNextRound & pack)
-{
-	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleNewRound, pack.battleID);
-}
-
-void ApplyClientNetPackVisitor::visitBattleSetActiveStack(BattleSetActiveStack & pack)
-{
-	if(!pack.askPlayerInterface)
-		return;
-
-	const CStack *activated = gs.getBattle(pack.battleID)->battleGetStackByID(pack.stack);
-	PlayerColor playerToCall; //pack.player that will move activated stack
-	if (activated->hasBonusOfType(BonusType::HYPNOTIZED))
-	{
-		playerToCall = (gs.getBattle(pack.battleID)->sides[0].color == activated->unitOwner()
-			? gs.getBattle(pack.battleID)->sides[1].color
-			: gs.getBattle(pack.battleID)->sides[0].color);
-	}
-	else
-	{
-		playerToCall = activated->unitOwner();
-	}
-
-	cl.startPlayerBattleAction(pack.battleID, playerToCall);
-}
-
-void ApplyClientNetPackVisitor::visitBattleLogMessage(BattleLogMessage & pack)
-{
-	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleLogMessage, pack.battleID, pack.lines);
-}
-
-void ApplyClientNetPackVisitor::visitBattleTriggerEffect(BattleTriggerEffect & pack)
-{
-	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleTriggerEffect, pack.battleID, pack);
-}
-
-void ApplyFirstClientNetPackVisitor::visitBattleUpdateGateState(BattleUpdateGateState & pack)
-{
-	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleGateStateChanged, pack.battleID, pack.state);
-}
-
-void ApplyFirstClientNetPackVisitor::visitBattleResult(BattleResult & pack)
-{
-	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleEnd, pack.battleID, &pack, pack.queryID);
-	cl.battleFinished(pack.battleID);
-}
-
-void ApplyFirstClientNetPackVisitor::visitBattleStackMoved(BattleStackMoved & pack)
-{
-	const CStack * movedStack = gs.getBattle(pack.battleID)->battleGetStackByID(pack.stack);
-	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStackMoved, pack.battleID, movedStack, pack.tilesToMove, pack.distance, pack.teleporting);
-}
-
-void ApplyFirstClientNetPackVisitor::visitBattleAttack(BattleAttack & pack)
-{
-	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleAttack, pack.battleID, &pack);
-
-	// battleStacksAttacked should be excuted before BattleAttack.applyGs() to play animation before damaging unit
-	// so this has to be here instead of ApplyClientNetPackVisitor::visitBattleAttack()
-	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStacksAttacked, pack.battleID, pack.bsa, pack.shot());
-}
-
-void ApplyClientNetPackVisitor::visitBattleAttack(BattleAttack & pack)
-{
-}
-
-void ApplyFirstClientNetPackVisitor::visitStartAction(StartAction & pack)
-{
-	cl.currentBattleAction = std::make_unique<BattleAction>(pack.ba);
-	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::actionStarted, pack.battleID, pack.ba);
-}
-
-void ApplyClientNetPackVisitor::visitBattleSpellCast(BattleSpellCast & pack)
-{
-	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleSpellCast, pack.battleID, &pack);
-}
-
-void ApplyClientNetPackVisitor::visitSetStackEffect(SetStackEffect & pack)
-{
-	//informing about effects
-	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStacksEffectsSet, pack.battleID, pack);
-}
-
-void ApplyClientNetPackVisitor::visitStacksInjured(StacksInjured & pack)
-{
-	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStacksAttacked, pack.battleID, pack.stacks, false);
-}
-
-void ApplyClientNetPackVisitor::visitBattleResultsApplied(BattleResultsApplied & pack)
-{
-	callInterfaceIfPresent(cl, pack.player1, &IGameEventsReceiver::battleResultsApplied);
-	callInterfaceIfPresent(cl, pack.player2, &IGameEventsReceiver::battleResultsApplied);
-	callInterfaceIfPresent(cl, PlayerColor::SPECTATOR, &IGameEventsReceiver::battleResultsApplied);
-}
-
-void ApplyClientNetPackVisitor::visitBattleUnitsChanged(BattleUnitsChanged & pack)
-{
-	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleUnitsChanged, pack.battleID, pack.changedStacks);
-}
-
-void ApplyClientNetPackVisitor::visitBattleObstaclesChanged(BattleObstaclesChanged & pack)
-{
-	//inform interfaces about removed obstacles
-	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleObstaclesChanged, pack.battleID, pack.changes);
-}
-
-void ApplyClientNetPackVisitor::visitCatapultAttack(CatapultAttack & pack)
-{
-	//inform interfaces about catapult attack
-	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleCatapultAttacked, pack.battleID, pack);
-}
-
-void ApplyClientNetPackVisitor::visitEndAction(EndAction & pack)
-{
-	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::actionFinished, pack.battleID, *cl.currentBattleAction);
-	cl.currentBattleAction.reset();
-}
-
-void ApplyClientNetPackVisitor::visitPackageApplied(PackageApplied & pack)
-{
-	callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::requestRealized, &pack);
-	if(!CClient::waitingRequest.tryRemovingElement(pack.requestID))
-		logNetwork->warn("Surprising server message! PackageApplied for unknown requestID!");
-}
-
-void ApplyClientNetPackVisitor::visitSystemMessage(SystemMessage & pack)
-{
-	std::ostringstream str;
-	str << "System message: " << pack.text;
-
-	logNetwork->error(str.str()); // usually used to receive error messages from server
-	if(LOCPLINT && !settings["session"]["hideSystemMessages"].Bool())
-		LOCPLINT->cingconsole->print(str.str());
-}
-
-void ApplyClientNetPackVisitor::visitPlayerBlocked(PlayerBlocked & pack)
-{
-	callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::playerBlocked, pack.reason, pack.startOrEnd == PlayerBlocked::BLOCKADE_STARTED);
-}
-
-void ApplyClientNetPackVisitor::visitPlayerStartsTurn(PlayerStartsTurn & pack)
-{
-	logNetwork->debug("Server gives turn to %s", pack.player.toString());
-
-	callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, pack.player);
-	callOnlyThatInterface(cl, pack.player, &CGameInterface::yourTurn, pack.queryID);
-}
-
-void ApplyClientNetPackVisitor::visitPlayerEndsTurn(PlayerEndsTurn & pack)
-{
-	logNetwork->debug("Server ends turn of %s", pack.player.toString());
-
-	callAllInterfaces(cl, &IGameEventsReceiver::playerEndsTurn, pack.player);
-}
-
-void ApplyClientNetPackVisitor::visitTurnTimeUpdate(TurnTimeUpdate & pack)
-{
-	logNetwork->debug("Server sets turn timer {turn: %d, base: %d, battle: %d, creature: %d} for %s", pack.turnTimer.turnTimer, pack.turnTimer.baseTimer, pack.turnTimer.battleTimer, pack.turnTimer.creatureTimer, pack.player.toString());
-}
-
-void ApplyClientNetPackVisitor::visitPlayerMessageClient(PlayerMessageClient & pack)
-{
-	logNetwork->debug("pack.player %s sends a message: %s", pack.player.toString(), pack.text);
-
-	std::ostringstream str;
-	if(pack.player.isSpectator())
-		str << "Spectator: " << pack.text;
-	else
-		str << cl.getPlayerState(pack.player)->nodeName() <<": " << pack.text;
-	if(LOCPLINT)
-		LOCPLINT->cingconsole->print(str.str());
-}
-
-void ApplyClientNetPackVisitor::visitAdvmapSpellCast(AdvmapSpellCast & pack)
-{
-	cl.invalidatePaths();
-	auto caster = cl.getHero(pack.casterID);
-	if(caster)
-		//consider notifying other interfaces that see hero?
-		callInterfaceIfPresent(cl, caster->getOwner(), &IGameEventsReceiver::advmapSpellCast, caster, pack.spellID);
-	else
-		logNetwork->error("Invalid hero instance");
-}
-
-void ApplyClientNetPackVisitor::visitShowWorldViewEx(ShowWorldViewEx & pack)
-{
-	callOnlyThatInterface(cl, pack.player, &CGameInterface::showWorldViewEx, pack.objectPositions, pack.showTerrain);
-}
-
-void ApplyClientNetPackVisitor::visitOpenWindow(OpenWindow & pack)
-{
-	switch(pack.window)
-	{
-	case EOpenWindowMode::RECRUITMENT_FIRST:
-	case EOpenWindowMode::RECRUITMENT_ALL:
-		{
-			const CGDwelling *dw = dynamic_cast<const CGDwelling*>(cl.getObj(ObjectInstanceID(pack.object)));
-			const CArmedInstance *dst = dynamic_cast<const CArmedInstance*>(cl.getObj(ObjectInstanceID(pack.visitor)));
-			callInterfaceIfPresent(cl, dst->tempOwner, &IGameEventsReceiver::showRecruitmentDialog, dw, dst, pack.window == EOpenWindowMode::RECRUITMENT_FIRST ? 0 : -1, pack.queryID);
-		}
-		break;
-	case EOpenWindowMode::SHIPYARD_WINDOW:
-		{
-			assert(pack.queryID == QueryID::NONE);
-			const IShipyard *sy = IShipyard::castFrom(cl.getObj(ObjectInstanceID(pack.object)));
-			callInterfaceIfPresent(cl, sy->getObject()->getOwner(), &IGameEventsReceiver::showShipyardDialog, sy);
-		}
-		break;
-	case EOpenWindowMode::THIEVES_GUILD:
-		{
-			assert(pack.queryID == QueryID::NONE);
-			//displays Thieves' Guild window (when hero enters Den of Thieves)
-			const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object));
-			const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor));
-			callInterfaceIfPresent(cl, hero->getOwner(), &IGameEventsReceiver::showThievesGuildWindow, obj);
-		}
-		break;
-	case EOpenWindowMode::UNIVERSITY_WINDOW:
-		{
-			//displays University window (when hero enters University on adventure map)
-			const IMarket *market = IMarket::castFrom(cl.getObj(ObjectInstanceID(pack.object)));
-			const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor));
-			callInterfaceIfPresent(cl, hero->tempOwner, &IGameEventsReceiver::showUniversityWindow, market, hero, pack.queryID);
-		}
-		break;
-	case EOpenWindowMode::MARKET_WINDOW:
-		{
-			//displays Thieves' Guild window (when hero enters Den of Thieves)
-			const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object));
-			const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor));
-			const IMarket *market = IMarket::castFrom(obj);
-			callInterfaceIfPresent(cl, cl.getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showMarketWindow, market, hero, pack.queryID);
-		}
-		break;
-	case EOpenWindowMode::HILL_FORT_WINDOW:
-		{
-			assert(pack.queryID == QueryID::NONE);
-			//displays Hill fort window
-			const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object));
-			const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor));
-			callInterfaceIfPresent(cl, cl.getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showHillFortWindow, obj, hero);
-		}
-		break;
-	case EOpenWindowMode::PUZZLE_MAP:
-		{
-			assert(pack.queryID == QueryID::NONE);
-			const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor));
-			callInterfaceIfPresent(cl, hero->getOwner(), &IGameEventsReceiver::showPuzzleMap);
-		}
-		break;
-	case EOpenWindowMode::TAVERN_WINDOW:
-		{
-			const CGObjectInstance *obj1 = cl.getObj(ObjectInstanceID(pack.object));
-			const CGHeroInstance * hero = cl.getHero(ObjectInstanceID(pack.visitor));
-			callInterfaceIfPresent(cl, hero->tempOwner, &IGameEventsReceiver::showTavernWindow, obj1, hero, pack.queryID);
-		}
-		break;
-	}
-}
-
-void ApplyClientNetPackVisitor::visitCenterView(CenterView & pack)
-{
-	callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::centerView, pack.pos, pack.focusTime);
-}
-
-void ApplyClientNetPackVisitor::visitNewObject(NewObject & pack)
-{
-	cl.invalidatePaths();
-
-	const CGObjectInstance *obj = cl.getObj(pack.createdObjectID);
-	if(CGI->mh)
-		CGI->mh->onObjectFadeIn(obj, pack.initiator);
-
-	for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++)
-	{
-		if(gs.isVisible(obj, i->first))
-			i->second->newObject(obj);
-	}
-	CGI->mh->waitForOngoingAnimations();
-}
-
-void ApplyClientNetPackVisitor::visitSetAvailableArtifacts(SetAvailableArtifacts & pack)
-{
-	if(pack.id < 0) //artifact merchants globally
-	{
-		callAllInterfaces(cl, &IGameEventsReceiver::availableArtifactsChanged, nullptr);
-	}
-	else
-	{
-		const CGBlackMarket *bm = dynamic_cast<const CGBlackMarket *>(cl.getObj(ObjectInstanceID(pack.id)));
-		assert(bm);
-		callInterfaceIfPresent(cl, cl.getTile(bm->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::availableArtifactsChanged, bm);
-	}
-}
-
-
-void ApplyClientNetPackVisitor::visitEntitiesChanged(EntitiesChanged & pack)
-{
-	cl.invalidatePaths();
-}
+/*
+ * NetPacksClient.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 "ClientNetPackVisitors.h"
+
+#include "Client.h"
+#include "CPlayerInterface.h"
+#include "CGameInfo.h"
+#include "windows/GUIClasses.h"
+#include "mapView/mapHandler.h"
+#include "adventureMap/CInGameConsole.h"
+#include "battle/BattleInterface.h"
+#include "battle/BattleWindow.h"
+#include "gui/CGuiHandler.h"
+#include "gui/WindowHandler.h"
+#include "widgets/MiscWidgets.h"
+#include "CMT.h"
+#include "CServerHandler.h"
+
+#include "../CCallback.h"
+#include "../lib/filesystem/Filesystem.h"
+#include "../lib/filesystem/FileInfo.h"
+#include "../lib/serializer/Connection.h"
+#include "../lib/serializer/BinarySerializer.h"
+#include "../lib/CGeneralTextHandler.h"
+#include "../lib/CHeroHandler.h"
+#include "../lib/VCMI_Lib.h"
+#include "../lib/mapping/CMap.h"
+#include "../lib/VCMIDirs.h"
+#include "../lib/spells/CSpellHandler.h"
+#include "../lib/CSoundBase.h"
+#include "../lib/StartInfo.h"
+#include "../lib/CConfigHandler.h"
+#include "../lib/mapObjects/CGMarket.h"
+#include "../lib/gameState/CGameState.h"
+#include "../lib/CStack.h"
+#include "../lib/battle/BattleInfo.h"
+#include "../lib/GameConstants.h"
+#include "../lib/CPlayerState.h"
+
+// TODO: as Tow suggested these template should all be part of CClient
+// This will require rework spectator interface properly though
+
+template<typename T, typename ... Args, typename ... Args2>
+bool callOnlyThatInterface(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args)
+{
+	if(vstd::contains(cl.playerint, player))
+	{
+		((*cl.playerint[player]).*ptr)(std::forward<Args2>(args)...);
+		return true;
+	}
+	return false;
+}
+
+template<typename T, typename ... Args, typename ... Args2>
+bool callInterfaceIfPresent(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args)
+{
+	bool called = callOnlyThatInterface(cl, player, ptr, std::forward<Args2>(args)...);
+	return called;
+}
+
+template<typename T, typename ... Args, typename ... Args2>
+void callOnlyThatBattleInterface(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args)
+{
+	if(vstd::contains(cl.battleints,player))
+		((*cl.battleints[player]).*ptr)(std::forward<Args2>(args)...);
+
+	if(cl.additionalBattleInts.count(player))
+	{
+		for(auto bInt : cl.additionalBattleInts[player])
+			((*bInt).*ptr)(std::forward<Args2>(args)...);
+	}
+}
+
+template<typename T, typename ... Args, typename ... Args2>
+void callBattleInterfaceIfPresent(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args)
+{
+	callOnlyThatInterface(cl, player, ptr, std::forward<Args2>(args)...);
+}
+
+//calls all normal interfaces and privileged ones, playerints may be updated when iterating over it, so we need a copy
+template<typename T, typename ... Args, typename ... Args2>
+void callAllInterfaces(CClient & cl, void (T::*ptr)(Args...), Args2 && ...args)
+{
+	for(auto pInt : cl.playerint)
+		((*pInt.second).*ptr)(std::forward<Args2>(args)...);
+}
+
+//calls all normal interfaces and privileged ones, playerints may be updated when iterating over it, so we need a copy
+template<typename T, typename ... Args, typename ... Args2>
+void callBattleInterfaceIfPresentForBothSides(CClient & cl, const BattleID & battleID, void (T::*ptr)(Args...), Args2 && ...args)
+{
+	assert(cl.gameState()->getBattle(battleID));
+
+	if (!cl.gameState()->getBattle(battleID))
+	{
+		logGlobal->error("Attempt to call battle interface without ongoing battle!");
+		return;
+	}
+
+	callOnlyThatBattleInterface(cl, cl.gameState()->getBattle(battleID)->sides[0].color, ptr, std::forward<Args2>(args)...);
+	callOnlyThatBattleInterface(cl, cl.gameState()->getBattle(battleID)->sides[1].color, ptr, std::forward<Args2>(args)...);
+	if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool() && LOCPLINT->battleInt)
+	{
+		callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, ptr, std::forward<Args2>(args)...);
+	}
+}
+
+void ApplyClientNetPackVisitor::visitSetResources(SetResources & pack)
+{
+	//todo: inform on actual resource set transfered
+	callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::receivedResource);
+}
+
+void ApplyClientNetPackVisitor::visitSetPrimSkill(SetPrimSkill & pack)
+{
+	const CGHeroInstance * h = cl.getHero(pack.id);
+	if(!h)
+	{
+		logNetwork->error("Cannot find hero with pack.id %d", pack.id.getNum());
+		return;
+	}
+	callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroPrimarySkillChanged, h, pack.which, pack.val);
+}
+
+void ApplyClientNetPackVisitor::visitSetSecSkill(SetSecSkill & pack)
+{
+	const CGHeroInstance *h = cl.getHero(pack.id);
+	if(!h)
+	{
+		logNetwork->error("Cannot find hero with pack.id %d", pack.id.getNum());
+		return;
+	}
+	callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroSecondarySkillChanged, h, pack.which, pack.val);
+}
+
+void ApplyClientNetPackVisitor::visitHeroVisitCastle(HeroVisitCastle & pack)
+{
+	const CGHeroInstance *h = cl.getHero(pack.hid);
+	
+	if(pack.start())
+	{
+		callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroVisitsTown, h, gs.getTown(pack.tid));
+	}
+}
+
+void ApplyClientNetPackVisitor::visitSetMana(SetMana & pack)
+{
+	const CGHeroInstance *h = cl.getHero(pack.hid);
+	callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroManaPointsChanged, h);
+
+	for (auto window : GH.windows().findWindows<BattleWindow>())
+		window->heroManaPointsChanged(h);
+}
+
+void ApplyClientNetPackVisitor::visitSetMovePoints(SetMovePoints & pack)
+{
+	const CGHeroInstance *h = cl.getHero(pack.hid);
+	cl.invalidatePaths();
+	callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroMovePointsChanged, h);
+}
+
+void ApplyClientNetPackVisitor::visitFoWChange(FoWChange & pack)
+{
+	for(auto &i : cl.playerint)
+	{
+		if(cl.getPlayerRelations(i.first, pack.player) == PlayerRelations::SAME_PLAYER && pack.waitForDialogs && LOCPLINT == i.second.get())
+		{
+			LOCPLINT->waitWhileDialog();
+		}
+		if(cl.getPlayerRelations(i.first, pack.player) != PlayerRelations::ENEMIES)
+		{
+			if(pack.mode == ETileVisibility::REVEALED)
+				i.second->tileRevealed(pack.tiles);
+			else
+				i.second->tileHidden(pack.tiles);
+		}
+	}
+	cl.invalidatePaths();
+}
+
+static void dispatchGarrisonChange(CClient & cl, ObjectInstanceID army1, ObjectInstanceID army2)
+{
+	auto obj1 = cl.getObj(army1);
+	if(!obj1)
+	{
+		logNetwork->error("Cannot find army with pack.id %d", army1.getNum());
+		return;
+	}
+
+	callInterfaceIfPresent(cl, obj1->tempOwner, &IGameEventsReceiver::garrisonsChanged, army1, army2);
+
+	if(army2 != ObjectInstanceID() && army2 != army1)
+	{
+		auto obj2 = cl.getObj(army2);
+		if(!obj2)
+		{
+			logNetwork->error("Cannot find army with pack.id %d", army2.getNum());
+			return;
+		}
+
+		if(obj1->tempOwner != obj2->tempOwner)
+			callInterfaceIfPresent(cl, obj2->tempOwner, &IGameEventsReceiver::garrisonsChanged, army1, army2);
+	}
+}
+
+void ApplyClientNetPackVisitor::visitChangeStackCount(ChangeStackCount & pack)
+{
+	dispatchGarrisonChange(cl, pack.army, ObjectInstanceID());
+}
+
+void ApplyClientNetPackVisitor::visitSetStackType(SetStackType & pack)
+{
+	dispatchGarrisonChange(cl, pack.army, ObjectInstanceID());
+}
+
+void ApplyClientNetPackVisitor::visitEraseStack(EraseStack & pack)
+{
+	dispatchGarrisonChange(cl, pack.army, ObjectInstanceID());
+}
+
+void ApplyClientNetPackVisitor::visitSwapStacks(SwapStacks & pack)
+{
+	dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy);
+}
+
+void ApplyClientNetPackVisitor::visitInsertNewStack(InsertNewStack & pack)
+{
+	dispatchGarrisonChange(cl, pack.army, ObjectInstanceID());
+}
+
+void ApplyClientNetPackVisitor::visitRebalanceStacks(RebalanceStacks & pack)
+{
+	dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy);
+}
+
+void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & pack)
+{
+	if(!pack.moves.empty())
+	{
+		auto destArmy = pack.moves[0].srcArmy == pack.moves[0].dstArmy
+			? ObjectInstanceID()
+			: pack.moves[0].dstArmy;
+		dispatchGarrisonChange(cl, pack.moves[0].srcArmy, destArmy);
+	}
+}
+
+void ApplyClientNetPackVisitor::visitBulkSmartRebalanceStacks(BulkSmartRebalanceStacks & pack)
+{
+	if(!pack.moves.empty())
+	{
+		assert(pack.moves[0].srcArmy == pack.moves[0].dstArmy);
+		dispatchGarrisonChange(cl, pack.moves[0].srcArmy, ObjectInstanceID());
+	}
+	else if(!pack.changes.empty())
+	{
+		dispatchGarrisonChange(cl, pack.changes[0].army, ObjectInstanceID());
+	}
+}
+
+void ApplyClientNetPackVisitor::visitPutArtifact(PutArtifact & pack)
+{
+	callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactPut, pack.al);
+	if(pack.askAssemble)
+		callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::askToAssembleArtifact, pack.al);
+}
+
+void ApplyClientNetPackVisitor::visitEraseArtifact(EraseArtifact & pack)
+{
+	callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactRemoved, pack.al);
+}
+
+void ApplyClientNetPackVisitor::visitMoveArtifact(MoveArtifact & pack)
+{
+	auto moveArtifact = [this, &pack](PlayerColor player) -> void
+	{
+		callInterfaceIfPresent(cl, player, &IGameEventsReceiver::artifactMoved, pack.src, pack.dst);
+		if(pack.askAssemble)
+			callInterfaceIfPresent(cl, player, &IGameEventsReceiver::askToAssembleArtifact, pack.dst);
+	};
+
+	moveArtifact(pack.src.owningPlayer());
+	if(pack.src.owningPlayer() != pack.dst.owningPlayer())
+		moveArtifact(pack.dst.owningPlayer());
+
+	cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings
+}
+
+void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack)
+{
+	auto applyMove = [this, &pack](std::vector<BulkMoveArtifacts::LinkedSlots> & artsPack) -> void
+	{
+		for(auto & slotToMove : artsPack)
+		{
+			auto srcLoc = ArtifactLocation(pack.srcArtHolder, slotToMove.srcPos);
+			auto dstLoc = ArtifactLocation(pack.dstArtHolder, slotToMove.dstPos);
+			MoveArtifact ma(&srcLoc, &dstLoc, false);
+			visitMoveArtifact(ma);
+		}
+	};
+
+	auto srcOwner = std::get<ConstTransitivePtr<CGHeroInstance>>(pack.srcArtHolder)->tempOwner;
+	auto dstOwner = std::get<ConstTransitivePtr<CGHeroInstance>>(pack.dstArtHolder)->tempOwner;
+
+	// Begin a session of bulk movement of arts. It is not necessary but useful for the client optimization.
+	callInterfaceIfPresent(cl, srcOwner, &IGameEventsReceiver::bulkArtMovementStart, pack.artsPack0.size() + pack.artsPack1.size());
+	if(srcOwner != dstOwner)
+		callInterfaceIfPresent(cl, dstOwner, &IGameEventsReceiver::bulkArtMovementStart, pack.artsPack0.size() + pack.artsPack1.size());
+
+	applyMove(pack.artsPack0);
+	if(pack.swap)
+		applyMove(pack.artsPack1);
+}
+
+void ApplyClientNetPackVisitor::visitAssembledArtifact(AssembledArtifact & pack)
+{
+	callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactAssembled, pack.al);
+
+	cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings
+}
+
+void ApplyClientNetPackVisitor::visitDisassembledArtifact(DisassembledArtifact & pack)
+{
+	callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactDisassembled, pack.al);
+
+	cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings
+}
+
+void ApplyClientNetPackVisitor::visitHeroVisit(HeroVisit & pack)
+{
+	auto hero = cl.getHero(pack.heroId);
+	auto obj = cl.getObj(pack.objId, false);
+	callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::heroVisit, hero, obj, pack.starting);
+}
+
+void ApplyClientNetPackVisitor::visitNewTurn(NewTurn & pack)
+{
+	cl.invalidatePaths();
+}
+
+void ApplyClientNetPackVisitor::visitGiveBonus(GiveBonus & pack)
+{
+	cl.invalidatePaths();
+	switch(pack.who)
+	{
+	case GiveBonus::ETarget::HERO:
+		{
+			const CGHeroInstance *h = gs.getHero(ObjectInstanceID(pack.id));
+			callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, true);
+		}
+		break;
+	case GiveBonus::ETarget::PLAYER:
+		{
+			callInterfaceIfPresent(cl, PlayerColor(pack.id), &IGameEventsReceiver::playerBonusChanged, pack.bonus, true);
+		}
+		break;
+	}
+}
+
+void ApplyFirstClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack)
+{
+	CGObjectInstance *obj = gs.getObjInstance(pack.objid);
+	if(CGI->mh)
+		CGI->mh->onObjectFadeOut(obj, pack.initiator);
+
+	CGI->mh->waitForOngoingAnimations();
+}
+
+void ApplyClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack)
+{
+	CGObjectInstance *obj = gs.getObjInstance(pack.objid);
+	if(CGI->mh)
+		CGI->mh->onObjectFadeIn(obj, pack.initiator);
+
+	CGI->mh->waitForOngoingAnimations();
+	cl.invalidatePaths();
+}
+
+void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack)
+{
+	callAllInterfaces(cl, &IGameEventsReceiver::gameOver, pack.player, pack.victoryLossCheckResult);
+
+	// In auto testing pack.mode we always close client if red pack.player won or lose
+	if(!settings["session"]["testmap"].isNull() && pack.player == PlayerColor(0))
+		handleQuit(settings["session"]["spectate"].Bool()); // if spectator is active ask to close client or not
+}
+
+void ApplyClientNetPackVisitor::visitPlayerReinitInterface(PlayerReinitInterface & pack)
+{
+	auto initInterfaces = [this]()
+	{
+		cl.initPlayerInterfaces();
+
+		for (PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player)
+		{
+			if (cl.gameState()->isPlayerMakingTurn(player))
+			{
+				callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, player);
+				callOnlyThatInterface(cl, player, &CGameInterface::yourTurn, QueryID::NONE);
+			}
+		}
+	};
+	
+	for(auto player : pack.players)
+	{
+		auto & plSettings = CSH->si->getIthPlayersSettings(player);
+		if(pack.playerConnectionId == PlayerSettings::PLAYER_AI)
+		{
+			plSettings.connectedPlayerIDs.clear();
+			cl.initPlayerEnvironments();
+			initInterfaces();
+		}
+		else if(pack.playerConnectionId == CSH->c->connectionID)
+		{
+			plSettings.connectedPlayerIDs.insert(pack.playerConnectionId);
+			cl.playerint.clear();
+			initInterfaces();
+		}
+	}
+}
+
+void ApplyClientNetPackVisitor::visitRemoveBonus(RemoveBonus & pack)
+{
+	cl.invalidatePaths();
+	switch(pack.who)
+	{
+	case GiveBonus::ETarget::HERO:
+		{
+			const CGHeroInstance *h = gs.getHero(ObjectInstanceID(pack.whoID));
+			callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, false);
+		}
+		break;
+	case GiveBonus::ETarget::PLAYER:
+		{
+			//const PlayerState *p = gs.getPlayerState(pack.id);
+			callInterfaceIfPresent(cl, PlayerColor(pack.whoID), &IGameEventsReceiver::playerBonusChanged, pack.bonus, false);
+		}
+		break;
+	}
+}
+
+void ApplyFirstClientNetPackVisitor::visitRemoveObject(RemoveObject & pack)
+{
+	const CGObjectInstance *o = cl.getObj(pack.objectID);
+
+	if(CGI->mh)
+		CGI->mh->onObjectFadeOut(o, pack.initiator);
+
+	//notify interfaces about removal
+	for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++)
+	{
+		//below line contains little cheat for AI so it will be aware of deletion of enemy heroes that moved or got re-covered by FoW
+		//TODO: loose requirements as next AI related crashes appear, for example another pack.player collects object that got re-covered by FoW, unsure if AI code workarounds this
+		if(gs.isVisible(o, i->first) || (!cl.getPlayerState(i->first)->human && o->ID == Obj::HERO && o->tempOwner != i->first))
+			i->second->objectRemoved(o, pack.initiator);
+	}
+
+	CGI->mh->waitForOngoingAnimations();
+}
+
+void ApplyClientNetPackVisitor::visitRemoveObject(RemoveObject & pack)
+{
+	cl.invalidatePaths();
+	for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++)
+		i->second->objectRemovedAfter();
+}
+
+void ApplyFirstClientNetPackVisitor::visitTryMoveHero(TryMoveHero & pack)
+{
+	CGHeroInstance *h = gs.getHero(pack.id);
+
+	if(CGI->mh)
+	{
+		switch (pack.result)
+		{
+			case TryMoveHero::EMBARK:
+				CGI->mh->onBeforeHeroEmbark(h, pack.start, pack.end);
+				break;
+			case TryMoveHero::TELEPORTATION:
+				CGI->mh->onBeforeHeroTeleported(h, pack.start, pack.end);
+				break;
+			case TryMoveHero::DISEMBARK:
+				CGI->mh->onBeforeHeroDisembark(h, pack.start, pack.end);
+				break;
+		}
+		CGI->mh->waitForOngoingAnimations();
+	}
+}
+
+void ApplyClientNetPackVisitor::visitTryMoveHero(TryMoveHero & pack)
+{
+	const CGHeroInstance *h = cl.getHero(pack.id);
+	cl.invalidatePaths();
+
+	if(CGI->mh)
+	{
+		switch(pack.result)
+		{
+			case TryMoveHero::SUCCESS:
+				CGI->mh->onHeroMoved(h, pack.start, pack.end);
+				break;
+			case TryMoveHero::EMBARK:
+				CGI->mh->onAfterHeroEmbark(h, pack.start, pack.end);
+				break;
+			case TryMoveHero::TELEPORTATION:
+				CGI->mh->onAfterHeroTeleported(h, pack.start, pack.end);
+				break;
+			case TryMoveHero::DISEMBARK:
+				CGI->mh->onAfterHeroDisembark(h, pack.start, pack.end);
+				break;
+		}
+	}
+
+	PlayerColor player = h->tempOwner;
+
+	for(auto &i : cl.playerint)
+		if(cl.getPlayerRelations(i.first, player) != PlayerRelations::ENEMIES)
+			i.second->tileRevealed(pack.fowRevealed);
+
+	for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++)
+	{
+		if(i->first != PlayerColor::SPECTATOR && gs.checkForStandardLoss(i->first)) // Do not notify vanquished pack.player's interface
+			continue;
+
+		if(gs.isVisible(h->convertToVisitablePos(pack.start), i->first)
+			|| gs.isVisible(h->convertToVisitablePos(pack.end), i->first))
+		{
+			// pack.src and pack.dst of enemy hero move may be not visible => 'verbose' should be false
+			const bool verbose = cl.getPlayerRelations(i->first, player) != PlayerRelations::ENEMIES;
+			i->second->heroMoved(pack, verbose);
+		}
+	}
+}
+
+void ApplyClientNetPackVisitor::visitNewStructures(NewStructures & pack)
+{
+	CGTownInstance *town = gs.getTown(pack.tid);
+	for(const auto & id : pack.bid)
+	{
+		callInterfaceIfPresent(cl, town->getOwner(), &IGameEventsReceiver::buildChanged, town, id, 1);
+	}
+
+	// invalidate section of map view with our object and force an update
+	CGI->mh->onObjectInstantRemove(town, town->getOwner());
+	CGI->mh->onObjectInstantAdd(town, town->getOwner());
+
+}
+void ApplyClientNetPackVisitor::visitRazeStructures(RazeStructures & pack)
+{
+	CGTownInstance * town = gs.getTown(pack.tid);
+	for(const auto & id : pack.bid)
+	{
+		callInterfaceIfPresent(cl, town->getOwner(), &IGameEventsReceiver::buildChanged, town, id, 2);
+	}
+
+	// invalidate section of map view with our object and force an update
+	CGI->mh->onObjectInstantRemove(town, town->getOwner());
+	CGI->mh->onObjectInstantAdd(town, town->getOwner());
+}
+
+void ApplyClientNetPackVisitor::visitSetAvailableCreatures(SetAvailableCreatures & pack)
+{
+	const CGDwelling * dw = static_cast<const CGDwelling*>(cl.getObj(pack.tid));
+
+	PlayerColor p;
+	if(dw->ID == Obj::WAR_MACHINE_FACTORY) //War Machines Factory is not flaggable, it's "owned" by visitor
+		p = cl.getTile(dw->visitablePos())->visitableObjects.back()->tempOwner;
+	else
+		p = dw->tempOwner;
+
+	callInterfaceIfPresent(cl, p, &IGameEventsReceiver::availableCreaturesChanged, dw);
+}
+
+void ApplyClientNetPackVisitor::visitSetHeroesInTown(SetHeroesInTown & pack)
+{
+	CGTownInstance * t = gs.getTown(pack.tid);
+	CGHeroInstance * hGarr  = gs.getHero(pack.garrison);
+	CGHeroInstance * hVisit = gs.getHero(pack.visiting);
+
+	//inform all players that see this object
+	for(auto i = cl.playerint.cbegin(); i != cl.playerint.cend(); ++i)
+	{
+		if(!i->first.isValidPlayer())
+			continue;
+
+		if(gs.isVisible(t, i->first) ||
+			(hGarr && gs.isVisible(hGarr, i->first)) ||
+			(hVisit && gs.isVisible(hVisit, i->first)))
+		{
+			cl.playerint[i->first]->heroInGarrisonChange(t);
+		}
+	}
+}
+
+void ApplyClientNetPackVisitor::visitHeroRecruited(HeroRecruited & pack)
+{
+	CGHeroInstance *h = gs.map->heroesOnMap.back();
+	if(h->subID != pack.hid)
+	{
+		logNetwork->error("Something wrong with hero recruited!");
+	}
+
+	if(callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroCreated, h))
+	{
+		if(const CGTownInstance *t = gs.getTown(pack.tid))
+			callInterfaceIfPresent(cl, h->getOwner(), &IGameEventsReceiver::heroInGarrisonChange, t);
+	}
+	if(CGI->mh)
+		CGI->mh->onObjectInstantAdd(h, h->getOwner());
+}
+
+void ApplyClientNetPackVisitor::visitGiveHero(GiveHero & pack)
+{
+	CGHeroInstance *h = gs.getHero(pack.id);
+	if(CGI->mh)
+		CGI->mh->onObjectInstantAdd(h, h->getOwner());
+	callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroCreated, h);
+}
+
+void ApplyFirstClientNetPackVisitor::visitGiveHero(GiveHero & pack)
+{
+}
+
+void ApplyClientNetPackVisitor::visitInfoWindow(InfoWindow & pack)
+{
+	std::string str = pack.text.toString();
+
+	if(!callInterfaceIfPresent(cl, pack.player, &CGameInterface::showInfoDialog, pack.type, str, pack.components,(soundBase::soundID)pack.soundID))
+		logNetwork->warn("We received InfoWindow for not our player...");
+}
+
+void ApplyFirstClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & pack)
+{
+	//inform all players that see this object
+	for(auto it = cl.playerint.cbegin(); it != cl.playerint.cend(); ++it)
+	{
+		if(gs.isVisible(gs.getObjInstance(pack.id), it->first))
+			callInterfaceIfPresent(cl, it->first, &IGameEventsReceiver::beforeObjectPropertyChanged, &pack);
+	}
+
+	// invalidate section of map view with our object and force an update with new flag color
+	if (pack.what == ObjProperty::OWNER)
+	{
+		auto object = gs.getObjInstance(pack.id);
+		CGI->mh->onObjectInstantRemove(object, object->getOwner());
+	}
+}
+
+void ApplyClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & pack)
+{
+	//inform all players that see this object
+	for(auto it = cl.playerint.cbegin(); it != cl.playerint.cend(); ++it)
+	{
+		if(gs.isVisible(gs.getObjInstance(pack.id), it->first))
+			callInterfaceIfPresent(cl, it->first, &IGameEventsReceiver::objectPropertyChanged, &pack);
+	}
+
+	// invalidate section of map view with our object and force an update with new flag color
+	if (pack.what == ObjProperty::OWNER)
+	{
+		auto object = gs.getObjInstance(pack.id);
+		CGI->mh->onObjectInstantAdd(object, object->getOwner());
+	}
+}
+
+void ApplyClientNetPackVisitor::visitHeroLevelUp(HeroLevelUp & pack)
+{
+	const CGHeroInstance * hero = cl.getHero(pack.heroId);
+	assert(hero);
+	callOnlyThatInterface(cl, pack.player, &CGameInterface::heroGotLevel, hero, pack.primskill, pack.skills, pack.queryID);
+}
+
+void ApplyClientNetPackVisitor::visitCommanderLevelUp(CommanderLevelUp & pack)
+{
+	const CGHeroInstance * hero = cl.getHero(pack.heroId);
+	assert(hero);
+	const CCommanderInstance * commander = hero->commander;
+	assert(commander);
+	assert(commander->armyObj); //is it possible for Commander to exist beyond armed instance?
+	callOnlyThatInterface(cl, pack.player, &CGameInterface::commanderGotLevel, commander, pack.skills, pack.queryID);
+}
+
+void ApplyClientNetPackVisitor::visitBlockingDialog(BlockingDialog & pack)
+{
+	std::string str = pack.text.toString();
+
+	if(!callOnlyThatInterface(cl, pack.player, &CGameInterface::showBlockingDialog, str, pack.components, pack.queryID, (soundBase::soundID)pack.soundID, pack.selection(), pack.cancel()))
+		logNetwork->warn("We received YesNoDialog for not our player...");
+}
+
+void ApplyClientNetPackVisitor::visitGarrisonDialog(GarrisonDialog & pack)
+{
+	const CGHeroInstance *h = cl.getHero(pack.hid);
+	const CArmedInstance *obj = static_cast<const CArmedInstance*>(cl.getObj(pack.objid));
+
+	callOnlyThatInterface(cl, h->getOwner(), &CGameInterface::showGarrisonDialog, obj, h, pack.removableUnits, pack.queryID);
+}
+
+void ApplyClientNetPackVisitor::visitExchangeDialog(ExchangeDialog & pack)
+{
+	callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::heroExchangeStarted, pack.hero1, pack.hero2, pack.queryID);
+}
+
+void ApplyClientNetPackVisitor::visitTeleportDialog(TeleportDialog & pack)
+{
+	const CGHeroInstance *h = cl.getHero(pack.hero);
+	callOnlyThatInterface(cl, h->getOwner(), &CGameInterface::showTeleportDialog, h, pack.channel, pack.exits, pack.impassable, pack.queryID);
+}
+
+void ApplyClientNetPackVisitor::visitMapObjectSelectDialog(MapObjectSelectDialog & pack)
+{
+	callOnlyThatInterface(cl, pack.player, &CGameInterface::showMapObjectSelectDialog, pack.queryID, pack.icon, pack.title, pack.description, pack.objects);
+}
+
+void ApplyFirstClientNetPackVisitor::visitBattleStart(BattleStart & pack)
+{
+	// Cannot use the usual code because curB is not set yet
+	callOnlyThatBattleInterface(cl, pack.info->sides[0].color, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject,
+		pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero);
+	callOnlyThatBattleInterface(cl, pack.info->sides[1].color, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject,
+		pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero);
+	callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject,
+		pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero);
+}
+
+void ApplyClientNetPackVisitor::visitBattleStart(BattleStart & pack)
+{
+	cl.battleStarted(pack.info);
+}
+
+void ApplyFirstClientNetPackVisitor::visitBattleNextRound(BattleNextRound & pack)
+{
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleNewRoundFirst, pack.battleID);
+}
+
+void ApplyClientNetPackVisitor::visitBattleNextRound(BattleNextRound & pack)
+{
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleNewRound, pack.battleID);
+}
+
+void ApplyClientNetPackVisitor::visitBattleSetActiveStack(BattleSetActiveStack & pack)
+{
+	if(!pack.askPlayerInterface)
+		return;
+
+	const CStack *activated = gs.getBattle(pack.battleID)->battleGetStackByID(pack.stack);
+	PlayerColor playerToCall; //pack.player that will move activated stack
+	if (activated->hasBonusOfType(BonusType::HYPNOTIZED))
+	{
+		playerToCall = (gs.getBattle(pack.battleID)->sides[0].color == activated->unitOwner()
+			? gs.getBattle(pack.battleID)->sides[1].color
+			: gs.getBattle(pack.battleID)->sides[0].color);
+	}
+	else
+	{
+		playerToCall = activated->unitOwner();
+	}
+
+	cl.startPlayerBattleAction(pack.battleID, playerToCall);
+}
+
+void ApplyClientNetPackVisitor::visitBattleLogMessage(BattleLogMessage & pack)
+{
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleLogMessage, pack.battleID, pack.lines);
+}
+
+void ApplyClientNetPackVisitor::visitBattleTriggerEffect(BattleTriggerEffect & pack)
+{
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleTriggerEffect, pack.battleID, pack);
+}
+
+void ApplyFirstClientNetPackVisitor::visitBattleUpdateGateState(BattleUpdateGateState & pack)
+{
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleGateStateChanged, pack.battleID, pack.state);
+}
+
+void ApplyFirstClientNetPackVisitor::visitBattleResult(BattleResult & pack)
+{
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleEnd, pack.battleID, &pack, pack.queryID);
+	cl.battleFinished(pack.battleID);
+}
+
+void ApplyFirstClientNetPackVisitor::visitBattleStackMoved(BattleStackMoved & pack)
+{
+	const CStack * movedStack = gs.getBattle(pack.battleID)->battleGetStackByID(pack.stack);
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStackMoved, pack.battleID, movedStack, pack.tilesToMove, pack.distance, pack.teleporting);
+}
+
+void ApplyFirstClientNetPackVisitor::visitBattleAttack(BattleAttack & pack)
+{
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleAttack, pack.battleID, &pack);
+
+	// battleStacksAttacked should be excuted before BattleAttack.applyGs() to play animation before damaging unit
+	// so this has to be here instead of ApplyClientNetPackVisitor::visitBattleAttack()
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStacksAttacked, pack.battleID, pack.bsa, pack.shot());
+}
+
+void ApplyClientNetPackVisitor::visitBattleAttack(BattleAttack & pack)
+{
+}
+
+void ApplyFirstClientNetPackVisitor::visitStartAction(StartAction & pack)
+{
+	cl.currentBattleAction = std::make_unique<BattleAction>(pack.ba);
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::actionStarted, pack.battleID, pack.ba);
+}
+
+void ApplyClientNetPackVisitor::visitBattleSpellCast(BattleSpellCast & pack)
+{
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleSpellCast, pack.battleID, &pack);
+}
+
+void ApplyClientNetPackVisitor::visitSetStackEffect(SetStackEffect & pack)
+{
+	//informing about effects
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStacksEffectsSet, pack.battleID, pack);
+}
+
+void ApplyClientNetPackVisitor::visitStacksInjured(StacksInjured & pack)
+{
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStacksAttacked, pack.battleID, pack.stacks, false);
+}
+
+void ApplyClientNetPackVisitor::visitBattleResultsApplied(BattleResultsApplied & pack)
+{
+	callInterfaceIfPresent(cl, pack.player1, &IGameEventsReceiver::battleResultsApplied);
+	callInterfaceIfPresent(cl, pack.player2, &IGameEventsReceiver::battleResultsApplied);
+	callInterfaceIfPresent(cl, PlayerColor::SPECTATOR, &IGameEventsReceiver::battleResultsApplied);
+}
+
+void ApplyClientNetPackVisitor::visitBattleUnitsChanged(BattleUnitsChanged & pack)
+{
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleUnitsChanged, pack.battleID, pack.changedStacks);
+}
+
+void ApplyClientNetPackVisitor::visitBattleObstaclesChanged(BattleObstaclesChanged & pack)
+{
+	//inform interfaces about removed obstacles
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleObstaclesChanged, pack.battleID, pack.changes);
+}
+
+void ApplyClientNetPackVisitor::visitCatapultAttack(CatapultAttack & pack)
+{
+	//inform interfaces about catapult attack
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleCatapultAttacked, pack.battleID, pack);
+}
+
+void ApplyClientNetPackVisitor::visitEndAction(EndAction & pack)
+{
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::actionFinished, pack.battleID, *cl.currentBattleAction);
+	cl.currentBattleAction.reset();
+}
+
+void ApplyClientNetPackVisitor::visitPackageApplied(PackageApplied & pack)
+{
+	callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::requestRealized, &pack);
+	if(!CClient::waitingRequest.tryRemovingElement(pack.requestID))
+		logNetwork->warn("Surprising server message! PackageApplied for unknown requestID!");
+}
+
+void ApplyClientNetPackVisitor::visitSystemMessage(SystemMessage & pack)
+{
+	std::ostringstream str;
+	str << "System message: " << pack.text;
+
+	logNetwork->error(str.str()); // usually used to receive error messages from server
+	if(LOCPLINT && !settings["session"]["hideSystemMessages"].Bool())
+		LOCPLINT->cingconsole->print(str.str());
+}
+
+void ApplyClientNetPackVisitor::visitPlayerBlocked(PlayerBlocked & pack)
+{
+	callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::playerBlocked, pack.reason, pack.startOrEnd == PlayerBlocked::BLOCKADE_STARTED);
+}
+
+void ApplyClientNetPackVisitor::visitPlayerStartsTurn(PlayerStartsTurn & pack)
+{
+	logNetwork->debug("Server gives turn to %s", pack.player.toString());
+
+	callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, pack.player);
+	callOnlyThatInterface(cl, pack.player, &CGameInterface::yourTurn, pack.queryID);
+}
+
+void ApplyClientNetPackVisitor::visitPlayerEndsTurn(PlayerEndsTurn & pack)
+{
+	logNetwork->debug("Server ends turn of %s", pack.player.toString());
+
+	callAllInterfaces(cl, &IGameEventsReceiver::playerEndsTurn, pack.player);
+}
+
+void ApplyClientNetPackVisitor::visitTurnTimeUpdate(TurnTimeUpdate & pack)
+{
+	logNetwork->debug("Server sets turn timer {turn: %d, base: %d, battle: %d, creature: %d} for %s", pack.turnTimer.turnTimer, pack.turnTimer.baseTimer, pack.turnTimer.battleTimer, pack.turnTimer.creatureTimer, pack.player.toString());
+}
+
+void ApplyClientNetPackVisitor::visitPlayerMessageClient(PlayerMessageClient & pack)
+{
+	logNetwork->debug("pack.player %s sends a message: %s", pack.player.toString(), pack.text);
+
+	std::ostringstream str;
+	if(pack.player.isSpectator())
+		str << "Spectator: " << pack.text;
+	else
+		str << cl.getPlayerState(pack.player)->nodeName() <<": " << pack.text;
+	if(LOCPLINT)
+		LOCPLINT->cingconsole->print(str.str());
+}
+
+void ApplyClientNetPackVisitor::visitAdvmapSpellCast(AdvmapSpellCast & pack)
+{
+	cl.invalidatePaths();
+	auto caster = cl.getHero(pack.casterID);
+	if(caster)
+		//consider notifying other interfaces that see hero?
+		callInterfaceIfPresent(cl, caster->getOwner(), &IGameEventsReceiver::advmapSpellCast, caster, pack.spellID);
+	else
+		logNetwork->error("Invalid hero instance");
+}
+
+void ApplyClientNetPackVisitor::visitShowWorldViewEx(ShowWorldViewEx & pack)
+{
+	callOnlyThatInterface(cl, pack.player, &CGameInterface::showWorldViewEx, pack.objectPositions, pack.showTerrain);
+}
+
+void ApplyClientNetPackVisitor::visitOpenWindow(OpenWindow & pack)
+{
+	switch(pack.window)
+	{
+	case EOpenWindowMode::RECRUITMENT_FIRST:
+	case EOpenWindowMode::RECRUITMENT_ALL:
+		{
+			const CGDwelling *dw = dynamic_cast<const CGDwelling*>(cl.getObj(ObjectInstanceID(pack.object)));
+			const CArmedInstance *dst = dynamic_cast<const CArmedInstance*>(cl.getObj(ObjectInstanceID(pack.visitor)));
+			callInterfaceIfPresent(cl, dst->tempOwner, &IGameEventsReceiver::showRecruitmentDialog, dw, dst, pack.window == EOpenWindowMode::RECRUITMENT_FIRST ? 0 : -1, pack.queryID);
+		}
+		break;
+	case EOpenWindowMode::SHIPYARD_WINDOW:
+		{
+			assert(pack.queryID == QueryID::NONE);
+			const IShipyard *sy = IShipyard::castFrom(cl.getObj(ObjectInstanceID(pack.object)));
+			callInterfaceIfPresent(cl, sy->getObject()->getOwner(), &IGameEventsReceiver::showShipyardDialog, sy);
+		}
+		break;
+	case EOpenWindowMode::THIEVES_GUILD:
+		{
+			assert(pack.queryID == QueryID::NONE);
+			//displays Thieves' Guild window (when hero enters Den of Thieves)
+			const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object));
+			const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor));
+			callInterfaceIfPresent(cl, hero->getOwner(), &IGameEventsReceiver::showThievesGuildWindow, obj);
+		}
+		break;
+	case EOpenWindowMode::UNIVERSITY_WINDOW:
+		{
+			//displays University window (when hero enters University on adventure map)
+			const IMarket *market = IMarket::castFrom(cl.getObj(ObjectInstanceID(pack.object)));
+			const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor));
+			callInterfaceIfPresent(cl, hero->tempOwner, &IGameEventsReceiver::showUniversityWindow, market, hero, pack.queryID);
+		}
+		break;
+	case EOpenWindowMode::MARKET_WINDOW:
+		{
+			//displays Thieves' Guild window (when hero enters Den of Thieves)
+			const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object));
+			const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor));
+			const IMarket *market = IMarket::castFrom(obj);
+			callInterfaceIfPresent(cl, cl.getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showMarketWindow, market, hero, pack.queryID);
+		}
+		break;
+	case EOpenWindowMode::HILL_FORT_WINDOW:
+		{
+			assert(pack.queryID == QueryID::NONE);
+			//displays Hill fort window
+			const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object));
+			const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor));
+			callInterfaceIfPresent(cl, cl.getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showHillFortWindow, obj, hero);
+		}
+		break;
+	case EOpenWindowMode::PUZZLE_MAP:
+		{
+			assert(pack.queryID == QueryID::NONE);
+			const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor));
+			callInterfaceIfPresent(cl, hero->getOwner(), &IGameEventsReceiver::showPuzzleMap);
+		}
+		break;
+	case EOpenWindowMode::TAVERN_WINDOW:
+		{
+			const CGObjectInstance *obj1 = cl.getObj(ObjectInstanceID(pack.object));
+			const CGHeroInstance * hero = cl.getHero(ObjectInstanceID(pack.visitor));
+			callInterfaceIfPresent(cl, hero->tempOwner, &IGameEventsReceiver::showTavernWindow, obj1, hero, pack.queryID);
+		}
+		break;
+	}
+}
+
+void ApplyClientNetPackVisitor::visitCenterView(CenterView & pack)
+{
+	callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::centerView, pack.pos, pack.focusTime);
+}
+
+void ApplyClientNetPackVisitor::visitNewObject(NewObject & pack)
+{
+	cl.invalidatePaths();
+
+	const CGObjectInstance *obj = cl.getObj(pack.createdObjectID);
+	if(CGI->mh)
+		CGI->mh->onObjectFadeIn(obj, pack.initiator);
+
+	for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++)
+	{
+		if(gs.isVisible(obj, i->first))
+			i->second->newObject(obj);
+	}
+	CGI->mh->waitForOngoingAnimations();
+}
+
+void ApplyClientNetPackVisitor::visitSetAvailableArtifacts(SetAvailableArtifacts & pack)
+{
+	if(pack.id < 0) //artifact merchants globally
+	{
+		callAllInterfaces(cl, &IGameEventsReceiver::availableArtifactsChanged, nullptr);
+	}
+	else
+	{
+		const CGBlackMarket *bm = dynamic_cast<const CGBlackMarket *>(cl.getObj(ObjectInstanceID(pack.id)));
+		assert(bm);
+		callInterfaceIfPresent(cl, cl.getTile(bm->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::availableArtifactsChanged, bm);
+	}
+}
+
+
+void ApplyClientNetPackVisitor::visitEntitiesChanged(EntitiesChanged & pack)
+{
+	cl.invalidatePaths();
+}

+ 285 - 285
client/PlayerLocalState.cpp

@@ -1,285 +1,285 @@
-/*
- * PlayerLocalState.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 "PlayerLocalState.h"
-
-#include "../CCallback.h"
-#include "../lib/mapObjects/CGHeroInstance.h"
-#include "../lib/mapObjects/CGTownInstance.h"
-#include "../lib/pathfinder/CGPathNode.h"
-#include "CPlayerInterface.h"
-#include "adventureMap/AdventureMapInterface.h"
-
-PlayerLocalState::PlayerLocalState(CPlayerInterface & owner)
-	: owner(owner)
-	, currentSelection(nullptr)
-{
-}
-
-void PlayerLocalState::saveHeroPaths(std::map<const CGHeroInstance *, int3> & pathsMap)
-{
-	for(auto & p : paths)
-	{
-		if(p.second.nodes.size())
-			pathsMap[p.first] = p.second.endPos();
-		else
-			logGlobal->debug("%s has assigned an empty path! Ignoring it...", p.first->getNameTranslated());
-	}
-}
-
-void PlayerLocalState::loadHeroPaths(std::map<const CGHeroInstance *, int3> & pathsMap)
-{
-	if(owner.cb)
-	{
-		for(auto & p : pathsMap)
-		{
-			CGPath path;
-			owner.cb->getPathsInfo(p.first)->getPath(path, p.second);
-			paths[p.first] = path;
-			logGlobal->trace("Restored path for hero %s leading to %s with %d nodes", p.first->nodeName(), p.second.toString(), path.nodes.size());
-		}
-	}
-}
-
-void PlayerLocalState::setPath(const CGHeroInstance * h, const CGPath & path)
-{
-	paths[h] = path;
-}
-
-const CGPath & PlayerLocalState::getPath(const CGHeroInstance * h) const
-{
-	assert(hasPath(h));
-	return paths.at(h);
-}
-
-bool PlayerLocalState::hasPath(const CGHeroInstance * h) const
-{
-	return paths.count(h) > 0;
-}
-
-bool PlayerLocalState::setPath(const CGHeroInstance * h, const int3 & destination)
-{
-	CGPath path;
-	if(!owner.cb->getPathsInfo(h)->getPath(path, destination))
-	{
-		paths.erase(h); //invalidate previously possible path if selected (before other hero blocked only path / fly spell expired)
-		return false;
-	}
-
-	setPath(h, path);
-	return true;
-}
-
-void PlayerLocalState::removeLastNode(const CGHeroInstance * h)
-{
-	assert(hasPath(h));
-	if(!hasPath(h))
-		return;
-
-	auto & path = paths[h];
-	path.nodes.pop_back();
-	if(path.nodes.size() < 2) //if it was the last one, remove entire path and path with only one tile is not a real path
-		erasePath(h);
-}
-
-void PlayerLocalState::erasePath(const CGHeroInstance * h)
-{
-	paths.erase(h);
-	adventureInt->onHeroChanged(h);
-}
-
-void PlayerLocalState::verifyPath(const CGHeroInstance * h)
-{
-	if(!hasPath(h))
-		return;
-	setPath(h, getPath(h).endPos());
-}
-
-const CGHeroInstance * PlayerLocalState::getCurrentHero() const
-{
-	if(currentSelection && currentSelection->ID == Obj::HERO)
-		return dynamic_cast<const CGHeroInstance *>(currentSelection);
-	else
-		return nullptr;
-}
-
-const CGHeroInstance * PlayerLocalState::getNextWanderingHero(const CGHeroInstance * currentHero)
-{
-	bool currentHeroFound = false;
-	const CGHeroInstance * firstSuitable = nullptr;
-	const CGHeroInstance * nextSuitable = nullptr;
-
-	for(const auto * hero : getWanderingHeroes())
-	{
-		if (hero == currentHero)
-		{
-			currentHeroFound = true;
-			continue;
-		}
-
-		if (isHeroSleeping(hero))
-			continue;
-
-		if (hero->movementPointsRemaining() == 0)
-			continue;
-
-		if (!firstSuitable)
-			firstSuitable = hero;
-
-		if (!nextSuitable && currentHeroFound)
-			nextSuitable = hero;
-	}
-
-	// if we found suitable hero after currently selected hero -> return this hero
-	if (nextSuitable)
-		return nextSuitable;
-
-	// othervice -> loop over and return first suitable hero in the list (or null if none)
-	return firstSuitable;
-}
-
-const CGTownInstance * PlayerLocalState::getCurrentTown() const
-{
-	if(currentSelection && currentSelection->ID == Obj::TOWN)
-		return dynamic_cast<const CGTownInstance *>(currentSelection);
-	else
-		return nullptr;
-}
-
-const CArmedInstance * PlayerLocalState::getCurrentArmy() const
-{
-	if(currentSelection)
-		return dynamic_cast<const CArmedInstance *>(currentSelection);
-	else
-		return nullptr;
-}
-
-void PlayerLocalState::setSelection(const CArmedInstance * selection)
-{
-	if (currentSelection == selection)
-		return;
-
-	currentSelection = selection;
-
-	if (selection)
-		adventureInt->onSelectionChanged(selection);
-}
-
-bool PlayerLocalState::isHeroSleeping(const CGHeroInstance * hero) const
-{
-	return vstd::contains(sleepingHeroes, hero);
-}
-
-void PlayerLocalState::setHeroAsleep(const CGHeroInstance * hero)
-{
-	assert(hero);
-	assert(vstd::contains(wanderingHeroes, hero));
-	assert(!vstd::contains(sleepingHeroes, hero));
-
-	sleepingHeroes.push_back(hero);
-}
-
-void PlayerLocalState::setHeroAwaken(const CGHeroInstance * hero)
-{
-	assert(hero);
-	assert(vstd::contains(wanderingHeroes, hero));
-	assert(vstd::contains(sleepingHeroes, hero));
-
-	vstd::erase(sleepingHeroes, hero);
-}
-
-const std::vector<const CGHeroInstance *> & PlayerLocalState::getWanderingHeroes()
-{
-	return wanderingHeroes;
-}
-
-const CGHeroInstance * PlayerLocalState::getWanderingHero(size_t index)
-{
-	if(index < wanderingHeroes.size())
-		return wanderingHeroes[index];
-	return nullptr;
-}
-
-void PlayerLocalState::addWanderingHero(const CGHeroInstance * hero)
-{
-	assert(hero);
-	assert(!vstd::contains(wanderingHeroes, hero));
-	wanderingHeroes.push_back(hero);
-}
-
-void PlayerLocalState::removeWanderingHero(const CGHeroInstance * hero)
-{
-	assert(hero);
-	assert(vstd::contains(wanderingHeroes, hero));
-
-	if (hero == currentSelection)
-	{
-		auto const * nextHero = getNextWanderingHero(hero);
-		setSelection(nextHero);
-	}
-
-	vstd::erase(wanderingHeroes, hero);
-	vstd::erase(sleepingHeroes, hero);
-
-	if (currentSelection == nullptr && !wanderingHeroes.empty())
-		setSelection(wanderingHeroes.front());
-
-	if (currentSelection == nullptr && !ownedTowns.empty())
-		setSelection(ownedTowns.front());
-}
-
-void PlayerLocalState::swapWanderingHero(int pos1, int pos2)
-{
-	assert(wanderingHeroes[pos1] && wanderingHeroes[pos2]);
-	std::swap(wanderingHeroes[pos1], wanderingHeroes[pos2]);
-}
-
-const std::vector<const CGTownInstance *> & PlayerLocalState::getOwnedTowns()
-{
-	return ownedTowns;
-}
-
-const CGTownInstance * PlayerLocalState::getOwnedTown(size_t index)
-{
-	if(index < ownedTowns.size())
-		return ownedTowns[index];
-	return nullptr;
-}
-
-void PlayerLocalState::addOwnedTown(const CGTownInstance * town)
-{
-	assert(town);
-	assert(!vstd::contains(ownedTowns, town));
-	ownedTowns.push_back(town);
-}
-
-void PlayerLocalState::removeOwnedTown(const CGTownInstance * town)
-{
-	assert(town);
-	assert(vstd::contains(ownedTowns, town));
-	vstd::erase(ownedTowns, town);
-
-	if (town == currentSelection)
-		setSelection(nullptr);
-
-	if (currentSelection == nullptr && !wanderingHeroes.empty())
-		setSelection(wanderingHeroes.front());
-
-	if (currentSelection == nullptr && !ownedTowns.empty())
-		setSelection(ownedTowns.front());
-}
-
-void PlayerLocalState::swapOwnedTowns(int pos1, int pos2)
-{
-	assert(ownedTowns[pos1] && ownedTowns[pos2]);
-	std::swap(ownedTowns[pos1], ownedTowns[pos2]);
-
-	adventureInt->onTownOrderChanged();
-}
+/*
+ * PlayerLocalState.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 "PlayerLocalState.h"
+
+#include "../CCallback.h"
+#include "../lib/mapObjects/CGHeroInstance.h"
+#include "../lib/mapObjects/CGTownInstance.h"
+#include "../lib/pathfinder/CGPathNode.h"
+#include "CPlayerInterface.h"
+#include "adventureMap/AdventureMapInterface.h"
+
+PlayerLocalState::PlayerLocalState(CPlayerInterface & owner)
+	: owner(owner)
+	, currentSelection(nullptr)
+{
+}
+
+void PlayerLocalState::saveHeroPaths(std::map<const CGHeroInstance *, int3> & pathsMap)
+{
+	for(auto & p : paths)
+	{
+		if(p.second.nodes.size())
+			pathsMap[p.first] = p.second.endPos();
+		else
+			logGlobal->debug("%s has assigned an empty path! Ignoring it...", p.first->getNameTranslated());
+	}
+}
+
+void PlayerLocalState::loadHeroPaths(std::map<const CGHeroInstance *, int3> & pathsMap)
+{
+	if(owner.cb)
+	{
+		for(auto & p : pathsMap)
+		{
+			CGPath path;
+			owner.cb->getPathsInfo(p.first)->getPath(path, p.second);
+			paths[p.first] = path;
+			logGlobal->trace("Restored path for hero %s leading to %s with %d nodes", p.first->nodeName(), p.second.toString(), path.nodes.size());
+		}
+	}
+}
+
+void PlayerLocalState::setPath(const CGHeroInstance * h, const CGPath & path)
+{
+	paths[h] = path;
+}
+
+const CGPath & PlayerLocalState::getPath(const CGHeroInstance * h) const
+{
+	assert(hasPath(h));
+	return paths.at(h);
+}
+
+bool PlayerLocalState::hasPath(const CGHeroInstance * h) const
+{
+	return paths.count(h) > 0;
+}
+
+bool PlayerLocalState::setPath(const CGHeroInstance * h, const int3 & destination)
+{
+	CGPath path;
+	if(!owner.cb->getPathsInfo(h)->getPath(path, destination))
+	{
+		paths.erase(h); //invalidate previously possible path if selected (before other hero blocked only path / fly spell expired)
+		return false;
+	}
+
+	setPath(h, path);
+	return true;
+}
+
+void PlayerLocalState::removeLastNode(const CGHeroInstance * h)
+{
+	assert(hasPath(h));
+	if(!hasPath(h))
+		return;
+
+	auto & path = paths[h];
+	path.nodes.pop_back();
+	if(path.nodes.size() < 2) //if it was the last one, remove entire path and path with only one tile is not a real path
+		erasePath(h);
+}
+
+void PlayerLocalState::erasePath(const CGHeroInstance * h)
+{
+	paths.erase(h);
+	adventureInt->onHeroChanged(h);
+}
+
+void PlayerLocalState::verifyPath(const CGHeroInstance * h)
+{
+	if(!hasPath(h))
+		return;
+	setPath(h, getPath(h).endPos());
+}
+
+const CGHeroInstance * PlayerLocalState::getCurrentHero() const
+{
+	if(currentSelection && currentSelection->ID == Obj::HERO)
+		return dynamic_cast<const CGHeroInstance *>(currentSelection);
+	else
+		return nullptr;
+}
+
+const CGHeroInstance * PlayerLocalState::getNextWanderingHero(const CGHeroInstance * currentHero)
+{
+	bool currentHeroFound = false;
+	const CGHeroInstance * firstSuitable = nullptr;
+	const CGHeroInstance * nextSuitable = nullptr;
+
+	for(const auto * hero : getWanderingHeroes())
+	{
+		if (hero == currentHero)
+		{
+			currentHeroFound = true;
+			continue;
+		}
+
+		if (isHeroSleeping(hero))
+			continue;
+
+		if (hero->movementPointsRemaining() == 0)
+			continue;
+
+		if (!firstSuitable)
+			firstSuitable = hero;
+
+		if (!nextSuitable && currentHeroFound)
+			nextSuitable = hero;
+	}
+
+	// if we found suitable hero after currently selected hero -> return this hero
+	if (nextSuitable)
+		return nextSuitable;
+
+	// othervice -> loop over and return first suitable hero in the list (or null if none)
+	return firstSuitable;
+}
+
+const CGTownInstance * PlayerLocalState::getCurrentTown() const
+{
+	if(currentSelection && currentSelection->ID == Obj::TOWN)
+		return dynamic_cast<const CGTownInstance *>(currentSelection);
+	else
+		return nullptr;
+}
+
+const CArmedInstance * PlayerLocalState::getCurrentArmy() const
+{
+	if(currentSelection)
+		return dynamic_cast<const CArmedInstance *>(currentSelection);
+	else
+		return nullptr;
+}
+
+void PlayerLocalState::setSelection(const CArmedInstance * selection)
+{
+	if (currentSelection == selection)
+		return;
+
+	currentSelection = selection;
+
+	if (selection)
+		adventureInt->onSelectionChanged(selection);
+}
+
+bool PlayerLocalState::isHeroSleeping(const CGHeroInstance * hero) const
+{
+	return vstd::contains(sleepingHeroes, hero);
+}
+
+void PlayerLocalState::setHeroAsleep(const CGHeroInstance * hero)
+{
+	assert(hero);
+	assert(vstd::contains(wanderingHeroes, hero));
+	assert(!vstd::contains(sleepingHeroes, hero));
+
+	sleepingHeroes.push_back(hero);
+}
+
+void PlayerLocalState::setHeroAwaken(const CGHeroInstance * hero)
+{
+	assert(hero);
+	assert(vstd::contains(wanderingHeroes, hero));
+	assert(vstd::contains(sleepingHeroes, hero));
+
+	vstd::erase(sleepingHeroes, hero);
+}
+
+const std::vector<const CGHeroInstance *> & PlayerLocalState::getWanderingHeroes()
+{
+	return wanderingHeroes;
+}
+
+const CGHeroInstance * PlayerLocalState::getWanderingHero(size_t index)
+{
+	if(index < wanderingHeroes.size())
+		return wanderingHeroes[index];
+	return nullptr;
+}
+
+void PlayerLocalState::addWanderingHero(const CGHeroInstance * hero)
+{
+	assert(hero);
+	assert(!vstd::contains(wanderingHeroes, hero));
+	wanderingHeroes.push_back(hero);
+}
+
+void PlayerLocalState::removeWanderingHero(const CGHeroInstance * hero)
+{
+	assert(hero);
+	assert(vstd::contains(wanderingHeroes, hero));
+
+	if (hero == currentSelection)
+	{
+		auto const * nextHero = getNextWanderingHero(hero);
+		setSelection(nextHero);
+	}
+
+	vstd::erase(wanderingHeroes, hero);
+	vstd::erase(sleepingHeroes, hero);
+
+	if (currentSelection == nullptr && !wanderingHeroes.empty())
+		setSelection(wanderingHeroes.front());
+
+	if (currentSelection == nullptr && !ownedTowns.empty())
+		setSelection(ownedTowns.front());
+}
+
+void PlayerLocalState::swapWanderingHero(int pos1, int pos2)
+{
+	assert(wanderingHeroes[pos1] && wanderingHeroes[pos2]);
+	std::swap(wanderingHeroes[pos1], wanderingHeroes[pos2]);
+}
+
+const std::vector<const CGTownInstance *> & PlayerLocalState::getOwnedTowns()
+{
+	return ownedTowns;
+}
+
+const CGTownInstance * PlayerLocalState::getOwnedTown(size_t index)
+{
+	if(index < ownedTowns.size())
+		return ownedTowns[index];
+	return nullptr;
+}
+
+void PlayerLocalState::addOwnedTown(const CGTownInstance * town)
+{
+	assert(town);
+	assert(!vstd::contains(ownedTowns, town));
+	ownedTowns.push_back(town);
+}
+
+void PlayerLocalState::removeOwnedTown(const CGTownInstance * town)
+{
+	assert(town);
+	assert(vstd::contains(ownedTowns, town));
+	vstd::erase(ownedTowns, town);
+
+	if (town == currentSelection)
+		setSelection(nullptr);
+
+	if (currentSelection == nullptr && !wanderingHeroes.empty())
+		setSelection(wanderingHeroes.front());
+
+	if (currentSelection == nullptr && !ownedTowns.empty())
+		setSelection(ownedTowns.front());
+}
+
+void PlayerLocalState::swapOwnedTowns(int pos1, int pos2)
+{
+	assert(ownedTowns[pos1] && ownedTowns[pos2]);
+	std::swap(ownedTowns[pos1], ownedTowns[pos2]);
+
+	adventureInt->onTownOrderChanged();
+}

+ 113 - 113
client/PlayerLocalState.h

@@ -1,113 +1,113 @@
-/*
- * PlayerLocalState.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 CGHeroInstance;
-class CGTownInstance;
-class CArmedInstance;
-struct CGPath;
-class int3;
-
-VCMI_LIB_NAMESPACE_END
-
-class CPlayerInterface;
-
-/// Class that contains potentially serializeable state of a local player
-class PlayerLocalState
-{
-	CPlayerInterface & owner;
-
-	/// Currently selected object, can be town, hero or null
-	const CArmedInstance * currentSelection;
-
-	std::map<const CGHeroInstance *, CGPath> paths; //maps hero => selected path in adventure map
-	std::vector<const CGHeroInstance *> sleepingHeroes; //if hero is in here, he's sleeping
-	std::vector<const CGHeroInstance *> wanderingHeroes; //our heroes on the adventure map (not the garrisoned ones)
-	std::vector<const CGTownInstance *> ownedTowns; //our towns on the adventure map
-
-	void saveHeroPaths(std::map<const CGHeroInstance *, int3> & paths);
-	void loadHeroPaths(std::map<const CGHeroInstance *, int3> & paths);
-
-public:
-	struct SpellbookLastSetting
-	{
-		//on which page we left spellbook
-		int spellbookLastPageBattle = 0;
-		int spellbokLastPageAdvmap = 0;
-		int spellbookLastTabBattle = 4;
-		int spellbookLastTabAdvmap = 4;
-
-		template<typename Handler>
-		void serialize(Handler & h, const int version)
-		{
-			h & spellbookLastPageBattle;
-			h & spellbokLastPageAdvmap;
-			h & spellbookLastTabBattle;
-			h & spellbookLastTabAdvmap;
-		}
-	} spellbookSettings;
-
-	explicit PlayerLocalState(CPlayerInterface & owner);
-
-	bool isHeroSleeping(const CGHeroInstance * hero) const;
-	void setHeroAsleep(const CGHeroInstance * hero);
-	void setHeroAwaken(const CGHeroInstance * hero);
-
-	const std::vector<const CGTownInstance *> & getOwnedTowns();
-	const CGTownInstance * getOwnedTown(size_t index);
-	void addOwnedTown(const CGTownInstance * hero);
-	void removeOwnedTown(const CGTownInstance * hero);
-	void swapOwnedTowns(int pos1, int pos2);
-
-	const std::vector<const CGHeroInstance *> & getWanderingHeroes();
-	const CGHeroInstance * getWanderingHero(size_t index);
-	const CGHeroInstance * getNextWanderingHero(const CGHeroInstance * hero);
-	void addWanderingHero(const CGHeroInstance * hero);
-	void removeWanderingHero(const CGHeroInstance * hero);
-	void swapWanderingHero(int pos1, int pos2);
-
-	void setPath(const CGHeroInstance * h, const CGPath & path);
-	bool setPath(const CGHeroInstance * h, const int3 & destination);
-
-	const CGPath & getPath(const CGHeroInstance * h) const;
-	bool hasPath(const CGHeroInstance * h) const;
-
-	void removeLastNode(const CGHeroInstance * h);
-	void erasePath(const CGHeroInstance * h);
-	void verifyPath(const CGHeroInstance * h);
-
-	/// Returns currently selected object
-	const CGHeroInstance * getCurrentHero() const;
-	const CGTownInstance * getCurrentTown() const;
-	const CArmedInstance * getCurrentArmy() const;
-
-	/// Changes currently selected object
-	void setSelection(const CArmedInstance *sel);
-
-	template<typename Handler>
-	void serialize(Handler & h, int version)
-	{
-		//WARNING: this code is broken and not used. See CClient::loadGame
-		std::map<const CGHeroInstance *, int3> pathsMap; //hero -> dest
-		if(h.saving)
-			saveHeroPaths(pathsMap);
-
-		h & pathsMap;
-
-		if(!h.saving)
-			loadHeroPaths(pathsMap);
-
-		h & ownedTowns;
-		h & wanderingHeroes;
-		h & sleepingHeroes;
-	}
-};
+/*
+ * PlayerLocalState.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 CGHeroInstance;
+class CGTownInstance;
+class CArmedInstance;
+struct CGPath;
+class int3;
+
+VCMI_LIB_NAMESPACE_END
+
+class CPlayerInterface;
+
+/// Class that contains potentially serializeable state of a local player
+class PlayerLocalState
+{
+	CPlayerInterface & owner;
+
+	/// Currently selected object, can be town, hero or null
+	const CArmedInstance * currentSelection;
+
+	std::map<const CGHeroInstance *, CGPath> paths; //maps hero => selected path in adventure map
+	std::vector<const CGHeroInstance *> sleepingHeroes; //if hero is in here, he's sleeping
+	std::vector<const CGHeroInstance *> wanderingHeroes; //our heroes on the adventure map (not the garrisoned ones)
+	std::vector<const CGTownInstance *> ownedTowns; //our towns on the adventure map
+
+	void saveHeroPaths(std::map<const CGHeroInstance *, int3> & paths);
+	void loadHeroPaths(std::map<const CGHeroInstance *, int3> & paths);
+
+public:
+	struct SpellbookLastSetting
+	{
+		//on which page we left spellbook
+		int spellbookLastPageBattle = 0;
+		int spellbokLastPageAdvmap = 0;
+		int spellbookLastTabBattle = 4;
+		int spellbookLastTabAdvmap = 4;
+
+		template<typename Handler>
+		void serialize(Handler & h, const int version)
+		{
+			h & spellbookLastPageBattle;
+			h & spellbokLastPageAdvmap;
+			h & spellbookLastTabBattle;
+			h & spellbookLastTabAdvmap;
+		}
+	} spellbookSettings;
+
+	explicit PlayerLocalState(CPlayerInterface & owner);
+
+	bool isHeroSleeping(const CGHeroInstance * hero) const;
+	void setHeroAsleep(const CGHeroInstance * hero);
+	void setHeroAwaken(const CGHeroInstance * hero);
+
+	const std::vector<const CGTownInstance *> & getOwnedTowns();
+	const CGTownInstance * getOwnedTown(size_t index);
+	void addOwnedTown(const CGTownInstance * hero);
+	void removeOwnedTown(const CGTownInstance * hero);
+	void swapOwnedTowns(int pos1, int pos2);
+
+	const std::vector<const CGHeroInstance *> & getWanderingHeroes();
+	const CGHeroInstance * getWanderingHero(size_t index);
+	const CGHeroInstance * getNextWanderingHero(const CGHeroInstance * hero);
+	void addWanderingHero(const CGHeroInstance * hero);
+	void removeWanderingHero(const CGHeroInstance * hero);
+	void swapWanderingHero(int pos1, int pos2);
+
+	void setPath(const CGHeroInstance * h, const CGPath & path);
+	bool setPath(const CGHeroInstance * h, const int3 & destination);
+
+	const CGPath & getPath(const CGHeroInstance * h) const;
+	bool hasPath(const CGHeroInstance * h) const;
+
+	void removeLastNode(const CGHeroInstance * h);
+	void erasePath(const CGHeroInstance * h);
+	void verifyPath(const CGHeroInstance * h);
+
+	/// Returns currently selected object
+	const CGHeroInstance * getCurrentHero() const;
+	const CGTownInstance * getCurrentTown() const;
+	const CArmedInstance * getCurrentArmy() const;
+
+	/// Changes currently selected object
+	void setSelection(const CArmedInstance *sel);
+
+	template<typename Handler>
+	void serialize(Handler & h, int version)
+	{
+		//WARNING: this code is broken and not used. See CClient::loadGame
+		std::map<const CGHeroInstance *, int3> pathsMap; //hero -> dest
+		if(h.saving)
+			saveHeroPaths(pathsMap);
+
+		h & pathsMap;
+
+		if(!h.saving)
+			loadHeroPaths(pathsMap);
+
+		h & ownedTowns;
+		h & wanderingHeroes;
+		h & sleepingHeroes;
+	}
+};

+ 1 - 1
client/StdInc.cpp

@@ -1,2 +1,2 @@
-// Creates the precompiled header
+// Creates the precompiled header
 #include "StdInc.h"

+ 9 - 9
client/StdInc.h

@@ -1,9 +1,9 @@
-#pragma once
-
-#include "../Global.h"
-
-// This header should be treated as a pre compiled header file(PCH) in the compiler building settings.
-
-// Here you can add specific libraries and macros which are specific to this project.
-
-VCMI_LIB_USING_NAMESPACE
+#pragma once
+
+#include "../Global.h"
+
+// This header should be treated as a pre compiled header file(PCH) in the compiler building settings.
+
+// Here you can add specific libraries and macros which are specific to this project.
+
+VCMI_LIB_USING_NAMESPACE

+ 895 - 895
client/adventureMap/AdventureMapInterface.cpp

@@ -1,895 +1,895 @@
-/*
- * 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 "TurnTimerWidget.h"
-#include "AdventureMapWidget.h"
-#include "AdventureMapShortcuts.h"
-
-#include "../mapView/mapHandler.h"
-#include "../mapView/MapView.h"
-#include "../windows/InfoWindows.h"
-#include "../widgets/RadialMenu.h"
-#include "../CGameInfo.h"
-#include "../gui/CursorHandler.h"
-#include "../gui/CGuiHandler.h"
-#include "../gui/Shortcut.h"
-#include "../gui/WindowHandler.h"
-#include "../render/Canvas.h"
-#include "../CMT.h"
-#include "../PlayerLocalState.h"
-#include "../CPlayerInterface.h"
-
-#include "../../CCallback.h"
-#include "../../lib/CConfigHandler.h"
-#include "../../lib/StartInfo.h"
-#include "../../lib/CGeneralTextHandler.h"
-#include "../../lib/spells/CSpellHandler.h"
-#include "../../lib/mapObjects/CGHeroInstance.h"
-#include "../../lib/mapObjects/CGTownInstance.h"
-#include "../../lib/mapping/CMapDefines.h"
-#include "../../lib/pathfinder/CGPathNode.h"
-
-std::shared_ptr<AdventureMapInterface> adventureInt;
-
-AdventureMapInterface::AdventureMapInterface():
-	mapAudio(new MapAudioPlayer()),
-	spellBeingCasted(nullptr),
-	scrollingWasActive(false),
-	scrollingWasBlocked(false),
-	backgroundDimLevel(settings["adventure"]["backgroundDimLevel"].Integer())
-{
-	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
-	pos.x = pos.y = 0;
-	pos.w = GH.screenDimensions().x;
-	pos.h = GH.screenDimensions().y;
-
-	shortcuts = std::make_shared<AdventureMapShortcuts>(*this);
-
-	widget = std::make_shared<AdventureMapWidget>(shortcuts);
-	shortcuts->setState(EAdventureState::MAKING_TURN);
-	widget->getMapView()->onViewMapActivated();
-
-	if(LOCPLINT->cb->getStartInfo()->turnTimerInfo.isEnabled() || LOCPLINT->cb->getStartInfo()->turnTimerInfo.isBattleEnabled())
-		watches = std::make_shared<TurnTimerWidget>();
-	
-	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)
-{
-	if (shortcuts->optionMapViewActive())
-	{
-		widget->getInfoBar()->popAll();
-		widget->getInfoBar()->showSelection();
-	}
-}
-
-void AdventureMapInterface::onHeroChanged(const CGHeroInstance *h)
-{
-	widget->getHeroList()->updateElement(h);
-
-	if (h && h == LOCPLINT->localState->getCurrentHero() && !widget->getInfoBar()->showingComponents())
-		widget->getInfoBar()->showSelection();
-
-	widget->updateActiveState();
-}
-
-void AdventureMapInterface::onTownChanged(const CGTownInstance * town)
-{
-	widget->getTownList()->updateElement(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
-
-	// workaround for an edge case:
-	// if player unequips Angel Wings / Boots of Levitation of currently active hero
-	// game will correctly invalidate paths but current route will not be updated since verifyPath() is not called for current hero
-	if (LOCPLINT->makingTurn && LOCPLINT->localState->getCurrentHero())
-		LOCPLINT->localState->verifyPath(LOCPLINT->localState->getCurrentHero());
-}
-
-void AdventureMapInterface::deactivate()
-{
-	CIntObject::deactivate();
-	CCS->curh->set(Cursor::Map::POINTER);
-
-	if(LOCPLINT)
-		LOCPLINT->cingconsole->deactivate();
-}
-
-void AdventureMapInterface::showAll(Canvas & to)
-{
-	CIntObject::showAll(to);
-	dim(to);
-	LOCPLINT->cingconsole->show(to);
-}
-
-void AdventureMapInterface::show(Canvas & to)
-{
-	CIntObject::show(to);
-	dim(to);
-	LOCPLINT->cingconsole->show(to);
-}
-
-void AdventureMapInterface::dim(Canvas & to)
-{
-	for (auto window : GH.windows().findWindows<IShowActivatable>())
-	{
-		if (!std::dynamic_pointer_cast<AdventureMapInterface>(window) && !std::dynamic_pointer_cast<RadialMenu>(window) && !window->isPopupWindow())
-		{
-			Rect targetRect(0, 0, GH.screenDimensions().x, GH.screenDimensions().y);
-			ColorRGBA colorToFill(0, 0, 0, std::clamp<int>(backgroundDimLevel, 0, 255));
-			if(backgroundDimLevel > 0)
-				to.drawColorBlended(targetRect, colorToFill);
-			return;
-		}
-	}
-}
-
-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 int32_t borderScrollWidth = 15;
-
-	int32_t scrollSpeedPixels = settings["adventure"]["scrollSpeedPixels"].Float();
-	int32_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 && shortcuts->optionMapScrollingActive() && !scrollingWasBlocked;
-	bool scrollingBlocked = GH.isKeyboardCtrlDown() || !settings["adventure"]["borderScroll"].Bool();
-
-	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()->updateWidget();;
-		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::onTownOrderChanged()
-{
-	widget->getTownList()->updateWidget();
-}
-
-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)
-{
-	backgroundDimLevel = 255;
-
-	onCurrentPlayerChanged(playerID);
-	setState(EAdventureState::HOTSEAT_WAIT);
-}
-
-void AdventureMapInterface::onEnemyTurnStarted(PlayerColor playerID, bool isHuman)
-{
-	if(settings["session"]["spectate"].Bool())
-		return;
-
-	mapAudio->onEnemyTurnStarted();
-	widget->getMinimap()->setAIRadar(!isHuman);
-	widget->getInfoBar()->startEnemyTurn(playerID);
-	setState(isHuman ? EAdventureState::OTHER_HUMAN_PLAYER_TURN : EAdventureState::AI_PLAYER_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());
-
-	widget->setInputEnabled(widgetMustBeActive);
-	widget->getMapView()->setInputEnabled(mapViewMustBeActive);
-}
-
-void AdventureMapInterface::onCurrentPlayerChanged(PlayerColor playerID)
-{
-	LOCPLINT->localState->setSelection(nullptr);
-
-	if (playerID == currentPlayerID)
-		return;
-
-	currentPlayerID = playerID;
-	widget->setPlayer(playerID);
-}
-
-void AdventureMapInterface::onPlayerTurnStarted(PlayerColor playerID)
-{
-	backgroundDimLevel = settings["adventure"]["backgroundDimLevel"].Integer();
-
-	onCurrentPlayerChanged(playerID);
-
-	setState(EAdventureState::MAKING_TURN);
-	if(playerID == LOCPLINT->playerID || settings["session"]["spectate"].Bool())
-	{
-		widget->getMinimap()->setAIRadar(false);
-		widget->getInfoBar()->showSelection();
-	}
-
-	widget->getHeroList()->updateWidget();
-	widget->getTownList()->updateWidget();
-
-	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, except for 1st day of the game
-	if (LOCPLINT->cb->getDate(Date::DAY) != 1)
-		widget->getInfoBar()->showDate();
-
-	onHeroChanged(nullptr);
-	Canvas canvas = Canvas::createFromSurface(screen);
-	showAll(canvas);
-	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;
-
-	if(!settings["general"]["startTurnAutosave"].Bool())
-	{
-		LOCPLINT->performAutosave();
-	}
-
-	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) != PlayerRelations::ENEMIES;
-
-	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
-			{
-				assert(!CGI->mh->hasOngoingAnimations());
-				if(!CGI->mh->hasOngoingAnimations() && LOCPLINT->localState->getPath(currentHero).nextNode().turns == 0)
-					LOCPLINT->moveHero(currentHero, LOCPLINT->localState->getPath(currentHero));
-				return;
-			}
-			else
-			{
-				if(GH.isKeyboardCtrlDown()) //normal click behaviour (as no hero selected)
-				{
-					if(canSelect)
-						LOCPLINT->localState->setSelection(static_cast<const CArmedInstance*>(topBlocking));
-				}
-				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 || GH.isKeyboardCtrlDown())
-	{
-		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 EPathNodeAction::NORMAL:
-		case EPathNodeAction::TELEPORT_NORMAL:
-			if(pathNode->layer == EPathfindingLayer::LAND)
-				CCS->curh->set(cursorMove[turns]);
-			else
-				CCS->curh->set(cursorSailVisit[turns]);
-			break;
-
-		case EPathNodeAction::VISIT:
-		case EPathNodeAction::BLOCKING_VISIT:
-		case EPathNodeAction::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 EPathNodeAction::BATTLE:
-		case EPathNodeAction::TELEPORT_BATTLE:
-			CCS->curh->set(cursorAttack[turns]);
-			break;
-
-		case EPathNodeAction::EMBARK:
-			CCS->curh->set(cursorSail[turns]);
-			break;
-
-		case EPathNodeAction::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.movementPointsLimit(pathNode.layer == EPathfindingLayer::LAND) : hero.movementPointsRemaining();
-	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;
-
-	// remember our activation state and reactive after reconstruction
-	// since othervice activate() calls for created elements will bypass virtual dispatch
-	// and will call directly CIntObject::activate() instead of dispatching virtual function call
-	bool widgetActive = isActive();
-
-	if (widgetActive)
-		deactivate();
-
-	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();
-
-	if (LOCPLINT && LOCPLINT->localState->getCurrentArmy())
-		widget->getMapView()->onCenteredObject(LOCPLINT->localState->getCurrentArmy());
-
-	adjustActiveness();
-
-	if (widgetActive)
-		activate();
-}
+/*
+ * 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 "TurnTimerWidget.h"
+#include "AdventureMapWidget.h"
+#include "AdventureMapShortcuts.h"
+
+#include "../mapView/mapHandler.h"
+#include "../mapView/MapView.h"
+#include "../windows/InfoWindows.h"
+#include "../widgets/RadialMenu.h"
+#include "../CGameInfo.h"
+#include "../gui/CursorHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
+#include "../gui/WindowHandler.h"
+#include "../render/Canvas.h"
+#include "../CMT.h"
+#include "../PlayerLocalState.h"
+#include "../CPlayerInterface.h"
+
+#include "../../CCallback.h"
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/StartInfo.h"
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/spells/CSpellHandler.h"
+#include "../../lib/mapObjects/CGHeroInstance.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
+#include "../../lib/mapping/CMapDefines.h"
+#include "../../lib/pathfinder/CGPathNode.h"
+
+std::shared_ptr<AdventureMapInterface> adventureInt;
+
+AdventureMapInterface::AdventureMapInterface():
+	mapAudio(new MapAudioPlayer()),
+	spellBeingCasted(nullptr),
+	scrollingWasActive(false),
+	scrollingWasBlocked(false),
+	backgroundDimLevel(settings["adventure"]["backgroundDimLevel"].Integer())
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	pos.x = pos.y = 0;
+	pos.w = GH.screenDimensions().x;
+	pos.h = GH.screenDimensions().y;
+
+	shortcuts = std::make_shared<AdventureMapShortcuts>(*this);
+
+	widget = std::make_shared<AdventureMapWidget>(shortcuts);
+	shortcuts->setState(EAdventureState::MAKING_TURN);
+	widget->getMapView()->onViewMapActivated();
+
+	if(LOCPLINT->cb->getStartInfo()->turnTimerInfo.isEnabled() || LOCPLINT->cb->getStartInfo()->turnTimerInfo.isBattleEnabled())
+		watches = std::make_shared<TurnTimerWidget>();
+	
+	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)
+{
+	if (shortcuts->optionMapViewActive())
+	{
+		widget->getInfoBar()->popAll();
+		widget->getInfoBar()->showSelection();
+	}
+}
+
+void AdventureMapInterface::onHeroChanged(const CGHeroInstance *h)
+{
+	widget->getHeroList()->updateElement(h);
+
+	if (h && h == LOCPLINT->localState->getCurrentHero() && !widget->getInfoBar()->showingComponents())
+		widget->getInfoBar()->showSelection();
+
+	widget->updateActiveState();
+}
+
+void AdventureMapInterface::onTownChanged(const CGTownInstance * town)
+{
+	widget->getTownList()->updateElement(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
+
+	// workaround for an edge case:
+	// if player unequips Angel Wings / Boots of Levitation of currently active hero
+	// game will correctly invalidate paths but current route will not be updated since verifyPath() is not called for current hero
+	if (LOCPLINT->makingTurn && LOCPLINT->localState->getCurrentHero())
+		LOCPLINT->localState->verifyPath(LOCPLINT->localState->getCurrentHero());
+}
+
+void AdventureMapInterface::deactivate()
+{
+	CIntObject::deactivate();
+	CCS->curh->set(Cursor::Map::POINTER);
+
+	if(LOCPLINT)
+		LOCPLINT->cingconsole->deactivate();
+}
+
+void AdventureMapInterface::showAll(Canvas & to)
+{
+	CIntObject::showAll(to);
+	dim(to);
+	LOCPLINT->cingconsole->show(to);
+}
+
+void AdventureMapInterface::show(Canvas & to)
+{
+	CIntObject::show(to);
+	dim(to);
+	LOCPLINT->cingconsole->show(to);
+}
+
+void AdventureMapInterface::dim(Canvas & to)
+{
+	for (auto window : GH.windows().findWindows<IShowActivatable>())
+	{
+		if (!std::dynamic_pointer_cast<AdventureMapInterface>(window) && !std::dynamic_pointer_cast<RadialMenu>(window) && !window->isPopupWindow())
+		{
+			Rect targetRect(0, 0, GH.screenDimensions().x, GH.screenDimensions().y);
+			ColorRGBA colorToFill(0, 0, 0, std::clamp<int>(backgroundDimLevel, 0, 255));
+			if(backgroundDimLevel > 0)
+				to.drawColorBlended(targetRect, colorToFill);
+			return;
+		}
+	}
+}
+
+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 int32_t borderScrollWidth = 15;
+
+	int32_t scrollSpeedPixels = settings["adventure"]["scrollSpeedPixels"].Float();
+	int32_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 && shortcuts->optionMapScrollingActive() && !scrollingWasBlocked;
+	bool scrollingBlocked = GH.isKeyboardCtrlDown() || !settings["adventure"]["borderScroll"].Bool();
+
+	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()->updateWidget();;
+		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::onTownOrderChanged()
+{
+	widget->getTownList()->updateWidget();
+}
+
+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)
+{
+	backgroundDimLevel = 255;
+
+	onCurrentPlayerChanged(playerID);
+	setState(EAdventureState::HOTSEAT_WAIT);
+}
+
+void AdventureMapInterface::onEnemyTurnStarted(PlayerColor playerID, bool isHuman)
+{
+	if(settings["session"]["spectate"].Bool())
+		return;
+
+	mapAudio->onEnemyTurnStarted();
+	widget->getMinimap()->setAIRadar(!isHuman);
+	widget->getInfoBar()->startEnemyTurn(playerID);
+	setState(isHuman ? EAdventureState::OTHER_HUMAN_PLAYER_TURN : EAdventureState::AI_PLAYER_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());
+
+	widget->setInputEnabled(widgetMustBeActive);
+	widget->getMapView()->setInputEnabled(mapViewMustBeActive);
+}
+
+void AdventureMapInterface::onCurrentPlayerChanged(PlayerColor playerID)
+{
+	LOCPLINT->localState->setSelection(nullptr);
+
+	if (playerID == currentPlayerID)
+		return;
+
+	currentPlayerID = playerID;
+	widget->setPlayer(playerID);
+}
+
+void AdventureMapInterface::onPlayerTurnStarted(PlayerColor playerID)
+{
+	backgroundDimLevel = settings["adventure"]["backgroundDimLevel"].Integer();
+
+	onCurrentPlayerChanged(playerID);
+
+	setState(EAdventureState::MAKING_TURN);
+	if(playerID == LOCPLINT->playerID || settings["session"]["spectate"].Bool())
+	{
+		widget->getMinimap()->setAIRadar(false);
+		widget->getInfoBar()->showSelection();
+	}
+
+	widget->getHeroList()->updateWidget();
+	widget->getTownList()->updateWidget();
+
+	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, except for 1st day of the game
+	if (LOCPLINT->cb->getDate(Date::DAY) != 1)
+		widget->getInfoBar()->showDate();
+
+	onHeroChanged(nullptr);
+	Canvas canvas = Canvas::createFromSurface(screen);
+	showAll(canvas);
+	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;
+
+	if(!settings["general"]["startTurnAutosave"].Bool())
+	{
+		LOCPLINT->performAutosave();
+	}
+
+	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) != PlayerRelations::ENEMIES;
+
+	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
+			{
+				assert(!CGI->mh->hasOngoingAnimations());
+				if(!CGI->mh->hasOngoingAnimations() && LOCPLINT->localState->getPath(currentHero).nextNode().turns == 0)
+					LOCPLINT->moveHero(currentHero, LOCPLINT->localState->getPath(currentHero));
+				return;
+			}
+			else
+			{
+				if(GH.isKeyboardCtrlDown()) //normal click behaviour (as no hero selected)
+				{
+					if(canSelect)
+						LOCPLINT->localState->setSelection(static_cast<const CArmedInstance*>(topBlocking));
+				}
+				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 || GH.isKeyboardCtrlDown())
+	{
+		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 EPathNodeAction::NORMAL:
+		case EPathNodeAction::TELEPORT_NORMAL:
+			if(pathNode->layer == EPathfindingLayer::LAND)
+				CCS->curh->set(cursorMove[turns]);
+			else
+				CCS->curh->set(cursorSailVisit[turns]);
+			break;
+
+		case EPathNodeAction::VISIT:
+		case EPathNodeAction::BLOCKING_VISIT:
+		case EPathNodeAction::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 EPathNodeAction::BATTLE:
+		case EPathNodeAction::TELEPORT_BATTLE:
+			CCS->curh->set(cursorAttack[turns]);
+			break;
+
+		case EPathNodeAction::EMBARK:
+			CCS->curh->set(cursorSail[turns]);
+			break;
+
+		case EPathNodeAction::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.movementPointsLimit(pathNode.layer == EPathfindingLayer::LAND) : hero.movementPointsRemaining();
+	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;
+
+	// remember our activation state and reactive after reconstruction
+	// since othervice activate() calls for created elements will bypass virtual dispatch
+	// and will call directly CIntObject::activate() instead of dispatching virtual function call
+	bool widgetActive = isActive();
+
+	if (widgetActive)
+		deactivate();
+
+	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();
+
+	if (LOCPLINT && LOCPLINT->localState->getCurrentArmy())
+		widget->getMapView()->onCenteredObject(LOCPLINT->localState->getCurrentArmy());
+
+	adjustActiveness();
+
+	if (widgetActive)
+		activate();
+}

+ 194 - 194
client/adventureMap/AdventureMapInterface.h

@@ -1,194 +1,194 @@
-/*
- * AdventureMapInterface.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 CGObjectInstance;
-class CGHeroInstance;
-class CGTownInstance;
-class CArmedInstance;
-class IShipyard;
-struct CGPathNode;
-struct ObjectPosInfo;
-struct Component;
-class int3;
-
-VCMI_LIB_NAMESPACE_END
-
-class CButton;
-class IImage;
-class CAnimImage;
-class CGStatusBar;
-class AdventureMapWidget;
-class AdventureMapShortcuts;
-class CAnimation;
-class MapView;
-class CResDataBar;
-class CHeroList;
-class CTownList;
-class CInfoBar;
-class CMinimap;
-class MapAudioPlayer;
-class TurnTimerWidget;
-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 AdventureMapInterface : public CIntObject
-{
-private:
-	/// currently acting player
-	PlayerColor currentPlayerID;
-
-	/// 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;
-
-	/// how much should the background dimmed, when windows are on the top
-	int backgroundDimLevel;
-
-	/// 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;
-	std::shared_ptr<TurnTimerWidget> watches;
-
-private:
-	void setState(EAdventureState state);
-
-	/// updates active state of game window whenever game state changes
-	void adjustActiveness();
-
-	/// checks if obj is our ashipyard and cursor is 0,0 -> returns shipyard or nullptr else
-	const IShipyard * ourInaccessibleShipyard(const CGObjectInstance *obj) const;
-
-	/// check and if necessary reacts on scrolling by moving cursor to screen edge
-	void handleMapScrollingUpdate(uint32_t msPassed);
-
-	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 exitCastingMode();
-
-	/// casts current spell at specified location
-	void performSpellcasting(const int3 & castTarget);
-
-	/// dim interface if some windows opened
-	void dim(Canvas & to);
-
-protected:
-	/// CIntObject interface implementation
-
-	void activate() override;
-	void deactivate() override;
-
-	void tick(uint32_t msPassed) override;
-	void show(Canvas & to) override;
-	void showAll(Canvas & to) override;
-
-	void keyPressed(EShortcut key) override;
-
-	void onScreenResize() override;
-
-public:
-	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);
-
-	/// Called by PlayerInterface when AI or remote human player starts his turn
-	void onEnemyTurnStarted(PlayerColor playerID, bool isHuman);
-
-	/// Called by PlayerInterface when local human player starts his turn
-	void onPlayerTurnStarted(PlayerColor playerID);
-
-	/// Called by PlayerInterface when interface should be switched to specified player without starting turn
-	void onCurrentPlayerChanged(PlayerColor playerID);
-
-	/// Called by PlayerInterface when specific map tile changed and must be updated on minimap
-	void onMapTilesChanged(boost::optional<std::unordered_set<int3>> positions);
-
-	/// Called by PlayerInterface when hero starts movement
-	void onHeroMovementStarted(const CGHeroInstance * hero);
-
-	/// Called by PlayerInterface when hero state changed and hero list must be updated
-	void onHeroChanged(const CGHeroInstance * hero);
-
-	/// Called by PlayerInterface when town state changed and town list must be updated
-	void onTownChanged(const CGTownInstance * town);
-
-	/// Called when currently selected object changes
-	void onSelectionChanged(const CArmedInstance *sel);
-
-	/// Called when town order changes
-	void onTownOrderChanged();
-
-	/// Called when map audio should be paused, e.g. on combat or town screen access
-	void onAudioPaused();
-
-	/// Called when map audio should be resume, opposite to onPaused
-	void onAudioResumed();
-
-	/// Requests to display provided information inside infobox
-	void showInfoBoxMessage(const std::vector<Component> & components, std::string message, int timer);
-
-	/// Changes position on map to center selected location
-	void centerOnTile(int3 on);
-	void centerOnObject(const CGObjectInstance *obj);
-
-	/// called by MapView whenever currently visible area changes
-	/// visibleArea describes now visible map section measured in tiles
-	void onMapViewMoved(const Rect & visibleArea, int mapLevel);
-
-	/// called by MapView whenever tile is clicked
-	void onTileLeftClicked(const int3 & mapPos);
-
-	/// called by MapView whenever tile is hovered
-	void onTileHovered(const int3 & mapPos);
-
-	/// called by MapView whenever tile is clicked
-	void onTileRightClicked(const int3 & mapPos);
-
-	/// called by spell window when spell to cast has been selected
-	void enterCastingMode(const CSpell * sp);
-
-	/// returns area of screen covered by terrain (main game area)
-	Rect terrainAreaPixels() const;
-
-	/// opens world view at default scale
-	void openWorldView();
-
-	/// opens world view at specific scale
-	void openWorldView(int tileSize);
-
-	/// opens world view with specific info, e.g. after View Earth/Air is shown
-	void openWorldView(const std::vector<ObjectPosInfo>& objectPositions, bool showTerrain);
-};
-
-extern std::shared_ptr<AdventureMapInterface> adventureInt;
+/*
+ * AdventureMapInterface.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 CGObjectInstance;
+class CGHeroInstance;
+class CGTownInstance;
+class CArmedInstance;
+class IShipyard;
+struct CGPathNode;
+struct ObjectPosInfo;
+struct Component;
+class int3;
+
+VCMI_LIB_NAMESPACE_END
+
+class CButton;
+class IImage;
+class CAnimImage;
+class CGStatusBar;
+class AdventureMapWidget;
+class AdventureMapShortcuts;
+class CAnimation;
+class MapView;
+class CResDataBar;
+class CHeroList;
+class CTownList;
+class CInfoBar;
+class CMinimap;
+class MapAudioPlayer;
+class TurnTimerWidget;
+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 AdventureMapInterface : public CIntObject
+{
+private:
+	/// currently acting player
+	PlayerColor currentPlayerID;
+
+	/// 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;
+
+	/// how much should the background dimmed, when windows are on the top
+	int backgroundDimLevel;
+
+	/// 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;
+	std::shared_ptr<TurnTimerWidget> watches;
+
+private:
+	void setState(EAdventureState state);
+
+	/// updates active state of game window whenever game state changes
+	void adjustActiveness();
+
+	/// checks if obj is our ashipyard and cursor is 0,0 -> returns shipyard or nullptr else
+	const IShipyard * ourInaccessibleShipyard(const CGObjectInstance *obj) const;
+
+	/// check and if necessary reacts on scrolling by moving cursor to screen edge
+	void handleMapScrollingUpdate(uint32_t msPassed);
+
+	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 exitCastingMode();
+
+	/// casts current spell at specified location
+	void performSpellcasting(const int3 & castTarget);
+
+	/// dim interface if some windows opened
+	void dim(Canvas & to);
+
+protected:
+	/// CIntObject interface implementation
+
+	void activate() override;
+	void deactivate() override;
+
+	void tick(uint32_t msPassed) override;
+	void show(Canvas & to) override;
+	void showAll(Canvas & to) override;
+
+	void keyPressed(EShortcut key) override;
+
+	void onScreenResize() override;
+
+public:
+	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);
+
+	/// Called by PlayerInterface when AI or remote human player starts his turn
+	void onEnemyTurnStarted(PlayerColor playerID, bool isHuman);
+
+	/// Called by PlayerInterface when local human player starts his turn
+	void onPlayerTurnStarted(PlayerColor playerID);
+
+	/// Called by PlayerInterface when interface should be switched to specified player without starting turn
+	void onCurrentPlayerChanged(PlayerColor playerID);
+
+	/// Called by PlayerInterface when specific map tile changed and must be updated on minimap
+	void onMapTilesChanged(boost::optional<std::unordered_set<int3>> positions);
+
+	/// Called by PlayerInterface when hero starts movement
+	void onHeroMovementStarted(const CGHeroInstance * hero);
+
+	/// Called by PlayerInterface when hero state changed and hero list must be updated
+	void onHeroChanged(const CGHeroInstance * hero);
+
+	/// Called by PlayerInterface when town state changed and town list must be updated
+	void onTownChanged(const CGTownInstance * town);
+
+	/// Called when currently selected object changes
+	void onSelectionChanged(const CArmedInstance *sel);
+
+	/// Called when town order changes
+	void onTownOrderChanged();
+
+	/// Called when map audio should be paused, e.g. on combat or town screen access
+	void onAudioPaused();
+
+	/// Called when map audio should be resume, opposite to onPaused
+	void onAudioResumed();
+
+	/// Requests to display provided information inside infobox
+	void showInfoBoxMessage(const std::vector<Component> & components, std::string message, int timer);
+
+	/// Changes position on map to center selected location
+	void centerOnTile(int3 on);
+	void centerOnObject(const CGObjectInstance *obj);
+
+	/// called by MapView whenever currently visible area changes
+	/// visibleArea describes now visible map section measured in tiles
+	void onMapViewMoved(const Rect & visibleArea, int mapLevel);
+
+	/// called by MapView whenever tile is clicked
+	void onTileLeftClicked(const int3 & mapPos);
+
+	/// called by MapView whenever tile is hovered
+	void onTileHovered(const int3 & mapPos);
+
+	/// called by MapView whenever tile is clicked
+	void onTileRightClicked(const int3 & mapPos);
+
+	/// called by spell window when spell to cast has been selected
+	void enterCastingMode(const CSpell * sp);
+
+	/// returns area of screen covered by terrain (main game area)
+	Rect terrainAreaPixels() const;
+
+	/// opens world view at default scale
+	void openWorldView();
+
+	/// opens world view at specific scale
+	void openWorldView(int tileSize);
+
+	/// opens world view with specific info, e.g. after View Earth/Air is shown
+	void openWorldView(const std::vector<ObjectPosInfo>& objectPositions, bool showTerrain);
+};
+
+extern std::shared_ptr<AdventureMapInterface> adventureInt;

+ 455 - 455
client/adventureMap/AdventureMapWidget.cpp

@@ -1,455 +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 "../render/IRenderHandler.h"
-#include "../widgets/Buttons.h"
-#include "../widgets/Images.h"
-#include "../widgets/TextControls.h"
-
-#include "../CPlayerInterface.h"
-#include "../PlayerLocalState.h"
-
-#include "../../lib/constants/StringConstants.h"
-#include "../../lib/filesystem/ResourcePath.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(JsonPath::builtin("config/widgets/adventureMap.json"));
-
-	for(const auto & entry : config["options"]["imagesPlayerColored"].Vector())
-		playerColorerImages.push_back(ImagePath::fromJson(entry));
-
-	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 JsonNode & name)
-{
-	ImagePath resource = ImagePath::fromJson(name);
-
-	if(images.count(resource) == 0)
-		images[resource] = GH.renderHandler().loadImage(resource);
-
-	return images[resource];
-}
-
-std::shared_ptr<CAnimation> AdventureMapWidget::loadAnimation(const JsonNode & name)
-{
-	AnimationPath resource = AnimationPath::fromJson(name);
-
-	if(animations.count(resource) == 0)
-		animations[resource] = GH.renderHandler().loadAnimation(resource);
-
-	return animations[resource];
-}
-
-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"]);
-
-	return std::make_shared<CFilledTexture>(loadImage(input["image"]), targetArea, sourceArea);
-}
-
-std::shared_ptr<CIntObject> AdventureMapWidget::buildMapButton(const JsonNode & input)
-{
-	auto position = readTargetArea(input["area"]);
-	auto image = AnimationPath::fromJson(input["image"]);
-	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();
-
-	return std::make_shared<CAdventureMapIcon>(area.topLeft(), loadAnimation(input["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"]);
-	auto image = ImagePath::fromJson(input["image"]);
-
-	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"]);
-	auto image = ImagePath::fromJson(input["image"]);
-
-	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 = ImagePath::fromJson(input["image"]);
-	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 resDataBar = dynamic_cast<CResDataBar *>(entry);
-		auto texture = dynamic_cast<FilledTexturePlayerColored *>(entry);
-
-		if(button)
-			button->setPlayerColor(player);
-
-		if(resDataBar)
-			resDataBar->colorize(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(Canvas & 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);
-}
+/*
+ * 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 "../render/IRenderHandler.h"
+#include "../widgets/Buttons.h"
+#include "../widgets/Images.h"
+#include "../widgets/TextControls.h"
+
+#include "../CPlayerInterface.h"
+#include "../PlayerLocalState.h"
+
+#include "../../lib/constants/StringConstants.h"
+#include "../../lib/filesystem/ResourcePath.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(JsonPath::builtin("config/widgets/adventureMap.json"));
+
+	for(const auto & entry : config["options"]["imagesPlayerColored"].Vector())
+		playerColorerImages.push_back(ImagePath::fromJson(entry));
+
+	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 JsonNode & name)
+{
+	ImagePath resource = ImagePath::fromJson(name);
+
+	if(images.count(resource) == 0)
+		images[resource] = GH.renderHandler().loadImage(resource);
+
+	return images[resource];
+}
+
+std::shared_ptr<CAnimation> AdventureMapWidget::loadAnimation(const JsonNode & name)
+{
+	AnimationPath resource = AnimationPath::fromJson(name);
+
+	if(animations.count(resource) == 0)
+		animations[resource] = GH.renderHandler().loadAnimation(resource);
+
+	return animations[resource];
+}
+
+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"]);
+
+	return std::make_shared<CFilledTexture>(loadImage(input["image"]), targetArea, sourceArea);
+}
+
+std::shared_ptr<CIntObject> AdventureMapWidget::buildMapButton(const JsonNode & input)
+{
+	auto position = readTargetArea(input["area"]);
+	auto image = AnimationPath::fromJson(input["image"]);
+	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();
+
+	return std::make_shared<CAdventureMapIcon>(area.topLeft(), loadAnimation(input["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"]);
+	auto image = ImagePath::fromJson(input["image"]);
+
+	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"]);
+	auto image = ImagePath::fromJson(input["image"]);
+
+	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 = ImagePath::fromJson(input["image"]);
+	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 resDataBar = dynamic_cast<CResDataBar *>(entry);
+		auto texture = dynamic_cast<FilledTexturePlayerColored *>(entry);
+
+		if(button)
+			button->setPlayerColor(player);
+
+		if(resDataBar)
+			resDataBar->colorize(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(Canvas & 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);
+}

+ 110 - 110
client/adventureMap/AdventureMapWidget.h

@@ -1,110 +1,110 @@
-/*
- * 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 CAnimation;
-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<ImagePath> playerColorerImages;
-
-	/// list of named images shared between widgets
-	std::map<ImagePath, std::shared_ptr<IImage>> images;
-	std::map<AnimationPath, 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 JsonNode & name);
-	std::shared_ptr<CAnimation> loadAnimation(const JsonNode & 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(Canvas & 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);
-};
+/*
+ * 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 CAnimation;
+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<ImagePath> playerColorerImages;
+
+	/// list of named images shared between widgets
+	std::map<ImagePath, std::shared_ptr<IImage>> images;
+	std::map<AnimationPath, 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 JsonNode & name);
+	std::shared_ptr<CAnimation> loadAnimation(const JsonNode & 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(Canvas & 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);
+};

+ 61 - 61
client/adventureMap/AdventureOptions.cpp

@@ -1,61 +1,61 @@
-/*
- * CAdventureOptions.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 "AdventureOptions.h"
-
-#include "../CGameInfo.h"
-#include "../CPlayerInterface.h"
-#include "../PlayerLocalState.h"
-#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"
-
-AdventureOptions::AdventureOptions()
-	: CWindowObject(PLAYER_COLORED, ImagePath::builtin("ADVOPTS"))
-{
-	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
-
-	viewWorld = std::make_shared<CButton>(Point(24, 23), AnimationPath::builtin("ADVVIEW.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_WORLD);
-	viewWorld->addCallback( [] { LOCPLINT->viewWorldMap(); });
-
-	exit = std::make_shared<CButton>(Point(204, 313), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(), std::bind(&AdventureOptions::close, this), EShortcut::GLOBAL_RETURN);
-
-	scenInfo = std::make_shared<CButton>(Point(24, 198), AnimationPath::builtin("ADVINFO.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_SCENARIO);
-	scenInfo->addCallback(AdventureOptions::showScenarioInfo);
-
-	puzzle = std::make_shared<CButton>(Point(24, 81), AnimationPath::builtin("ADVPUZ.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_PUZZLE);
-	puzzle->addCallback(std::bind(&CPlayerInterface::showPuzzleMap, LOCPLINT));
-
-	dig = std::make_shared<CButton>(Point(24, 139), AnimationPath::builtin("ADVDIG.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_DIG_GRAIL);
-	if(const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero())
-		dig->addCallback(std::bind(&CPlayerInterface::tryDigging, LOCPLINT, h));
-	else
-		dig->block(true);
-}
-
-void AdventureOptions::showScenarioInfo()
-{
-	if(LOCPLINT->cb->getStartInfo()->campState)
-	{
-		GH.windows().createAndPushWindow<CCampaignInfoScreen>();
-	}
-	else
-	{
-		GH.windows().createAndPushWindow<CScenarioInfoScreen>();
-	}
-}
-
+/*
+ * CAdventureOptions.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 "AdventureOptions.h"
+
+#include "../CGameInfo.h"
+#include "../CPlayerInterface.h"
+#include "../PlayerLocalState.h"
+#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"
+
+AdventureOptions::AdventureOptions()
+	: CWindowObject(PLAYER_COLORED, ImagePath::builtin("ADVOPTS"))
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
+
+	viewWorld = std::make_shared<CButton>(Point(24, 23), AnimationPath::builtin("ADVVIEW.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_WORLD);
+	viewWorld->addCallback( [] { LOCPLINT->viewWorldMap(); });
+
+	exit = std::make_shared<CButton>(Point(204, 313), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(), std::bind(&AdventureOptions::close, this), EShortcut::GLOBAL_RETURN);
+
+	scenInfo = std::make_shared<CButton>(Point(24, 198), AnimationPath::builtin("ADVINFO.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_SCENARIO);
+	scenInfo->addCallback(AdventureOptions::showScenarioInfo);
+
+	puzzle = std::make_shared<CButton>(Point(24, 81), AnimationPath::builtin("ADVPUZ.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_PUZZLE);
+	puzzle->addCallback(std::bind(&CPlayerInterface::showPuzzleMap, LOCPLINT));
+
+	dig = std::make_shared<CButton>(Point(24, 139), AnimationPath::builtin("ADVDIG.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_DIG_GRAIL);
+	if(const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero())
+		dig->addCallback(std::bind(&CPlayerInterface::tryDigging, LOCPLINT, h));
+	else
+		dig->block(true);
+}
+
+void AdventureOptions::showScenarioInfo()
+{
+	if(LOCPLINT->cb->getStartInfo()->campState)
+	{
+		GH.windows().createAndPushWindow<CCampaignInfoScreen>();
+	}
+	else
+	{
+		GH.windows().createAndPushWindow<CScenarioInfoScreen>();
+	}
+}
+

+ 31 - 31
client/adventureMap/AdventureOptions.h

@@ -1,31 +1,31 @@
-/*
- * CAdventureOptions.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 "../windows/CWindowObject.h"
-
-class CButton;
-
-/// Adventure options dialog where you can view the world, dig, play the replay of the last turn,...
-class AdventureOptions : public CWindowObject
-{
-	std::shared_ptr<CButton> exit;
-	std::shared_ptr<CButton> viewWorld;
-	std::shared_ptr<CButton> puzzle;
-	std::shared_ptr<CButton> dig;
-	std::shared_ptr<CButton> scenInfo;
-	/*std::shared_ptr<CButton> replay*/
-
-public:
-	AdventureOptions();
-
-	static void showScenarioInfo();
-};
-
+/*
+ * CAdventureOptions.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 "../windows/CWindowObject.h"
+
+class CButton;
+
+/// Adventure options dialog where you can view the world, dig, play the replay of the last turn,...
+class AdventureOptions : public CWindowObject
+{
+	std::shared_ptr<CButton> exit;
+	std::shared_ptr<CButton> viewWorld;
+	std::shared_ptr<CButton> puzzle;
+	std::shared_ptr<CButton> dig;
+	std::shared_ptr<CButton> scenInfo;
+	/*std::shared_ptr<CButton> replay*/
+
+public:
+	AdventureOptions();
+
+	static void showScenarioInfo();
+};
+

+ 90 - 90
client/adventureMap/CResDataBar.cpp

@@ -1,90 +1,90 @@
-/*
- * CResDataBar.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 "CResDataBar.h"
-
-#include "../CGameInfo.h"
-#include "../CPlayerInterface.h"
-#include "../render/Canvas.h"
-#include "../render/Colors.h"
-#include "../render/EFont.h"
-#include "../gui/CGuiHandler.h"
-#include "../gui/TextAlignment.h"
-#include "../widgets/Images.h"
-
-#include "../../CCallback.h"
-#include "../../lib/CConfigHandler.h"
-#include "../../lib/CGeneralTextHandler.h"
-#include "../../lib/ResourceSet.h"
-
-CResDataBar::CResDataBar(const ImagePath & imageName, const Point & position)
-{
-	pos.x += position.x;
-	pos.y += position.y;
-
-	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
-	background = std::make_shared<CPicture>(imageName, 0, 0);
-	background->colorize(LOCPLINT->playerID);
-
-	pos.w = background->pos.w;
-	pos.h = background->pos.h;
-}
-
-CResDataBar::CResDataBar(const ImagePath & defname, int x, int y, int offx, int offy, int resdist, int datedist):
-	CResDataBar(defname, Point(x,y))
-{
-	for (int i = 0; i < 7 ; i++)
-		resourcePositions[GameResID(i)] = Point( offx + resdist*i, offy );
-
-	datePosition = resourcePositions[EGameResID::GOLD] + Point(datedist, 0);
-}
-
-void CResDataBar::setDatePosition(const Point & position)
-{
-	datePosition = position;
-}
-
-void CResDataBar::setResourcePosition(const GameResID & resource, const Point & position)
-{
-	resourcePositions[resource] = position;
-}
-
-std::string CResDataBar::buildDateString()
-{
-	std::string pattern = "%s: %d, %s: %d, %s: %d";
-
-	auto formatted = boost::format(pattern)
-		% CGI->generaltexth->translate("core.genrltxt.62") % LOCPLINT->cb->getDate(Date::MONTH)
-		% CGI->generaltexth->translate("core.genrltxt.63") % LOCPLINT->cb->getDate(Date::WEEK)
-		% CGI->generaltexth->translate("core.genrltxt.64") % LOCPLINT->cb->getDate(Date::DAY_OF_WEEK);
-
-	return boost::str(formatted);
-}
-
-void CResDataBar::showAll(Canvas & to)
-{
-	CIntObject::showAll(to);
-
-	//TODO: all this should be labels, but they require proper text update on change
-	for (auto & entry : resourcePositions)
-	{
-		std::string text = std::to_string(LOCPLINT->cb->getResourceAmount(entry.first));
-
-		to.drawText(pos.topLeft() + entry.second, FONT_SMALL, Colors::WHITE, ETextAlignment::TOPLEFT, text);
-	}
-
-	if (datePosition)
-		to.drawText(pos.topLeft() + *datePosition, FONT_SMALL, Colors::WHITE, ETextAlignment::TOPLEFT, buildDateString());
-}
-
-void CResDataBar::colorize(PlayerColor player)
-{
-	background->colorize(player);
-}
+/*
+ * CResDataBar.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 "CResDataBar.h"
+
+#include "../CGameInfo.h"
+#include "../CPlayerInterface.h"
+#include "../render/Canvas.h"
+#include "../render/Colors.h"
+#include "../render/EFont.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/TextAlignment.h"
+#include "../widgets/Images.h"
+
+#include "../../CCallback.h"
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/ResourceSet.h"
+
+CResDataBar::CResDataBar(const ImagePath & imageName, const Point & position)
+{
+	pos.x += position.x;
+	pos.y += position.y;
+
+	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
+	background = std::make_shared<CPicture>(imageName, 0, 0);
+	background->colorize(LOCPLINT->playerID);
+
+	pos.w = background->pos.w;
+	pos.h = background->pos.h;
+}
+
+CResDataBar::CResDataBar(const ImagePath & defname, int x, int y, int offx, int offy, int resdist, int datedist):
+	CResDataBar(defname, Point(x,y))
+{
+	for (int i = 0; i < 7 ; i++)
+		resourcePositions[GameResID(i)] = Point( offx + resdist*i, offy );
+
+	datePosition = resourcePositions[EGameResID::GOLD] + Point(datedist, 0);
+}
+
+void CResDataBar::setDatePosition(const Point & position)
+{
+	datePosition = position;
+}
+
+void CResDataBar::setResourcePosition(const GameResID & resource, const Point & position)
+{
+	resourcePositions[resource] = position;
+}
+
+std::string CResDataBar::buildDateString()
+{
+	std::string pattern = "%s: %d, %s: %d, %s: %d";
+
+	auto formatted = boost::format(pattern)
+		% CGI->generaltexth->translate("core.genrltxt.62") % LOCPLINT->cb->getDate(Date::MONTH)
+		% CGI->generaltexth->translate("core.genrltxt.63") % LOCPLINT->cb->getDate(Date::WEEK)
+		% CGI->generaltexth->translate("core.genrltxt.64") % LOCPLINT->cb->getDate(Date::DAY_OF_WEEK);
+
+	return boost::str(formatted);
+}
+
+void CResDataBar::showAll(Canvas & to)
+{
+	CIntObject::showAll(to);
+
+	//TODO: all this should be labels, but they require proper text update on change
+	for (auto & entry : resourcePositions)
+	{
+		std::string text = std::to_string(LOCPLINT->cb->getResourceAmount(entry.first));
+
+		to.drawText(pos.topLeft() + entry.second, FONT_SMALL, Colors::WHITE, ETextAlignment::TOPLEFT, text);
+	}
+
+	if (datePosition)
+		to.drawText(pos.topLeft() + *datePosition, FONT_SMALL, Colors::WHITE, ETextAlignment::TOPLEFT, buildDateString());
+}
+
+void CResDataBar::colorize(PlayerColor player)
+{
+	background->colorize(player);
+}

+ 40 - 40
client/adventureMap/CResDataBar.h

@@ -1,40 +1,40 @@
-/*
- * CResDataBar.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"
-#include "../../lib/filesystem/ResourcePath.h"
-
-/// Resources bar which shows information about how many gold, crystals,... you have
-/// Current date is displayed too
-class CResDataBar : public CIntObject
-{
-	std::string buildDateString();
-
-	std::shared_ptr<CPicture> background;
-
-	std::map<GameResID, Point> resourcePositions;
-	std::optional<Point> datePosition;
-
-public:
-
-	/// For dynamically-sized UI windows, e.g. adventure map interface
-	CResDataBar(const ImagePath & imageName, const Point & position);
-
-	/// For fixed-size UI windows, e.g. CastleInterface
-	CResDataBar(const ImagePath & 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(Canvas & to) override;
-};
-
+/*
+ * CResDataBar.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"
+#include "../../lib/filesystem/ResourcePath.h"
+
+/// Resources bar which shows information about how many gold, crystals,... you have
+/// Current date is displayed too
+class CResDataBar : public CIntObject
+{
+	std::string buildDateString();
+
+	std::shared_ptr<CPicture> background;
+
+	std::map<GameResID, Point> resourcePositions;
+	std::optional<Point> datePosition;
+
+public:
+
+	/// For dynamically-sized UI windows, e.g. adventure map interface
+	CResDataBar(const ImagePath & imageName, const Point & position);
+
+	/// For fixed-size UI windows, e.g. CastleInterface
+	CResDataBar(const ImagePath & 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(Canvas & to) override;
+};
+

+ 252 - 252
client/adventureMap/MapAudioPlayer.cpp

@@ -1,252 +1,252 @@
-/*
- * MapAudioPlayer.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 "MapAudioPlayer.h"
-
-#include "../CCallback.h"
-#include "../CGameInfo.h"
-#include "../CMusicHandler.h"
-#include "../CPlayerInterface.h"
-#include "../mapView/mapHandler.h"
-
-#include "../../lib/TerrainHandler.h"
-#include "../../lib/mapObjects/CArmedInstance.h"
-#include "../../lib/mapObjects/CGHeroInstance.h"
-#include "../../lib/mapping/CMap.h"
-
-bool MapAudioPlayer::hasOngoingAnimations()
-{
-	return false;
-}
-
-void MapAudioPlayer::onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest)
-{
-	if(obj == currentSelection)
-		update();
-}
-
-void MapAudioPlayer::onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest)
-{
-	if(obj == currentSelection)
-		update();
-}
-
-void MapAudioPlayer::onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest)
-{
-	if(obj == currentSelection)
-		update();
-}
-
-void MapAudioPlayer::onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest)
-{
-	if(obj == currentSelection)
-		update();
-}
-
-void MapAudioPlayer::onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator)
-{
-	addObject(obj);
-}
-
-void MapAudioPlayer::onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator)
-{
-	removeObject(obj);
-}
-
-void MapAudioPlayer::onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator)
-{
-	addObject(obj);
-}
-
-void MapAudioPlayer::onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator)
-{
-	removeObject(obj);
-}
-
-void MapAudioPlayer::addObject(const CGObjectInstance * obj)
-{
-	if(obj->isTile2Terrain())
-	{
-		// terrain overlay - all covering tiles act as sound source
-		for(int fx = 0; fx < obj->getWidth(); ++fx)
-		{
-			for(int fy = 0; fy < obj->getHeight(); ++fy)
-			{
-				int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z);
-
-				if(LOCPLINT->cb->isInTheMap(currTile) && obj->coveringAt(currTile.x, currTile.y))
-					objects[currTile.z][currTile.x][currTile.y].push_back(obj->id);
-			}
-		}
-		return;
-	}
-
-	if(obj->isVisitable())
-	{
-		// visitable object - visitable tile acts as sound source
-		int3 currTile = obj->visitablePos();
-
-		if(LOCPLINT->cb->isInTheMap(currTile))
-			objects[currTile.z][currTile.x][currTile.y].push_back(obj->id);
-
-		return;
-	}
-
-	if(!obj->isVisitable())
-	{
-		// static object - blocking tiles act as sound source
-		auto tiles = obj->getBlockedOffsets();
-
-		for(const auto & tile : tiles)
-		{
-			int3 currTile = obj->pos + tile;
-
-			if(LOCPLINT->cb->isInTheMap(currTile))
-				objects[currTile.z][currTile.x][currTile.y].push_back(obj->id);
-		}
-		return;
-	}
-}
-
-void MapAudioPlayer::removeObject(const CGObjectInstance * obj)
-{
-	for(int z = 0; z < LOCPLINT->cb->getMapSize().z; z++)
-		for(int x = 0; x < LOCPLINT->cb->getMapSize().x; x++)
-			for(int y = 0; y < LOCPLINT->cb->getMapSize().y; y++)
-				vstd::erase(objects[z][x][y], obj->id);
-}
-
-std::vector<AudioPath> MapAudioPlayer::getAmbientSounds(const int3 & tile)
-{
-	std::vector<AudioPath> result;
-
-	for(auto & objectID : objects[tile.z][tile.x][tile.y])
-	{
-		const auto & object = CGI->mh->getMap()->objects[objectID.getNum()];
-
-		assert(object);
-		if (!object)
-			logGlobal->warn("Already removed object %d found on tile! (%d %d %d)", objectID.getNum(), tile.x, tile.y, tile.z);
-
-		if(object && object->getAmbientSound())
-			result.push_back(object->getAmbientSound().value());
-	}
-
-	if(CGI->mh->getMap()->isCoastalTile(tile))
-		result.emplace_back(AudioPath::builtin("LOOPOCEA"));
-
-	return result;
-}
-
-void MapAudioPlayer::updateAmbientSounds()
-{
-	std::map<AudioPath, int> currentSounds;
-	auto updateSounds = [&](const AudioPath& soundId, int distance) -> void
-	{
-		if(vstd::contains(currentSounds, soundId))
-			currentSounds[soundId] = std::min(currentSounds[soundId], distance);
-		else
-			currentSounds.insert(std::make_pair(soundId, distance));
-	};
-
-	int3 pos = currentSelection->getSightCenter();
-	std::unordered_set<int3> tiles;
-	LOCPLINT->cb->getVisibleTilesInRange(tiles, pos, CCS->soundh->ambientGetRange(), int3::DIST_CHEBYSHEV);
-	for(int3 tile : tiles)
-	{
-		int dist = pos.dist(tile, int3::DIST_CHEBYSHEV);
-
-		for(auto & soundName : getAmbientSounds(tile))
-			updateSounds(soundName, dist);
-	}
-	CCS->soundh->ambientUpdateChannels(currentSounds);
-}
-
-void MapAudioPlayer::updateMusic()
-{
-	if(audioPlaying && playerMakingTurn && currentSelection)
-	{
-		const auto * terrain = LOCPLINT->cb->getTile(currentSelection->visitablePos())->terType;
-		CCS->musich->playMusicFromSet("terrain", terrain->getJsonKey(), true, false);
-	}
-
-	if(audioPlaying && enemyMakingTurn)
-	{
-		CCS->musich->playMusicFromSet("enemy-turn", true, false);
-	}
-}
-
-void MapAudioPlayer::update()
-{
-	updateMusic();
-
-	if(audioPlaying && playerMakingTurn && currentSelection)
-		updateAmbientSounds();
-}
-
-MapAudioPlayer::MapAudioPlayer()
-{
-	auto mapSize = LOCPLINT->cb->getMapSize();
-
-	objects.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]);
-
-	for(const auto & obj : CGI->mh->getMap()->objects)
-	{
-		if (obj)
-			addObject(obj);
-	}
-}
-
-MapAudioPlayer::~MapAudioPlayer()
-{
-	CCS->soundh->ambientStopAllChannels();
-	CCS->musich->stopMusic(1000);
-}
-
-void MapAudioPlayer::onSelectionChanged(const CArmedInstance * newSelection)
-{
-	currentSelection = newSelection;
-	update();
-}
-
-void MapAudioPlayer::onAudioPaused()
-{
-	audioPlaying = false;
-	CCS->soundh->ambientStopAllChannels();
-	CCS->musich->stopMusic(1000);
-}
-
-void MapAudioPlayer::onAudioResumed()
-{
-	audioPlaying = true;
-	update();
-}
-
-void MapAudioPlayer::onPlayerTurnStarted()
-{
-	enemyMakingTurn = false;
-	playerMakingTurn = true;
-	update();
-}
-
-void MapAudioPlayer::onEnemyTurnStarted()
-{
-	playerMakingTurn = false;
-	enemyMakingTurn = true;
-	update();
-}
-
-void MapAudioPlayer::onPlayerTurnEnded()
-{
-	playerMakingTurn = false;
-	enemyMakingTurn = false;
-	CCS->soundh->ambientStopAllChannels();
-	CCS->musich->stopMusic(1000);
-}
+/*
+ * MapAudioPlayer.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 "MapAudioPlayer.h"
+
+#include "../CCallback.h"
+#include "../CGameInfo.h"
+#include "../CMusicHandler.h"
+#include "../CPlayerInterface.h"
+#include "../mapView/mapHandler.h"
+
+#include "../../lib/TerrainHandler.h"
+#include "../../lib/mapObjects/CArmedInstance.h"
+#include "../../lib/mapObjects/CGHeroInstance.h"
+#include "../../lib/mapping/CMap.h"
+
+bool MapAudioPlayer::hasOngoingAnimations()
+{
+	return false;
+}
+
+void MapAudioPlayer::onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest)
+{
+	if(obj == currentSelection)
+		update();
+}
+
+void MapAudioPlayer::onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest)
+{
+	if(obj == currentSelection)
+		update();
+}
+
+void MapAudioPlayer::onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest)
+{
+	if(obj == currentSelection)
+		update();
+}
+
+void MapAudioPlayer::onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest)
+{
+	if(obj == currentSelection)
+		update();
+}
+
+void MapAudioPlayer::onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator)
+{
+	addObject(obj);
+}
+
+void MapAudioPlayer::onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator)
+{
+	removeObject(obj);
+}
+
+void MapAudioPlayer::onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator)
+{
+	addObject(obj);
+}
+
+void MapAudioPlayer::onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator)
+{
+	removeObject(obj);
+}
+
+void MapAudioPlayer::addObject(const CGObjectInstance * obj)
+{
+	if(obj->isTile2Terrain())
+	{
+		// terrain overlay - all covering tiles act as sound source
+		for(int fx = 0; fx < obj->getWidth(); ++fx)
+		{
+			for(int fy = 0; fy < obj->getHeight(); ++fy)
+			{
+				int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z);
+
+				if(LOCPLINT->cb->isInTheMap(currTile) && obj->coveringAt(currTile.x, currTile.y))
+					objects[currTile.z][currTile.x][currTile.y].push_back(obj->id);
+			}
+		}
+		return;
+	}
+
+	if(obj->isVisitable())
+	{
+		// visitable object - visitable tile acts as sound source
+		int3 currTile = obj->visitablePos();
+
+		if(LOCPLINT->cb->isInTheMap(currTile))
+			objects[currTile.z][currTile.x][currTile.y].push_back(obj->id);
+
+		return;
+	}
+
+	if(!obj->isVisitable())
+	{
+		// static object - blocking tiles act as sound source
+		auto tiles = obj->getBlockedOffsets();
+
+		for(const auto & tile : tiles)
+		{
+			int3 currTile = obj->pos + tile;
+
+			if(LOCPLINT->cb->isInTheMap(currTile))
+				objects[currTile.z][currTile.x][currTile.y].push_back(obj->id);
+		}
+		return;
+	}
+}
+
+void MapAudioPlayer::removeObject(const CGObjectInstance * obj)
+{
+	for(int z = 0; z < LOCPLINT->cb->getMapSize().z; z++)
+		for(int x = 0; x < LOCPLINT->cb->getMapSize().x; x++)
+			for(int y = 0; y < LOCPLINT->cb->getMapSize().y; y++)
+				vstd::erase(objects[z][x][y], obj->id);
+}
+
+std::vector<AudioPath> MapAudioPlayer::getAmbientSounds(const int3 & tile)
+{
+	std::vector<AudioPath> result;
+
+	for(auto & objectID : objects[tile.z][tile.x][tile.y])
+	{
+		const auto & object = CGI->mh->getMap()->objects[objectID.getNum()];
+
+		assert(object);
+		if (!object)
+			logGlobal->warn("Already removed object %d found on tile! (%d %d %d)", objectID.getNum(), tile.x, tile.y, tile.z);
+
+		if(object && object->getAmbientSound())
+			result.push_back(object->getAmbientSound().value());
+	}
+
+	if(CGI->mh->getMap()->isCoastalTile(tile))
+		result.emplace_back(AudioPath::builtin("LOOPOCEA"));
+
+	return result;
+}
+
+void MapAudioPlayer::updateAmbientSounds()
+{
+	std::map<AudioPath, int> currentSounds;
+	auto updateSounds = [&](const AudioPath& soundId, int distance) -> void
+	{
+		if(vstd::contains(currentSounds, soundId))
+			currentSounds[soundId] = std::min(currentSounds[soundId], distance);
+		else
+			currentSounds.insert(std::make_pair(soundId, distance));
+	};
+
+	int3 pos = currentSelection->getSightCenter();
+	std::unordered_set<int3> tiles;
+	LOCPLINT->cb->getVisibleTilesInRange(tiles, pos, CCS->soundh->ambientGetRange(), int3::DIST_CHEBYSHEV);
+	for(int3 tile : tiles)
+	{
+		int dist = pos.dist(tile, int3::DIST_CHEBYSHEV);
+
+		for(auto & soundName : getAmbientSounds(tile))
+			updateSounds(soundName, dist);
+	}
+	CCS->soundh->ambientUpdateChannels(currentSounds);
+}
+
+void MapAudioPlayer::updateMusic()
+{
+	if(audioPlaying && playerMakingTurn && currentSelection)
+	{
+		const auto * terrain = LOCPLINT->cb->getTile(currentSelection->visitablePos())->terType;
+		CCS->musich->playMusicFromSet("terrain", terrain->getJsonKey(), true, false);
+	}
+
+	if(audioPlaying && enemyMakingTurn)
+	{
+		CCS->musich->playMusicFromSet("enemy-turn", true, false);
+	}
+}
+
+void MapAudioPlayer::update()
+{
+	updateMusic();
+
+	if(audioPlaying && playerMakingTurn && currentSelection)
+		updateAmbientSounds();
+}
+
+MapAudioPlayer::MapAudioPlayer()
+{
+	auto mapSize = LOCPLINT->cb->getMapSize();
+
+	objects.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]);
+
+	for(const auto & obj : CGI->mh->getMap()->objects)
+	{
+		if (obj)
+			addObject(obj);
+	}
+}
+
+MapAudioPlayer::~MapAudioPlayer()
+{
+	CCS->soundh->ambientStopAllChannels();
+	CCS->musich->stopMusic(1000);
+}
+
+void MapAudioPlayer::onSelectionChanged(const CArmedInstance * newSelection)
+{
+	currentSelection = newSelection;
+	update();
+}
+
+void MapAudioPlayer::onAudioPaused()
+{
+	audioPlaying = false;
+	CCS->soundh->ambientStopAllChannels();
+	CCS->musich->stopMusic(1000);
+}
+
+void MapAudioPlayer::onAudioResumed()
+{
+	audioPlaying = true;
+	update();
+}
+
+void MapAudioPlayer::onPlayerTurnStarted()
+{
+	enemyMakingTurn = false;
+	playerMakingTurn = true;
+	update();
+}
+
+void MapAudioPlayer::onEnemyTurnStarted()
+{
+	playerMakingTurn = false;
+	enemyMakingTurn = true;
+	update();
+}
+
+void MapAudioPlayer::onPlayerTurnEnded()
+{
+	playerMakingTurn = false;
+	enemyMakingTurn = false;
+	CCS->soundh->ambientStopAllChannels();
+	CCS->musich->stopMusic(1000);
+}

+ 77 - 77
client/adventureMap/MapAudioPlayer.h

@@ -1,77 +1,77 @@
-/*
- * MapAudioPlayer.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 "../mapView/IMapRendererObserver.h"
-#include "../../lib/filesystem/ResourcePath.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-class ObjectInstanceID;
-class CArmedInstance;
-class PlayerColor;
-VCMI_LIB_NAMESPACE_END
-
-class MapAudioPlayer : public IMapObjectObserver
-{
-	using MapObjectsList = std::vector<ObjectInstanceID>;
-
-	boost::multi_array<MapObjectsList, 3> objects;
-	const CArmedInstance * currentSelection = nullptr;
-	bool playerMakingTurn = false;
-	bool enemyMakingTurn = false;
-	bool audioPlaying = true;
-
-	void addObject(const CGObjectInstance * obj);
-	void removeObject(const CGObjectInstance * obj);
-
-	std::vector<AudioPath> getAmbientSounds(const int3 & tile);
-	void updateAmbientSounds();
-	void updateMusic();
-	void update();
-
-protected:
-	// IMapObjectObserver impl
-	bool hasOngoingAnimations() override;
-	void onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) override;
-	void onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) override;
-	void onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) override;
-	void onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) override;
-
-	void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override;
-	void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override;
-	void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override;
-	void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override;
-
-	void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {}
-	void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {}
-	void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {}
-
-public:
-	MapAudioPlayer();
-	~MapAudioPlayer() override;
-
-	/// Called whenever current adventure map selection changes
-	void onSelectionChanged(const CArmedInstance * newSelection);
-
-	/// Called when local player starts his turn
-	void onPlayerTurnStarted();
-
-	/// Called when AI or non-local player start his turn
-	void onEnemyTurnStarted();
-
-	/// Called when local player ends his turn
-	void onPlayerTurnEnded();
-
-	/// Called when map audio should be paused, e.g. on combat or town scren access
-	void onAudioPaused();
-
-	/// Called when map audio should be resume, opposite to onPaused
-	void onAudioResumed();
-};
+/*
+ * MapAudioPlayer.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 "../mapView/IMapRendererObserver.h"
+#include "../../lib/filesystem/ResourcePath.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+class ObjectInstanceID;
+class CArmedInstance;
+class PlayerColor;
+VCMI_LIB_NAMESPACE_END
+
+class MapAudioPlayer : public IMapObjectObserver
+{
+	using MapObjectsList = std::vector<ObjectInstanceID>;
+
+	boost::multi_array<MapObjectsList, 3> objects;
+	const CArmedInstance * currentSelection = nullptr;
+	bool playerMakingTurn = false;
+	bool enemyMakingTurn = false;
+	bool audioPlaying = true;
+
+	void addObject(const CGObjectInstance * obj);
+	void removeObject(const CGObjectInstance * obj);
+
+	std::vector<AudioPath> getAmbientSounds(const int3 & tile);
+	void updateAmbientSounds();
+	void updateMusic();
+	void update();
+
+protected:
+	// IMapObjectObserver impl
+	bool hasOngoingAnimations() override;
+	void onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) override;
+	void onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) override;
+	void onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) override;
+	void onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) override;
+
+	void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override;
+	void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override;
+	void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override;
+	void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override;
+
+	void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {}
+	void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {}
+	void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {}
+
+public:
+	MapAudioPlayer();
+	~MapAudioPlayer() override;
+
+	/// Called whenever current adventure map selection changes
+	void onSelectionChanged(const CArmedInstance * newSelection);
+
+	/// Called when local player starts his turn
+	void onPlayerTurnStarted();
+
+	/// Called when AI or non-local player start his turn
+	void onEnemyTurnStarted();
+
+	/// Called when local player ends his turn
+	void onPlayerTurnEnded();
+
+	/// Called when map audio should be paused, e.g. on combat or town scren access
+	void onAudioPaused();
+
+	/// Called when map audio should be resume, opposite to onPaused
+	void onAudioResumed();
+};

+ 1054 - 1054
client/battle/BattleActionsController.cpp

@@ -1,1054 +1,1054 @@
-/*
- * BattleActionsController.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 "BattleActionsController.h"
-
-#include "BattleWindow.h"
-#include "BattleStacksController.h"
-#include "BattleInterface.h"
-#include "BattleFieldController.h"
-#include "BattleSiegeController.h"
-#include "BattleInterfaceClasses.h"
-
-#include "../CGameInfo.h"
-#include "../CPlayerInterface.h"
-#include "../gui/CursorHandler.h"
-#include "../gui/CGuiHandler.h"
-#include "../gui/CIntObject.h"
-#include "../gui/WindowHandler.h"
-#include "../windows/CCreatureWindow.h"
-#include "../windows/InfoWindows.h"
-
-#include "../../CCallback.h"
-#include "../../lib/CConfigHandler.h"
-#include "../../lib/CGeneralTextHandler.h"
-#include "../../lib/CStack.h"
-#include "../../lib/battle/BattleAction.h"
-#include "../../lib/spells/CSpellHandler.h"
-#include "../../lib/spells/ISpellMechanics.h"
-#include "../../lib/spells/Problem.h"
-
-struct TextReplacement
-{
-	std::string placeholder;
-	std::string replacement;
-};
-
-using TextReplacementList = std::vector<TextReplacement>;
-
-static std::string replacePlaceholders(std::string input, const TextReplacementList & format )
-{
-	for(const auto & entry : format)
-		boost::replace_all(input, entry.placeholder, entry.replacement);
-
-	return input;
-}
-
-static std::string translatePlural(int amount, const std::string& baseTextID)
-{
-	if(amount == 1)
-		return CGI->generaltexth->translate(baseTextID + ".1");
-	return CGI->generaltexth->translate(baseTextID);
-}
-
-static std::string formatPluralImpl(int amount, const std::string & amountString, const std::string & baseTextID)
-{
-	std::string baseString = translatePlural(amount, baseTextID);
-	TextReplacementList replacements {
-		{ "%d", amountString }
-	};
-
-	return replacePlaceholders(baseString, replacements);
-}
-
-static std::string formatPlural(int amount, const std::string & baseTextID)
-{
-	return formatPluralImpl(amount, std::to_string(amount), baseTextID);
-}
-
-static std::string formatPlural(DamageRange range, const std::string & baseTextID)
-{
-	if (range.min == range.max)
-		return formatPlural(range.min, baseTextID);
-
-	std::string rangeString = std::to_string(range.min) + " - " + std::to_string(range.max);
-
-	return formatPluralImpl(range.max, rangeString, baseTextID);
-}
-
-static std::string formatAttack(const DamageEstimation & estimation, const std::string & creatureName, const std::string & baseTextID, int shotsLeft)
-{
-	TextReplacementList replacements = {
-		{ "%CREATURE", creatureName },
-		{ "%DAMAGE", formatPlural(estimation.damage, "vcmi.battleWindow.damageEstimation.damage") },
-		{ "%SHOTS", formatPlural(shotsLeft, "vcmi.battleWindow.damageEstimation.shots") },
-		{ "%KILLS", formatPlural(estimation.kills, "vcmi.battleWindow.damageEstimation.kills") },
-	};
-
-	return replacePlaceholders(CGI->generaltexth->translate(baseTextID), replacements);
-}
-
-static std::string formatMeleeAttack(const DamageEstimation & estimation, const std::string & creatureName)
-{
-	std::string baseTextID = estimation.kills.max == 0 ?
-		"vcmi.battleWindow.damageEstimation.melee" :
-		"vcmi.battleWindow.damageEstimation.meleeKills";
-
-	return formatAttack(estimation, creatureName, baseTextID, 0);
-}
-
-static std::string formatRangedAttack(const DamageEstimation & estimation, const std::string & creatureName, int shotsLeft)
-{
-	std::string baseTextID = estimation.kills.max == 0 ?
-		"vcmi.battleWindow.damageEstimation.ranged" :
-		"vcmi.battleWindow.damageEstimation.rangedKills";
-
-	return formatAttack(estimation, creatureName, baseTextID, shotsLeft);
-}
-
-BattleActionsController::BattleActionsController(BattleInterface & owner):
-	owner(owner),
-	selectedStack(nullptr),
-	heroSpellToCast(nullptr)
-{
-}
-
-void BattleActionsController::endCastingSpell()
-{
-	if(heroSpellToCast)
-	{
-		heroSpellToCast.reset();
-		owner.windowObject->blockUI(false);
-	}
-
-	if(owner.stacksController->getActiveStack())
-		possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack()); //restore actions after they were cleared
-
-	selectedStack = nullptr;
-	GH.fakeMouseMove();
-}
-
-bool BattleActionsController::isActiveStackSpellcaster() const
-{
-	const CStack * casterStack = owner.stacksController->getActiveStack();
-	if (!casterStack)
-		return false;
-
-	bool spellcaster = casterStack->hasBonusOfType(BonusType::SPELLCASTER);
-	return (spellcaster && casterStack->canCast());
-}
-
-void BattleActionsController::enterCreatureCastingMode()
-{
-	//silently check for possible errors
-	if (owner.tacticsMode)
-		return;
-
-	//hero is casting a spell
-	if (heroSpellToCast)
-		return;
-
-	if (!owner.stacksController->getActiveStack())
-		return;
-
-	if (!isActiveStackSpellcaster())
-		return;
-
-	for(const auto & action : possibleActions)
-	{
-		if (action.get() != PossiblePlayerBattleAction::NO_LOCATION)
-			continue;
-
-		const spells::Caster * caster = owner.stacksController->getActiveStack();
-		const CSpell * spell = action.spell().toSpell();
-
-		spells::Target target;
-		target.emplace_back();
-
-		spells::BattleCast cast(owner.getBattle().get(), caster, spells::Mode::CREATURE_ACTIVE, spell);
-
-		auto m = spell->battleMechanics(&cast);
-		spells::detail::ProblemImpl ignored;
-
-		const bool isCastingPossible = m->canBeCastAt(target, ignored);
-
-		if (isCastingPossible)
-		{
-			owner.giveCommand(EActionType::MONSTER_SPELL, BattleHex::INVALID, spell->getId());
-			selectedStack = nullptr;
-
-			CCS->curh->set(Cursor::Combat::POINTER);
-		}
-		return;
-	}
-
-	possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack());
-
-	auto actionFilterPredicate = [](const PossiblePlayerBattleAction x)
-	{
-		return !x.spellcast();
-	};
-
-	vstd::erase_if(possibleActions, actionFilterPredicate);
-	GH.fakeMouseMove();
-}
-
-std::vector<PossiblePlayerBattleAction> BattleActionsController::getPossibleActionsForStack(const CStack *stack) const
-{
-	BattleClientInterfaceData data; //hard to get rid of these things so for now they're required data to pass
-
-	for(const auto & spell : creatureSpells)
-		data.creatureSpellsToCast.push_back(spell->id);
-
-	data.tacticsMode = owner.tacticsMode;
-	auto allActions = owner.getBattle()->getClientActionsForStack(stack, data);
-
-	allActions.push_back(PossiblePlayerBattleAction::HERO_INFO);
-	allActions.push_back(PossiblePlayerBattleAction::CREATURE_INFO);
-
-	return std::vector<PossiblePlayerBattleAction>(allActions);
-}
-
-void BattleActionsController::reorderPossibleActionsPriority(const CStack * stack, const CStack * targetStack)
-{
-	if(owner.tacticsMode || possibleActions.empty()) return; //this function is not supposed to be called in tactics mode or before getPossibleActionsForStack
-
-	auto assignPriority = [&](const PossiblePlayerBattleAction & item
-						  ) -> uint8_t //large lambda assigning priority which would have to be part of possibleActions without it
-	{
-		switch(item.get())
-		{
-			case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
-			case PossiblePlayerBattleAction::ANY_LOCATION:
-			case PossiblePlayerBattleAction::NO_LOCATION:
-			case PossiblePlayerBattleAction::FREE_LOCATION:
-			case PossiblePlayerBattleAction::OBSTACLE:
-				if(!stack->hasBonusOfType(BonusType::NO_SPELLCAST_BY_DEFAULT) && targetStack != nullptr)
-				{
-					PlayerColor stackOwner = owner.getBattle()->battleGetOwner(targetStack);
-					bool enemyTargetingPositiveSpellcast = item.spell().toSpell()->isPositive() && stackOwner != LOCPLINT->playerID;
-					bool friendTargetingNegativeSpellcast = item.spell().toSpell()->isNegative() && stackOwner == LOCPLINT->playerID;
-
-					if(!enemyTargetingPositiveSpellcast && !friendTargetingNegativeSpellcast)
-						return 1;
-				}
-				return 100; //bottom priority
-
-				break;
-			case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
-				return 2;
-				break;
-			case PossiblePlayerBattleAction::SHOOT:
-				return 4;
-				break;
-			case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
-				return 5;
-				break;
-			case PossiblePlayerBattleAction::ATTACK:
-				return 6;
-				break;
-			case PossiblePlayerBattleAction::WALK_AND_ATTACK:
-				return 7;
-				break;
-			case PossiblePlayerBattleAction::MOVE_STACK:
-				return 8;
-				break;
-			case PossiblePlayerBattleAction::CATAPULT:
-				return 9;
-				break;
-			case PossiblePlayerBattleAction::HEAL:
-				return 10;
-				break;
-			case PossiblePlayerBattleAction::CREATURE_INFO:
-				return 11;
-				break;
-			case PossiblePlayerBattleAction::HERO_INFO:
-				return 12;
-				break;
-			case PossiblePlayerBattleAction::TELEPORT:
-				return 13;
-				break;
-			default:
-				assert(0);
-				return 200;
-				break;
-		}
-	};
-
-	auto comparer = [&](const PossiblePlayerBattleAction & lhs, const PossiblePlayerBattleAction & rhs)
-	{
-		return assignPriority(lhs) < assignPriority(rhs);
-	};
-
-	std::sort(possibleActions.begin(), possibleActions.end(), comparer);
-}
-
-void BattleActionsController::castThisSpell(SpellID spellID)
-{
-	heroSpellToCast = std::make_shared<BattleAction>();
-	heroSpellToCast->actionType = EActionType::HERO_SPELL;
-	heroSpellToCast->spell = spellID;
-	heroSpellToCast->stackNumber = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? -1 : -2;
-	heroSpellToCast->side = owner.defendingHeroInstance ? (owner.curInt->playerID == owner.defendingHeroInstance->tempOwner) : false;
-
-	//choosing possible targets
-	const CGHeroInstance *castingHero = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? owner.attackingHeroInstance : owner.defendingHeroInstance;
-	assert(castingHero); // code below assumes non-null hero
-	PossiblePlayerBattleAction spellSelMode = owner.getBattle()->getCasterAction(spellID.toSpell(), castingHero, spells::Mode::HERO);
-
-	if (spellSelMode.get() == PossiblePlayerBattleAction::NO_LOCATION) //user does not have to select location
-	{
-		heroSpellToCast->aimToHex(BattleHex::INVALID);
-		owner.curInt->cb->battleMakeSpellAction(owner.getBattleID(), *heroSpellToCast);
-		endCastingSpell();
-	}
-	else
-	{
-		possibleActions.clear();
-		possibleActions.push_back (spellSelMode); //only this one action can be performed at the moment
-		GH.fakeMouseMove();//update cursor
-	}
-
-	owner.windowObject->blockUI(true);
-}
-
-const CSpell * BattleActionsController::getHeroSpellToCast( ) const
-{
-	if (heroSpellToCast)
-		return heroSpellToCast->spell.toSpell();
-	return nullptr;
-}
-
-const CSpell * BattleActionsController::getStackSpellToCast(BattleHex hoveredHex)
-{
-	if (heroSpellToCast)
-		return nullptr;
-
-	if (!owner.stacksController->getActiveStack())
-		return nullptr;
-
-	if (!hoveredHex.isValid())
-		return nullptr;
-
-	auto action = selectAction(hoveredHex);
-
-	if (action.spell() == SpellID::NONE)
-		return nullptr;
-
-	return action.spell().toSpell();
-}
-
-const CSpell * BattleActionsController::getCurrentSpell(BattleHex hoveredHex)
-{
-	if (getHeroSpellToCast())
-		return getHeroSpellToCast();
-	return getStackSpellToCast(hoveredHex);
-}
-
-const CStack * BattleActionsController::getStackForHex(BattleHex hoveredHex)
-{
-	const CStack * shere = owner.getBattle()->battleGetStackByPos(hoveredHex, true);
-	if(shere)
-		return shere;
-	return owner.getBattle()->battleGetStackByPos(hoveredHex, false);
-}
-
-void BattleActionsController::actionSetCursor(PossiblePlayerBattleAction action, BattleHex targetHex)
-{
-	switch (action.get())
-	{
-		case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
-			CCS->curh->set(Cursor::Combat::POINTER);
-			return;
-
-		case PossiblePlayerBattleAction::MOVE_TACTICS:
-		case PossiblePlayerBattleAction::MOVE_STACK:
-			if (owner.stacksController->getActiveStack()->hasBonusOfType(BonusType::FLYING))
-				CCS->curh->set(Cursor::Combat::FLY);
-			else
-				CCS->curh->set(Cursor::Combat::MOVE);
-			return;
-
-		case PossiblePlayerBattleAction::ATTACK:
-		case PossiblePlayerBattleAction::WALK_AND_ATTACK:
-		case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
-		{
-			static const std::map<BattleHex::EDir, Cursor::Combat> sectorCursor = {
-				{BattleHex::TOP_LEFT,     Cursor::Combat::HIT_SOUTHEAST},
-				{BattleHex::TOP_RIGHT,    Cursor::Combat::HIT_SOUTHWEST},
-				{BattleHex::RIGHT,        Cursor::Combat::HIT_WEST     },
-				{BattleHex::BOTTOM_RIGHT, Cursor::Combat::HIT_NORTHWEST},
-				{BattleHex::BOTTOM_LEFT,  Cursor::Combat::HIT_NORTHEAST},
-				{BattleHex::LEFT,         Cursor::Combat::HIT_EAST     },
-				{BattleHex::TOP,          Cursor::Combat::HIT_SOUTH    },
-				{BattleHex::BOTTOM,       Cursor::Combat::HIT_NORTH    }
-			};
-
-			auto direction = owner.fieldController->selectAttackDirection(targetHex);
-
-			assert(sectorCursor.count(direction) > 0);
-			if (sectorCursor.count(direction))
-				CCS->curh->set(sectorCursor.at(direction));
-
-			return;
-		}
-
-		case PossiblePlayerBattleAction::SHOOT:
-			if (owner.getBattle()->battleHasShootingPenalty(owner.stacksController->getActiveStack(), targetHex))
-				CCS->curh->set(Cursor::Combat::SHOOT_PENALTY);
-			else
-				CCS->curh->set(Cursor::Combat::SHOOT);
-			return;
-
-		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
-		case PossiblePlayerBattleAction::ANY_LOCATION:
-		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
-		case PossiblePlayerBattleAction::FREE_LOCATION:
-		case PossiblePlayerBattleAction::OBSTACLE:
-			CCS->curh->set(Cursor::Spellcast::SPELL);
-			return;
-
-		case PossiblePlayerBattleAction::TELEPORT:
-			CCS->curh->set(Cursor::Combat::TELEPORT);
-			return;
-
-		case PossiblePlayerBattleAction::SACRIFICE:
-			CCS->curh->set(Cursor::Combat::SACRIFICE);
-			return;
-
-		case PossiblePlayerBattleAction::HEAL:
-			CCS->curh->set(Cursor::Combat::HEAL);
-			return;
-
-		case PossiblePlayerBattleAction::CATAPULT:
-			CCS->curh->set(Cursor::Combat::SHOOT_CATAPULT);
-			return;
-
-		case PossiblePlayerBattleAction::CREATURE_INFO:
-			CCS->curh->set(Cursor::Combat::QUERY);
-			return;
-		case PossiblePlayerBattleAction::HERO_INFO:
-			CCS->curh->set(Cursor::Combat::HERO);
-			return;
-	}
-	assert(0);
-}
-
-void BattleActionsController::actionSetCursorBlocked(PossiblePlayerBattleAction action, BattleHex targetHex)
-{
-	switch (action.get())
-	{
-		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
-		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
-		case PossiblePlayerBattleAction::TELEPORT:
-		case PossiblePlayerBattleAction::SACRIFICE:
-		case PossiblePlayerBattleAction::FREE_LOCATION:
-			CCS->curh->set(Cursor::Combat::BLOCKED);
-			return;
-		default:
-			if (targetHex == -1)
-				CCS->curh->set(Cursor::Combat::POINTER);
-			else
-				CCS->curh->set(Cursor::Combat::BLOCKED);
-			return;
-	}
-	assert(0);
-}
-
-std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattleAction action, BattleHex targetHex)
-{
-	const CStack * targetStack = getStackForHex(targetHex);
-
-	switch (action.get()) //display console message, realize selected action
-	{
-		case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
-			return (boost::format(CGI->generaltexth->allTexts[481]) % targetStack->getName()).str(); //Select %s
-
-		case PossiblePlayerBattleAction::MOVE_TACTICS:
-		case PossiblePlayerBattleAction::MOVE_STACK:
-			if (owner.stacksController->getActiveStack()->hasBonusOfType(BonusType::FLYING))
-				return (boost::format(CGI->generaltexth->allTexts[295]) % owner.stacksController->getActiveStack()->getName()).str(); //Fly %s here
-			else
-				return (boost::format(CGI->generaltexth->allTexts[294]) % owner.stacksController->getActiveStack()->getName()).str(); //Move %s here
-
-		case PossiblePlayerBattleAction::ATTACK:
-		case PossiblePlayerBattleAction::WALK_AND_ATTACK:
-		case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return
-			{
-				BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex);
-				DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex);
-				estimation.kills.max = std::min<int64_t>(estimation.kills.max, targetStack->getCount());
-				estimation.kills.min = std::min<int64_t>(estimation.kills.min, targetStack->getCount());
-
-				return formatMeleeAttack(estimation, targetStack->getName());
-			}
-
-		case PossiblePlayerBattleAction::SHOOT:
-		{
-			const auto * shooter = owner.stacksController->getActiveStack();
-
-			DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(shooter, targetStack, shooter->getPosition());
-			estimation.kills.max = std::min<int64_t>(estimation.kills.max, targetStack->getCount());
-			estimation.kills.min = std::min<int64_t>(estimation.kills.min, targetStack->getCount());
-
-			return formatRangedAttack(estimation, targetStack->getName(), shooter->shots.available());
-		}
-
-		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
-			return boost::str(boost::format(CGI->generaltexth->allTexts[27]) % action.spell().toSpell()->getNameTranslated() % targetStack->getName()); //Cast %s on %s
-
-		case PossiblePlayerBattleAction::ANY_LOCATION:
-			return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % action.spell().toSpell()->getNameTranslated()); //Cast %s
-
-		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell
-			return boost::str(boost::format(CGI->generaltexth->allTexts[301]) % targetStack->getName()); //Cast a spell on %
-
-		case PossiblePlayerBattleAction::TELEPORT:
-			return CGI->generaltexth->allTexts[25]; //Teleport Here
-
-		case PossiblePlayerBattleAction::OBSTACLE:
-			return CGI->generaltexth->allTexts[550];
-
-		case PossiblePlayerBattleAction::SACRIFICE:
-			return (boost::format(CGI->generaltexth->allTexts[549]) % targetStack->getName()).str(); //sacrifice the %s
-
-		case PossiblePlayerBattleAction::FREE_LOCATION:
-			return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % action.spell().toSpell()->getNameTranslated()); //Cast %s
-
-		case PossiblePlayerBattleAction::HEAL:
-			return (boost::format(CGI->generaltexth->allTexts[419]) % targetStack->getName()).str(); //Apply first aid to the %s
-
-		case PossiblePlayerBattleAction::CATAPULT:
-			return ""; // TODO
-
-		case PossiblePlayerBattleAction::CREATURE_INFO:
-			return (boost::format(CGI->generaltexth->allTexts[297]) % targetStack->getName()).str();
-
-		case PossiblePlayerBattleAction::HERO_INFO:
-			return  CGI->generaltexth->translate("core.genrltxt.417"); // "View Hero Stats"
-	}
-	assert(0);
-	return "";
-}
-
-std::string BattleActionsController::actionGetStatusMessageBlocked(PossiblePlayerBattleAction action, BattleHex targetHex)
-{
-	switch (action.get())
-	{
-		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
-		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
-			return CGI->generaltexth->allTexts[23];
-			break;
-		case PossiblePlayerBattleAction::TELEPORT:
-			return CGI->generaltexth->allTexts[24]; //Invalid Teleport Destination
-			break;
-		case PossiblePlayerBattleAction::SACRIFICE:
-			return CGI->generaltexth->allTexts[543]; //choose army to sacrifice
-			break;
-		case PossiblePlayerBattleAction::FREE_LOCATION:
-			return boost::str(boost::format(CGI->generaltexth->allTexts[181]) % action.spell().toSpell()->getNameTranslated()); //No room to place %s here
-			break;
-		default:
-			return "";
-	}
-}
-
-bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, BattleHex targetHex)
-{
-	const CStack * targetStack = getStackForHex(targetHex);
-	bool targetStackOwned = targetStack && targetStack->unitOwner() == owner.curInt->playerID;
-
-	switch (action.get())
-	{
-		case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
-			return (targetStack && targetStackOwned && targetStack->speed() > 0);
-
-		case PossiblePlayerBattleAction::CREATURE_INFO:
-			return (targetStack && targetStackOwned && targetStack->alive());
-
-		case PossiblePlayerBattleAction::HERO_INFO:
-			if (targetHex == BattleHex::HERO_ATTACKER)
-				return owner.attackingHero != nullptr;
-
-			if (targetHex == BattleHex::HERO_DEFENDER)
-				return owner.defendingHero != nullptr;
-
-			return false;
-
-		case PossiblePlayerBattleAction::MOVE_TACTICS:
-		case PossiblePlayerBattleAction::MOVE_STACK:
-			if (!(targetStack && targetStack->alive())) //we can walk on dead stacks
-			{
-				if(canStackMoveHere(owner.stacksController->getActiveStack(), targetHex))
-					return true;
-			}
-			return false;
-
-		case PossiblePlayerBattleAction::ATTACK:
-		case PossiblePlayerBattleAction::WALK_AND_ATTACK:
-		case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
-			if(owner.getBattle()->battleCanAttack(owner.stacksController->getActiveStack(), targetStack, targetHex))
-			{
-				if (owner.fieldController->isTileAttackable(targetHex)) // move isTileAttackable to be part of battleCanAttack?
-					return true;
-			}
-			return false;
-
-		case PossiblePlayerBattleAction::SHOOT:
-			return owner.getBattle()->battleCanShoot(owner.stacksController->getActiveStack(), targetHex);
-
-		case PossiblePlayerBattleAction::NO_LOCATION:
-			return false;
-
-		case PossiblePlayerBattleAction::ANY_LOCATION:
-			return isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex);
-
-		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
-			return !selectedStack && targetStack && isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex);
-
-		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
-			if(targetStack && targetStackOwned && targetStack != owner.stacksController->getActiveStack() && targetStack->alive()) //only positive spells for other allied creatures
-			{
-				int spellID = owner.getBattle()->battleGetRandomStackSpell(CRandomGenerator::getDefault(), targetStack, CBattleInfoCallback::RANDOM_GENIE);
-				return spellID > -1;
-			}
-			return false;
-
-		case PossiblePlayerBattleAction::TELEPORT:
-			return selectedStack && isCastingPossibleHere(action.spell().toSpell(), selectedStack, targetHex);
-
-		case PossiblePlayerBattleAction::SACRIFICE: //choose our living stack to sacrifice
-			return targetStack && targetStack != selectedStack && targetStackOwned && targetStack->alive();
-
-		case PossiblePlayerBattleAction::OBSTACLE:
-		case PossiblePlayerBattleAction::FREE_LOCATION:
-			return isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex);
-
-		case PossiblePlayerBattleAction::CATAPULT:
-			return owner.siegeController && owner.siegeController->isAttackableByCatapult(targetHex);
-
-		case PossiblePlayerBattleAction::HEAL:
-			return targetStack && targetStackOwned && targetStack->canBeHealed();
-	}
-
-	assert(0);
-	return false;
-}
-
-void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, BattleHex targetHex)
-{
-	const CStack * targetStack = getStackForHex(targetHex);
-
-	switch (action.get()) //display console message, realize selected action
-	{
-		case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
-		{
-			owner.stackActivated(targetStack);
-			return;
-		}
-
-		case PossiblePlayerBattleAction::MOVE_TACTICS:
-		case PossiblePlayerBattleAction::MOVE_STACK:
-		{
-			if(owner.stacksController->getActiveStack()->doubleWide())
-			{
-				std::vector<BattleHex> acc = owner.getBattle()->battleGetAvailableHexes(owner.stacksController->getActiveStack(), false);
-				BattleHex shiftedDest = targetHex.cloneInDirection(owner.stacksController->getActiveStack()->destShiftDir(), false);
-				if(vstd::contains(acc, targetHex))
-					owner.giveCommand(EActionType::WALK, targetHex);
-				else if(vstd::contains(acc, shiftedDest))
-					owner.giveCommand(EActionType::WALK, shiftedDest);
-			}
-			else
-			{
-				owner.giveCommand(EActionType::WALK, targetHex);
-			}
-			return;
-		}
-
-		case PossiblePlayerBattleAction::ATTACK:
-		case PossiblePlayerBattleAction::WALK_AND_ATTACK:
-		case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return
-		{
-			bool returnAfterAttack = action.get() == PossiblePlayerBattleAction::ATTACK_AND_RETURN;
-			BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex);
-			if(attackFromHex.isValid()) //we can be in this line when unreachable creature is L - clicked (as of revision 1308)
-			{
-				BattleAction command = BattleAction::makeMeleeAttack(owner.stacksController->getActiveStack(), targetHex, attackFromHex, returnAfterAttack);
-				owner.sendCommand(command, owner.stacksController->getActiveStack());
-			}
-			return;
-		}
-
-		case PossiblePlayerBattleAction::SHOOT:
-		{
-			owner.giveCommand(EActionType::SHOOT, targetHex);
-			return;
-		}
-
-		case PossiblePlayerBattleAction::HEAL:
-		{
-			owner.giveCommand(EActionType::STACK_HEAL, targetHex);
-			return;
-		};
-
-		case PossiblePlayerBattleAction::CATAPULT:
-		{
-			owner.giveCommand(EActionType::CATAPULT, targetHex);
-			return;
-		}
-
-		case PossiblePlayerBattleAction::CREATURE_INFO:
-		{
-			GH.windows().createAndPushWindow<CStackWindow>(targetStack, false);
-			return;
-		}
-
-		case PossiblePlayerBattleAction::HERO_INFO:
-		{
-			if (targetHex == BattleHex::HERO_ATTACKER)
-				owner.attackingHero->heroLeftClicked();
-
-			if (targetHex == BattleHex::HERO_DEFENDER)
-				owner.defendingHero->heroLeftClicked();
-
-			return;
-		}
-
-		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
-		case PossiblePlayerBattleAction::ANY_LOCATION:
-		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell
-		case PossiblePlayerBattleAction::TELEPORT:
-		case PossiblePlayerBattleAction::OBSTACLE:
-		case PossiblePlayerBattleAction::SACRIFICE:
-		case PossiblePlayerBattleAction::FREE_LOCATION:
-		{
-			if (action.get() == PossiblePlayerBattleAction::AIMED_SPELL_CREATURE )
-			{
-				if (action.spell() == SpellID::SACRIFICE)
-				{
-					heroSpellToCast->aimToHex(targetHex);
-					possibleActions.push_back({PossiblePlayerBattleAction::SACRIFICE, action.spell()});
-					selectedStack = targetStack;
-					return;
-				}
-				if (action.spell() == SpellID::TELEPORT)
-				{
-					heroSpellToCast->aimToUnit(targetStack);
-					possibleActions.push_back({PossiblePlayerBattleAction::TELEPORT, action.spell()});
-					selectedStack = targetStack;
-					return;
-				}
-			}
-
-			if (!spellcastingModeActive())
-			{
-				if (action.spell().toSpell())
-				{
-					owner.giveCommand(EActionType::MONSTER_SPELL, targetHex, action.spell());
-				}
-				else //unknown random spell
-				{
-					owner.giveCommand(EActionType::MONSTER_SPELL, targetHex);
-				}
-			}
-			else
-			{
-				assert(getHeroSpellToCast());
-				switch (getHeroSpellToCast()->id.toEnum())
-				{
-					case SpellID::SACRIFICE:
-						heroSpellToCast->aimToUnit(targetStack);//victim
-						break;
-					default:
-						heroSpellToCast->aimToHex(targetHex);
-						break;
-				}
-				owner.curInt->cb->battleMakeSpellAction(owner.getBattleID(), *heroSpellToCast);
-				endCastingSpell();
-			}
-			selectedStack = nullptr;
-			return;
-		}
-	}
-	assert(0);
-	return;
-}
-
-PossiblePlayerBattleAction BattleActionsController::selectAction(BattleHex targetHex)
-{
-	assert(owner.stacksController->getActiveStack() != nullptr);
-	assert(!possibleActions.empty());
-	assert(targetHex.isValid());
-
-	if (owner.stacksController->getActiveStack() == nullptr)
-		return PossiblePlayerBattleAction::INVALID;
-
-	if (possibleActions.empty())
-		return PossiblePlayerBattleAction::INVALID;
-
-	const CStack * targetStack = getStackForHex(targetHex);
-
-	reorderPossibleActionsPriority(owner.stacksController->getActiveStack(), targetStack);
-
-	for (PossiblePlayerBattleAction action : possibleActions)
-	{
-		if (actionIsLegal(action, targetHex))
-			return action;
-	}
-	return possibleActions.front();
-}
-
-void BattleActionsController::onHexHovered(BattleHex hoveredHex)
-{
-	if (owner.openingPlaying())
-	{
-		currentConsoleMsg = VLC->generaltexth->translate("vcmi.battleWindow.pressKeyToSkipIntro");
-		GH.statusbar()->write(currentConsoleMsg);
-		return;
-	}
-
-	if (owner.stacksController->getActiveStack() == nullptr)
-		return;
-
-	if (hoveredHex == BattleHex::INVALID)
-	{
-		if (!currentConsoleMsg.empty())
-			GH.statusbar()->clearIfMatching(currentConsoleMsg);
-
-		currentConsoleMsg.clear();
-		CCS->curh->set(Cursor::Combat::BLOCKED);
-		return;
-	}
-
-	auto action = selectAction(hoveredHex);
-
-	std::string newConsoleMsg;
-
-	if (actionIsLegal(action, hoveredHex))
-	{
-		actionSetCursor(action, hoveredHex);
-		newConsoleMsg = actionGetStatusMessage(action, hoveredHex);
-	}
-	else
-	{
-		actionSetCursorBlocked(action, hoveredHex);
-		newConsoleMsg = actionGetStatusMessageBlocked(action, hoveredHex);
-	}
-
-	if (!currentConsoleMsg.empty())
-		GH.statusbar()->clearIfMatching(currentConsoleMsg);
-
-	if (!newConsoleMsg.empty())
-		GH.statusbar()->write(newConsoleMsg);
-
-	currentConsoleMsg = newConsoleMsg;
-}
-
-void BattleActionsController::onHoverEnded()
-{
-	CCS->curh->set(Cursor::Combat::POINTER);
-
-	if (!currentConsoleMsg.empty())
-		GH.statusbar()->clearIfMatching(currentConsoleMsg);
-
-	currentConsoleMsg.clear();
-}
-
-void BattleActionsController::onHexLeftClicked(BattleHex clickedHex)
-{
-	if (owner.stacksController->getActiveStack() == nullptr)
-		return;
-
-	auto action = selectAction(clickedHex);
-
-	std::string newConsoleMsg;
-
-	if (!actionIsLegal(action, clickedHex))
-		return;
-	
-	actionRealize(action, clickedHex);
-	GH.statusbar()->clear();
-}
-
-void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterStack)
-{
-	creatureSpells.clear();
-
-	bool spellcaster = casterStack->hasBonusOfType(BonusType::SPELLCASTER);
-	if(casterStack->canCast() && spellcaster)
-	{
-		// faerie dragon can cast only one, randomly selected spell until their next move
-		//TODO: faerie dragon type spell should be selected by server
-		const auto * spellToCast = owner.getBattle()->battleGetRandomStackSpell(CRandomGenerator::getDefault(), casterStack, CBattleInfoCallback::RANDOM_AIMED).toSpell();
-
-		if (spellToCast)
-			creatureSpells.push_back(spellToCast);
-	}
-
-	TConstBonusListPtr bl = casterStack->getBonuses(Selector::type()(BonusType::SPELLCASTER));
-
-	for(const auto & bonus : *bl)
-	{
-		if (bonus->additionalInfo[0] <= 0)
-			creatureSpells.push_back(bonus->subtype.as<SpellID>().toSpell());
-	}
-}
-
-const spells::Caster * BattleActionsController::getCurrentSpellcaster() const
-{
-	if (heroSpellToCast)
-		return owner.getActiveHero();
-	else
-		return owner.stacksController->getActiveStack();
-}
-
-spells::Mode BattleActionsController::getCurrentCastMode() const
-{
-	if (heroSpellToCast)
-		return spells::Mode::HERO;
-	else
-		return spells::Mode::CREATURE_ACTIVE;
-
-}
-
-bool BattleActionsController::isCastingPossibleHere(const CSpell * currentSpell, const CStack *targetStack, BattleHex targetHex)
-{
-	assert(currentSpell);
-	if (!currentSpell)
-		return false;
-
-	auto caster = getCurrentSpellcaster();
-
-	const spells::Mode mode = heroSpellToCast ? spells::Mode::HERO : spells::Mode::CREATURE_ACTIVE;
-
-	spells::Target target;
-	if(targetStack)
-		target.emplace_back(targetStack);
-	target.emplace_back(targetHex);
-
-	spells::BattleCast cast(owner.getBattle().get(), caster, mode, currentSpell);
-
-	auto m = currentSpell->battleMechanics(&cast);
-	spells::detail::ProblemImpl problem; //todo: display problem in status bar
-
-	return m->canBeCastAt(target, problem);
-}
-
-bool BattleActionsController::canStackMoveHere(const CStack * stackToMove, BattleHex myNumber) const
-{
-	std::vector<BattleHex> acc = owner.getBattle()->battleGetAvailableHexes(stackToMove, false);
-	BattleHex shiftedDest = myNumber.cloneInDirection(stackToMove->destShiftDir(), false);
-
-	if (vstd::contains(acc, myNumber))
-		return true;
-	else if (stackToMove->doubleWide() && vstd::contains(acc, shiftedDest))
-		return true;
-	else
-		return false;
-}
-
-void BattleActionsController::activateStack()
-{
-	const CStack * s = owner.stacksController->getActiveStack();
-	if(s)
-	{
-		tryActivateStackSpellcasting(s);
-
-		possibleActions = getPossibleActionsForStack(s);
-		std::list<PossiblePlayerBattleAction> actionsToSelect;
-		if(!possibleActions.empty())
-		{
-			switch(possibleActions.front().get())
-			{
-				case PossiblePlayerBattleAction::SHOOT:
-					actionsToSelect.push_back(possibleActions.front());
-					actionsToSelect.push_back(PossiblePlayerBattleAction::ATTACK);
-					break;
-					
-				case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
-					actionsToSelect.push_back(possibleActions.front());
-					actionsToSelect.push_back(PossiblePlayerBattleAction::WALK_AND_ATTACK);
-					break;
-					
-				case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
-					actionsToSelect.push_back(possibleActions.front());
-					actionsToSelect.push_back(PossiblePlayerBattleAction::ATTACK);
-					break;
-				case PossiblePlayerBattleAction::ANY_LOCATION:
-					actionsToSelect.push_back(possibleActions.front());
-					actionsToSelect.push_back(PossiblePlayerBattleAction::ATTACK);
-					break;
-			}
-		}
-		owner.windowObject->setAlternativeActions(actionsToSelect);
-	}
-}
-
-void BattleActionsController::onHexRightClicked(BattleHex clickedHex)
-{
-	auto spellcastActionPredicate = [](PossiblePlayerBattleAction & action)
-	{
-		return action.spellcast();
-	};
-
-	bool isCurrentStackInSpellcastMode = std::all_of(possibleActions.begin(), possibleActions.end(), spellcastActionPredicate);
-
-	if (spellcastingModeActive() || isCurrentStackInSpellcastMode)
-	{
-		endCastingSpell();
-		CRClickPopup::createAndPush(CGI->generaltexth->translate("core.genrltxt.731")); // spell cancelled
-		return;
-	}
-
-	auto selectedStack = owner.getBattle()->battleGetStackByPos(clickedHex, true);
-
-	if (selectedStack != nullptr)
-		GH.windows().createAndPushWindow<CStackWindow>(selectedStack, true);
-
-	if (clickedHex == BattleHex::HERO_ATTACKER && owner.attackingHero)
-		owner.attackingHero->heroRightClicked();
-
-	if (clickedHex == BattleHex::HERO_DEFENDER && owner.defendingHero)
-		owner.defendingHero->heroRightClicked();
-}
-
-bool BattleActionsController::spellcastingModeActive() const
-{
-	return heroSpellToCast != nullptr;
-}
-
-bool BattleActionsController::currentActionSpellcasting(BattleHex hoveredHex)
-{
-	if (heroSpellToCast)
-		return true;
-
-	if (!owner.stacksController->getActiveStack())
-		return false;
-
-	auto action = selectAction(hoveredHex);
-
-	return action.spellcast();
-}
-
-const std::vector<PossiblePlayerBattleAction> & BattleActionsController::getPossibleActions() const
-{
-	return possibleActions;
-}
-
-void BattleActionsController::removePossibleAction(PossiblePlayerBattleAction action)
-{
-	vstd::erase(possibleActions, action);
-}
-
-void BattleActionsController::pushFrontPossibleAction(PossiblePlayerBattleAction action)
-{
-	possibleActions.insert(possibleActions.begin(), action);
-}
+/*
+ * BattleActionsController.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 "BattleActionsController.h"
+
+#include "BattleWindow.h"
+#include "BattleStacksController.h"
+#include "BattleInterface.h"
+#include "BattleFieldController.h"
+#include "BattleSiegeController.h"
+#include "BattleInterfaceClasses.h"
+
+#include "../CGameInfo.h"
+#include "../CPlayerInterface.h"
+#include "../gui/CursorHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/CIntObject.h"
+#include "../gui/WindowHandler.h"
+#include "../windows/CCreatureWindow.h"
+#include "../windows/InfoWindows.h"
+
+#include "../../CCallback.h"
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/CStack.h"
+#include "../../lib/battle/BattleAction.h"
+#include "../../lib/spells/CSpellHandler.h"
+#include "../../lib/spells/ISpellMechanics.h"
+#include "../../lib/spells/Problem.h"
+
+struct TextReplacement
+{
+	std::string placeholder;
+	std::string replacement;
+};
+
+using TextReplacementList = std::vector<TextReplacement>;
+
+static std::string replacePlaceholders(std::string input, const TextReplacementList & format )
+{
+	for(const auto & entry : format)
+		boost::replace_all(input, entry.placeholder, entry.replacement);
+
+	return input;
+}
+
+static std::string translatePlural(int amount, const std::string& baseTextID)
+{
+	if(amount == 1)
+		return CGI->generaltexth->translate(baseTextID + ".1");
+	return CGI->generaltexth->translate(baseTextID);
+}
+
+static std::string formatPluralImpl(int amount, const std::string & amountString, const std::string & baseTextID)
+{
+	std::string baseString = translatePlural(amount, baseTextID);
+	TextReplacementList replacements {
+		{ "%d", amountString }
+	};
+
+	return replacePlaceholders(baseString, replacements);
+}
+
+static std::string formatPlural(int amount, const std::string & baseTextID)
+{
+	return formatPluralImpl(amount, std::to_string(amount), baseTextID);
+}
+
+static std::string formatPlural(DamageRange range, const std::string & baseTextID)
+{
+	if (range.min == range.max)
+		return formatPlural(range.min, baseTextID);
+
+	std::string rangeString = std::to_string(range.min) + " - " + std::to_string(range.max);
+
+	return formatPluralImpl(range.max, rangeString, baseTextID);
+}
+
+static std::string formatAttack(const DamageEstimation & estimation, const std::string & creatureName, const std::string & baseTextID, int shotsLeft)
+{
+	TextReplacementList replacements = {
+		{ "%CREATURE", creatureName },
+		{ "%DAMAGE", formatPlural(estimation.damage, "vcmi.battleWindow.damageEstimation.damage") },
+		{ "%SHOTS", formatPlural(shotsLeft, "vcmi.battleWindow.damageEstimation.shots") },
+		{ "%KILLS", formatPlural(estimation.kills, "vcmi.battleWindow.damageEstimation.kills") },
+	};
+
+	return replacePlaceholders(CGI->generaltexth->translate(baseTextID), replacements);
+}
+
+static std::string formatMeleeAttack(const DamageEstimation & estimation, const std::string & creatureName)
+{
+	std::string baseTextID = estimation.kills.max == 0 ?
+		"vcmi.battleWindow.damageEstimation.melee" :
+		"vcmi.battleWindow.damageEstimation.meleeKills";
+
+	return formatAttack(estimation, creatureName, baseTextID, 0);
+}
+
+static std::string formatRangedAttack(const DamageEstimation & estimation, const std::string & creatureName, int shotsLeft)
+{
+	std::string baseTextID = estimation.kills.max == 0 ?
+		"vcmi.battleWindow.damageEstimation.ranged" :
+		"vcmi.battleWindow.damageEstimation.rangedKills";
+
+	return formatAttack(estimation, creatureName, baseTextID, shotsLeft);
+}
+
+BattleActionsController::BattleActionsController(BattleInterface & owner):
+	owner(owner),
+	selectedStack(nullptr),
+	heroSpellToCast(nullptr)
+{
+}
+
+void BattleActionsController::endCastingSpell()
+{
+	if(heroSpellToCast)
+	{
+		heroSpellToCast.reset();
+		owner.windowObject->blockUI(false);
+	}
+
+	if(owner.stacksController->getActiveStack())
+		possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack()); //restore actions after they were cleared
+
+	selectedStack = nullptr;
+	GH.fakeMouseMove();
+}
+
+bool BattleActionsController::isActiveStackSpellcaster() const
+{
+	const CStack * casterStack = owner.stacksController->getActiveStack();
+	if (!casterStack)
+		return false;
+
+	bool spellcaster = casterStack->hasBonusOfType(BonusType::SPELLCASTER);
+	return (spellcaster && casterStack->canCast());
+}
+
+void BattleActionsController::enterCreatureCastingMode()
+{
+	//silently check for possible errors
+	if (owner.tacticsMode)
+		return;
+
+	//hero is casting a spell
+	if (heroSpellToCast)
+		return;
+
+	if (!owner.stacksController->getActiveStack())
+		return;
+
+	if (!isActiveStackSpellcaster())
+		return;
+
+	for(const auto & action : possibleActions)
+	{
+		if (action.get() != PossiblePlayerBattleAction::NO_LOCATION)
+			continue;
+
+		const spells::Caster * caster = owner.stacksController->getActiveStack();
+		const CSpell * spell = action.spell().toSpell();
+
+		spells::Target target;
+		target.emplace_back();
+
+		spells::BattleCast cast(owner.getBattle().get(), caster, spells::Mode::CREATURE_ACTIVE, spell);
+
+		auto m = spell->battleMechanics(&cast);
+		spells::detail::ProblemImpl ignored;
+
+		const bool isCastingPossible = m->canBeCastAt(target, ignored);
+
+		if (isCastingPossible)
+		{
+			owner.giveCommand(EActionType::MONSTER_SPELL, BattleHex::INVALID, spell->getId());
+			selectedStack = nullptr;
+
+			CCS->curh->set(Cursor::Combat::POINTER);
+		}
+		return;
+	}
+
+	possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack());
+
+	auto actionFilterPredicate = [](const PossiblePlayerBattleAction x)
+	{
+		return !x.spellcast();
+	};
+
+	vstd::erase_if(possibleActions, actionFilterPredicate);
+	GH.fakeMouseMove();
+}
+
+std::vector<PossiblePlayerBattleAction> BattleActionsController::getPossibleActionsForStack(const CStack *stack) const
+{
+	BattleClientInterfaceData data; //hard to get rid of these things so for now they're required data to pass
+
+	for(const auto & spell : creatureSpells)
+		data.creatureSpellsToCast.push_back(spell->id);
+
+	data.tacticsMode = owner.tacticsMode;
+	auto allActions = owner.getBattle()->getClientActionsForStack(stack, data);
+
+	allActions.push_back(PossiblePlayerBattleAction::HERO_INFO);
+	allActions.push_back(PossiblePlayerBattleAction::CREATURE_INFO);
+
+	return std::vector<PossiblePlayerBattleAction>(allActions);
+}
+
+void BattleActionsController::reorderPossibleActionsPriority(const CStack * stack, const CStack * targetStack)
+{
+	if(owner.tacticsMode || possibleActions.empty()) return; //this function is not supposed to be called in tactics mode or before getPossibleActionsForStack
+
+	auto assignPriority = [&](const PossiblePlayerBattleAction & item
+						  ) -> uint8_t //large lambda assigning priority which would have to be part of possibleActions without it
+	{
+		switch(item.get())
+		{
+			case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
+			case PossiblePlayerBattleAction::ANY_LOCATION:
+			case PossiblePlayerBattleAction::NO_LOCATION:
+			case PossiblePlayerBattleAction::FREE_LOCATION:
+			case PossiblePlayerBattleAction::OBSTACLE:
+				if(!stack->hasBonusOfType(BonusType::NO_SPELLCAST_BY_DEFAULT) && targetStack != nullptr)
+				{
+					PlayerColor stackOwner = owner.getBattle()->battleGetOwner(targetStack);
+					bool enemyTargetingPositiveSpellcast = item.spell().toSpell()->isPositive() && stackOwner != LOCPLINT->playerID;
+					bool friendTargetingNegativeSpellcast = item.spell().toSpell()->isNegative() && stackOwner == LOCPLINT->playerID;
+
+					if(!enemyTargetingPositiveSpellcast && !friendTargetingNegativeSpellcast)
+						return 1;
+				}
+				return 100; //bottom priority
+
+				break;
+			case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
+				return 2;
+				break;
+			case PossiblePlayerBattleAction::SHOOT:
+				return 4;
+				break;
+			case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
+				return 5;
+				break;
+			case PossiblePlayerBattleAction::ATTACK:
+				return 6;
+				break;
+			case PossiblePlayerBattleAction::WALK_AND_ATTACK:
+				return 7;
+				break;
+			case PossiblePlayerBattleAction::MOVE_STACK:
+				return 8;
+				break;
+			case PossiblePlayerBattleAction::CATAPULT:
+				return 9;
+				break;
+			case PossiblePlayerBattleAction::HEAL:
+				return 10;
+				break;
+			case PossiblePlayerBattleAction::CREATURE_INFO:
+				return 11;
+				break;
+			case PossiblePlayerBattleAction::HERO_INFO:
+				return 12;
+				break;
+			case PossiblePlayerBattleAction::TELEPORT:
+				return 13;
+				break;
+			default:
+				assert(0);
+				return 200;
+				break;
+		}
+	};
+
+	auto comparer = [&](const PossiblePlayerBattleAction & lhs, const PossiblePlayerBattleAction & rhs)
+	{
+		return assignPriority(lhs) < assignPriority(rhs);
+	};
+
+	std::sort(possibleActions.begin(), possibleActions.end(), comparer);
+}
+
+void BattleActionsController::castThisSpell(SpellID spellID)
+{
+	heroSpellToCast = std::make_shared<BattleAction>();
+	heroSpellToCast->actionType = EActionType::HERO_SPELL;
+	heroSpellToCast->spell = spellID;
+	heroSpellToCast->stackNumber = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? -1 : -2;
+	heroSpellToCast->side = owner.defendingHeroInstance ? (owner.curInt->playerID == owner.defendingHeroInstance->tempOwner) : false;
+
+	//choosing possible targets
+	const CGHeroInstance *castingHero = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? owner.attackingHeroInstance : owner.defendingHeroInstance;
+	assert(castingHero); // code below assumes non-null hero
+	PossiblePlayerBattleAction spellSelMode = owner.getBattle()->getCasterAction(spellID.toSpell(), castingHero, spells::Mode::HERO);
+
+	if (spellSelMode.get() == PossiblePlayerBattleAction::NO_LOCATION) //user does not have to select location
+	{
+		heroSpellToCast->aimToHex(BattleHex::INVALID);
+		owner.curInt->cb->battleMakeSpellAction(owner.getBattleID(), *heroSpellToCast);
+		endCastingSpell();
+	}
+	else
+	{
+		possibleActions.clear();
+		possibleActions.push_back (spellSelMode); //only this one action can be performed at the moment
+		GH.fakeMouseMove();//update cursor
+	}
+
+	owner.windowObject->blockUI(true);
+}
+
+const CSpell * BattleActionsController::getHeroSpellToCast( ) const
+{
+	if (heroSpellToCast)
+		return heroSpellToCast->spell.toSpell();
+	return nullptr;
+}
+
+const CSpell * BattleActionsController::getStackSpellToCast(BattleHex hoveredHex)
+{
+	if (heroSpellToCast)
+		return nullptr;
+
+	if (!owner.stacksController->getActiveStack())
+		return nullptr;
+
+	if (!hoveredHex.isValid())
+		return nullptr;
+
+	auto action = selectAction(hoveredHex);
+
+	if (action.spell() == SpellID::NONE)
+		return nullptr;
+
+	return action.spell().toSpell();
+}
+
+const CSpell * BattleActionsController::getCurrentSpell(BattleHex hoveredHex)
+{
+	if (getHeroSpellToCast())
+		return getHeroSpellToCast();
+	return getStackSpellToCast(hoveredHex);
+}
+
+const CStack * BattleActionsController::getStackForHex(BattleHex hoveredHex)
+{
+	const CStack * shere = owner.getBattle()->battleGetStackByPos(hoveredHex, true);
+	if(shere)
+		return shere;
+	return owner.getBattle()->battleGetStackByPos(hoveredHex, false);
+}
+
+void BattleActionsController::actionSetCursor(PossiblePlayerBattleAction action, BattleHex targetHex)
+{
+	switch (action.get())
+	{
+		case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
+			CCS->curh->set(Cursor::Combat::POINTER);
+			return;
+
+		case PossiblePlayerBattleAction::MOVE_TACTICS:
+		case PossiblePlayerBattleAction::MOVE_STACK:
+			if (owner.stacksController->getActiveStack()->hasBonusOfType(BonusType::FLYING))
+				CCS->curh->set(Cursor::Combat::FLY);
+			else
+				CCS->curh->set(Cursor::Combat::MOVE);
+			return;
+
+		case PossiblePlayerBattleAction::ATTACK:
+		case PossiblePlayerBattleAction::WALK_AND_ATTACK:
+		case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
+		{
+			static const std::map<BattleHex::EDir, Cursor::Combat> sectorCursor = {
+				{BattleHex::TOP_LEFT,     Cursor::Combat::HIT_SOUTHEAST},
+				{BattleHex::TOP_RIGHT,    Cursor::Combat::HIT_SOUTHWEST},
+				{BattleHex::RIGHT,        Cursor::Combat::HIT_WEST     },
+				{BattleHex::BOTTOM_RIGHT, Cursor::Combat::HIT_NORTHWEST},
+				{BattleHex::BOTTOM_LEFT,  Cursor::Combat::HIT_NORTHEAST},
+				{BattleHex::LEFT,         Cursor::Combat::HIT_EAST     },
+				{BattleHex::TOP,          Cursor::Combat::HIT_SOUTH    },
+				{BattleHex::BOTTOM,       Cursor::Combat::HIT_NORTH    }
+			};
+
+			auto direction = owner.fieldController->selectAttackDirection(targetHex);
+
+			assert(sectorCursor.count(direction) > 0);
+			if (sectorCursor.count(direction))
+				CCS->curh->set(sectorCursor.at(direction));
+
+			return;
+		}
+
+		case PossiblePlayerBattleAction::SHOOT:
+			if (owner.getBattle()->battleHasShootingPenalty(owner.stacksController->getActiveStack(), targetHex))
+				CCS->curh->set(Cursor::Combat::SHOOT_PENALTY);
+			else
+				CCS->curh->set(Cursor::Combat::SHOOT);
+			return;
+
+		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
+		case PossiblePlayerBattleAction::ANY_LOCATION:
+		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
+		case PossiblePlayerBattleAction::FREE_LOCATION:
+		case PossiblePlayerBattleAction::OBSTACLE:
+			CCS->curh->set(Cursor::Spellcast::SPELL);
+			return;
+
+		case PossiblePlayerBattleAction::TELEPORT:
+			CCS->curh->set(Cursor::Combat::TELEPORT);
+			return;
+
+		case PossiblePlayerBattleAction::SACRIFICE:
+			CCS->curh->set(Cursor::Combat::SACRIFICE);
+			return;
+
+		case PossiblePlayerBattleAction::HEAL:
+			CCS->curh->set(Cursor::Combat::HEAL);
+			return;
+
+		case PossiblePlayerBattleAction::CATAPULT:
+			CCS->curh->set(Cursor::Combat::SHOOT_CATAPULT);
+			return;
+
+		case PossiblePlayerBattleAction::CREATURE_INFO:
+			CCS->curh->set(Cursor::Combat::QUERY);
+			return;
+		case PossiblePlayerBattleAction::HERO_INFO:
+			CCS->curh->set(Cursor::Combat::HERO);
+			return;
+	}
+	assert(0);
+}
+
+void BattleActionsController::actionSetCursorBlocked(PossiblePlayerBattleAction action, BattleHex targetHex)
+{
+	switch (action.get())
+	{
+		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
+		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
+		case PossiblePlayerBattleAction::TELEPORT:
+		case PossiblePlayerBattleAction::SACRIFICE:
+		case PossiblePlayerBattleAction::FREE_LOCATION:
+			CCS->curh->set(Cursor::Combat::BLOCKED);
+			return;
+		default:
+			if (targetHex == -1)
+				CCS->curh->set(Cursor::Combat::POINTER);
+			else
+				CCS->curh->set(Cursor::Combat::BLOCKED);
+			return;
+	}
+	assert(0);
+}
+
+std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattleAction action, BattleHex targetHex)
+{
+	const CStack * targetStack = getStackForHex(targetHex);
+
+	switch (action.get()) //display console message, realize selected action
+	{
+		case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
+			return (boost::format(CGI->generaltexth->allTexts[481]) % targetStack->getName()).str(); //Select %s
+
+		case PossiblePlayerBattleAction::MOVE_TACTICS:
+		case PossiblePlayerBattleAction::MOVE_STACK:
+			if (owner.stacksController->getActiveStack()->hasBonusOfType(BonusType::FLYING))
+				return (boost::format(CGI->generaltexth->allTexts[295]) % owner.stacksController->getActiveStack()->getName()).str(); //Fly %s here
+			else
+				return (boost::format(CGI->generaltexth->allTexts[294]) % owner.stacksController->getActiveStack()->getName()).str(); //Move %s here
+
+		case PossiblePlayerBattleAction::ATTACK:
+		case PossiblePlayerBattleAction::WALK_AND_ATTACK:
+		case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return
+			{
+				BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex);
+				DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex);
+				estimation.kills.max = std::min<int64_t>(estimation.kills.max, targetStack->getCount());
+				estimation.kills.min = std::min<int64_t>(estimation.kills.min, targetStack->getCount());
+
+				return formatMeleeAttack(estimation, targetStack->getName());
+			}
+
+		case PossiblePlayerBattleAction::SHOOT:
+		{
+			const auto * shooter = owner.stacksController->getActiveStack();
+
+			DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(shooter, targetStack, shooter->getPosition());
+			estimation.kills.max = std::min<int64_t>(estimation.kills.max, targetStack->getCount());
+			estimation.kills.min = std::min<int64_t>(estimation.kills.min, targetStack->getCount());
+
+			return formatRangedAttack(estimation, targetStack->getName(), shooter->shots.available());
+		}
+
+		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
+			return boost::str(boost::format(CGI->generaltexth->allTexts[27]) % action.spell().toSpell()->getNameTranslated() % targetStack->getName()); //Cast %s on %s
+
+		case PossiblePlayerBattleAction::ANY_LOCATION:
+			return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % action.spell().toSpell()->getNameTranslated()); //Cast %s
+
+		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell
+			return boost::str(boost::format(CGI->generaltexth->allTexts[301]) % targetStack->getName()); //Cast a spell on %
+
+		case PossiblePlayerBattleAction::TELEPORT:
+			return CGI->generaltexth->allTexts[25]; //Teleport Here
+
+		case PossiblePlayerBattleAction::OBSTACLE:
+			return CGI->generaltexth->allTexts[550];
+
+		case PossiblePlayerBattleAction::SACRIFICE:
+			return (boost::format(CGI->generaltexth->allTexts[549]) % targetStack->getName()).str(); //sacrifice the %s
+
+		case PossiblePlayerBattleAction::FREE_LOCATION:
+			return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % action.spell().toSpell()->getNameTranslated()); //Cast %s
+
+		case PossiblePlayerBattleAction::HEAL:
+			return (boost::format(CGI->generaltexth->allTexts[419]) % targetStack->getName()).str(); //Apply first aid to the %s
+
+		case PossiblePlayerBattleAction::CATAPULT:
+			return ""; // TODO
+
+		case PossiblePlayerBattleAction::CREATURE_INFO:
+			return (boost::format(CGI->generaltexth->allTexts[297]) % targetStack->getName()).str();
+
+		case PossiblePlayerBattleAction::HERO_INFO:
+			return  CGI->generaltexth->translate("core.genrltxt.417"); // "View Hero Stats"
+	}
+	assert(0);
+	return "";
+}
+
+std::string BattleActionsController::actionGetStatusMessageBlocked(PossiblePlayerBattleAction action, BattleHex targetHex)
+{
+	switch (action.get())
+	{
+		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
+		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
+			return CGI->generaltexth->allTexts[23];
+			break;
+		case PossiblePlayerBattleAction::TELEPORT:
+			return CGI->generaltexth->allTexts[24]; //Invalid Teleport Destination
+			break;
+		case PossiblePlayerBattleAction::SACRIFICE:
+			return CGI->generaltexth->allTexts[543]; //choose army to sacrifice
+			break;
+		case PossiblePlayerBattleAction::FREE_LOCATION:
+			return boost::str(boost::format(CGI->generaltexth->allTexts[181]) % action.spell().toSpell()->getNameTranslated()); //No room to place %s here
+			break;
+		default:
+			return "";
+	}
+}
+
+bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, BattleHex targetHex)
+{
+	const CStack * targetStack = getStackForHex(targetHex);
+	bool targetStackOwned = targetStack && targetStack->unitOwner() == owner.curInt->playerID;
+
+	switch (action.get())
+	{
+		case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
+			return (targetStack && targetStackOwned && targetStack->speed() > 0);
+
+		case PossiblePlayerBattleAction::CREATURE_INFO:
+			return (targetStack && targetStackOwned && targetStack->alive());
+
+		case PossiblePlayerBattleAction::HERO_INFO:
+			if (targetHex == BattleHex::HERO_ATTACKER)
+				return owner.attackingHero != nullptr;
+
+			if (targetHex == BattleHex::HERO_DEFENDER)
+				return owner.defendingHero != nullptr;
+
+			return false;
+
+		case PossiblePlayerBattleAction::MOVE_TACTICS:
+		case PossiblePlayerBattleAction::MOVE_STACK:
+			if (!(targetStack && targetStack->alive())) //we can walk on dead stacks
+			{
+				if(canStackMoveHere(owner.stacksController->getActiveStack(), targetHex))
+					return true;
+			}
+			return false;
+
+		case PossiblePlayerBattleAction::ATTACK:
+		case PossiblePlayerBattleAction::WALK_AND_ATTACK:
+		case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
+			if(owner.getBattle()->battleCanAttack(owner.stacksController->getActiveStack(), targetStack, targetHex))
+			{
+				if (owner.fieldController->isTileAttackable(targetHex)) // move isTileAttackable to be part of battleCanAttack?
+					return true;
+			}
+			return false;
+
+		case PossiblePlayerBattleAction::SHOOT:
+			return owner.getBattle()->battleCanShoot(owner.stacksController->getActiveStack(), targetHex);
+
+		case PossiblePlayerBattleAction::NO_LOCATION:
+			return false;
+
+		case PossiblePlayerBattleAction::ANY_LOCATION:
+			return isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex);
+
+		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
+			return !selectedStack && targetStack && isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex);
+
+		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
+			if(targetStack && targetStackOwned && targetStack != owner.stacksController->getActiveStack() && targetStack->alive()) //only positive spells for other allied creatures
+			{
+				int spellID = owner.getBattle()->battleGetRandomStackSpell(CRandomGenerator::getDefault(), targetStack, CBattleInfoCallback::RANDOM_GENIE);
+				return spellID > -1;
+			}
+			return false;
+
+		case PossiblePlayerBattleAction::TELEPORT:
+			return selectedStack && isCastingPossibleHere(action.spell().toSpell(), selectedStack, targetHex);
+
+		case PossiblePlayerBattleAction::SACRIFICE: //choose our living stack to sacrifice
+			return targetStack && targetStack != selectedStack && targetStackOwned && targetStack->alive();
+
+		case PossiblePlayerBattleAction::OBSTACLE:
+		case PossiblePlayerBattleAction::FREE_LOCATION:
+			return isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex);
+
+		case PossiblePlayerBattleAction::CATAPULT:
+			return owner.siegeController && owner.siegeController->isAttackableByCatapult(targetHex);
+
+		case PossiblePlayerBattleAction::HEAL:
+			return targetStack && targetStackOwned && targetStack->canBeHealed();
+	}
+
+	assert(0);
+	return false;
+}
+
+void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, BattleHex targetHex)
+{
+	const CStack * targetStack = getStackForHex(targetHex);
+
+	switch (action.get()) //display console message, realize selected action
+	{
+		case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
+		{
+			owner.stackActivated(targetStack);
+			return;
+		}
+
+		case PossiblePlayerBattleAction::MOVE_TACTICS:
+		case PossiblePlayerBattleAction::MOVE_STACK:
+		{
+			if(owner.stacksController->getActiveStack()->doubleWide())
+			{
+				std::vector<BattleHex> acc = owner.getBattle()->battleGetAvailableHexes(owner.stacksController->getActiveStack(), false);
+				BattleHex shiftedDest = targetHex.cloneInDirection(owner.stacksController->getActiveStack()->destShiftDir(), false);
+				if(vstd::contains(acc, targetHex))
+					owner.giveCommand(EActionType::WALK, targetHex);
+				else if(vstd::contains(acc, shiftedDest))
+					owner.giveCommand(EActionType::WALK, shiftedDest);
+			}
+			else
+			{
+				owner.giveCommand(EActionType::WALK, targetHex);
+			}
+			return;
+		}
+
+		case PossiblePlayerBattleAction::ATTACK:
+		case PossiblePlayerBattleAction::WALK_AND_ATTACK:
+		case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return
+		{
+			bool returnAfterAttack = action.get() == PossiblePlayerBattleAction::ATTACK_AND_RETURN;
+			BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex);
+			if(attackFromHex.isValid()) //we can be in this line when unreachable creature is L - clicked (as of revision 1308)
+			{
+				BattleAction command = BattleAction::makeMeleeAttack(owner.stacksController->getActiveStack(), targetHex, attackFromHex, returnAfterAttack);
+				owner.sendCommand(command, owner.stacksController->getActiveStack());
+			}
+			return;
+		}
+
+		case PossiblePlayerBattleAction::SHOOT:
+		{
+			owner.giveCommand(EActionType::SHOOT, targetHex);
+			return;
+		}
+
+		case PossiblePlayerBattleAction::HEAL:
+		{
+			owner.giveCommand(EActionType::STACK_HEAL, targetHex);
+			return;
+		};
+
+		case PossiblePlayerBattleAction::CATAPULT:
+		{
+			owner.giveCommand(EActionType::CATAPULT, targetHex);
+			return;
+		}
+
+		case PossiblePlayerBattleAction::CREATURE_INFO:
+		{
+			GH.windows().createAndPushWindow<CStackWindow>(targetStack, false);
+			return;
+		}
+
+		case PossiblePlayerBattleAction::HERO_INFO:
+		{
+			if (targetHex == BattleHex::HERO_ATTACKER)
+				owner.attackingHero->heroLeftClicked();
+
+			if (targetHex == BattleHex::HERO_DEFENDER)
+				owner.defendingHero->heroLeftClicked();
+
+			return;
+		}
+
+		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
+		case PossiblePlayerBattleAction::ANY_LOCATION:
+		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell
+		case PossiblePlayerBattleAction::TELEPORT:
+		case PossiblePlayerBattleAction::OBSTACLE:
+		case PossiblePlayerBattleAction::SACRIFICE:
+		case PossiblePlayerBattleAction::FREE_LOCATION:
+		{
+			if (action.get() == PossiblePlayerBattleAction::AIMED_SPELL_CREATURE )
+			{
+				if (action.spell() == SpellID::SACRIFICE)
+				{
+					heroSpellToCast->aimToHex(targetHex);
+					possibleActions.push_back({PossiblePlayerBattleAction::SACRIFICE, action.spell()});
+					selectedStack = targetStack;
+					return;
+				}
+				if (action.spell() == SpellID::TELEPORT)
+				{
+					heroSpellToCast->aimToUnit(targetStack);
+					possibleActions.push_back({PossiblePlayerBattleAction::TELEPORT, action.spell()});
+					selectedStack = targetStack;
+					return;
+				}
+			}
+
+			if (!spellcastingModeActive())
+			{
+				if (action.spell().toSpell())
+				{
+					owner.giveCommand(EActionType::MONSTER_SPELL, targetHex, action.spell());
+				}
+				else //unknown random spell
+				{
+					owner.giveCommand(EActionType::MONSTER_SPELL, targetHex);
+				}
+			}
+			else
+			{
+				assert(getHeroSpellToCast());
+				switch (getHeroSpellToCast()->id.toEnum())
+				{
+					case SpellID::SACRIFICE:
+						heroSpellToCast->aimToUnit(targetStack);//victim
+						break;
+					default:
+						heroSpellToCast->aimToHex(targetHex);
+						break;
+				}
+				owner.curInt->cb->battleMakeSpellAction(owner.getBattleID(), *heroSpellToCast);
+				endCastingSpell();
+			}
+			selectedStack = nullptr;
+			return;
+		}
+	}
+	assert(0);
+	return;
+}
+
+PossiblePlayerBattleAction BattleActionsController::selectAction(BattleHex targetHex)
+{
+	assert(owner.stacksController->getActiveStack() != nullptr);
+	assert(!possibleActions.empty());
+	assert(targetHex.isValid());
+
+	if (owner.stacksController->getActiveStack() == nullptr)
+		return PossiblePlayerBattleAction::INVALID;
+
+	if (possibleActions.empty())
+		return PossiblePlayerBattleAction::INVALID;
+
+	const CStack * targetStack = getStackForHex(targetHex);
+
+	reorderPossibleActionsPriority(owner.stacksController->getActiveStack(), targetStack);
+
+	for (PossiblePlayerBattleAction action : possibleActions)
+	{
+		if (actionIsLegal(action, targetHex))
+			return action;
+	}
+	return possibleActions.front();
+}
+
+void BattleActionsController::onHexHovered(BattleHex hoveredHex)
+{
+	if (owner.openingPlaying())
+	{
+		currentConsoleMsg = VLC->generaltexth->translate("vcmi.battleWindow.pressKeyToSkipIntro");
+		GH.statusbar()->write(currentConsoleMsg);
+		return;
+	}
+
+	if (owner.stacksController->getActiveStack() == nullptr)
+		return;
+
+	if (hoveredHex == BattleHex::INVALID)
+	{
+		if (!currentConsoleMsg.empty())
+			GH.statusbar()->clearIfMatching(currentConsoleMsg);
+
+		currentConsoleMsg.clear();
+		CCS->curh->set(Cursor::Combat::BLOCKED);
+		return;
+	}
+
+	auto action = selectAction(hoveredHex);
+
+	std::string newConsoleMsg;
+
+	if (actionIsLegal(action, hoveredHex))
+	{
+		actionSetCursor(action, hoveredHex);
+		newConsoleMsg = actionGetStatusMessage(action, hoveredHex);
+	}
+	else
+	{
+		actionSetCursorBlocked(action, hoveredHex);
+		newConsoleMsg = actionGetStatusMessageBlocked(action, hoveredHex);
+	}
+
+	if (!currentConsoleMsg.empty())
+		GH.statusbar()->clearIfMatching(currentConsoleMsg);
+
+	if (!newConsoleMsg.empty())
+		GH.statusbar()->write(newConsoleMsg);
+
+	currentConsoleMsg = newConsoleMsg;
+}
+
+void BattleActionsController::onHoverEnded()
+{
+	CCS->curh->set(Cursor::Combat::POINTER);
+
+	if (!currentConsoleMsg.empty())
+		GH.statusbar()->clearIfMatching(currentConsoleMsg);
+
+	currentConsoleMsg.clear();
+}
+
+void BattleActionsController::onHexLeftClicked(BattleHex clickedHex)
+{
+	if (owner.stacksController->getActiveStack() == nullptr)
+		return;
+
+	auto action = selectAction(clickedHex);
+
+	std::string newConsoleMsg;
+
+	if (!actionIsLegal(action, clickedHex))
+		return;
+	
+	actionRealize(action, clickedHex);
+	GH.statusbar()->clear();
+}
+
+void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterStack)
+{
+	creatureSpells.clear();
+
+	bool spellcaster = casterStack->hasBonusOfType(BonusType::SPELLCASTER);
+	if(casterStack->canCast() && spellcaster)
+	{
+		// faerie dragon can cast only one, randomly selected spell until their next move
+		//TODO: faerie dragon type spell should be selected by server
+		const auto * spellToCast = owner.getBattle()->battleGetRandomStackSpell(CRandomGenerator::getDefault(), casterStack, CBattleInfoCallback::RANDOM_AIMED).toSpell();
+
+		if (spellToCast)
+			creatureSpells.push_back(spellToCast);
+	}
+
+	TConstBonusListPtr bl = casterStack->getBonuses(Selector::type()(BonusType::SPELLCASTER));
+
+	for(const auto & bonus : *bl)
+	{
+		if (bonus->additionalInfo[0] <= 0)
+			creatureSpells.push_back(bonus->subtype.as<SpellID>().toSpell());
+	}
+}
+
+const spells::Caster * BattleActionsController::getCurrentSpellcaster() const
+{
+	if (heroSpellToCast)
+		return owner.getActiveHero();
+	else
+		return owner.stacksController->getActiveStack();
+}
+
+spells::Mode BattleActionsController::getCurrentCastMode() const
+{
+	if (heroSpellToCast)
+		return spells::Mode::HERO;
+	else
+		return spells::Mode::CREATURE_ACTIVE;
+
+}
+
+bool BattleActionsController::isCastingPossibleHere(const CSpell * currentSpell, const CStack *targetStack, BattleHex targetHex)
+{
+	assert(currentSpell);
+	if (!currentSpell)
+		return false;
+
+	auto caster = getCurrentSpellcaster();
+
+	const spells::Mode mode = heroSpellToCast ? spells::Mode::HERO : spells::Mode::CREATURE_ACTIVE;
+
+	spells::Target target;
+	if(targetStack)
+		target.emplace_back(targetStack);
+	target.emplace_back(targetHex);
+
+	spells::BattleCast cast(owner.getBattle().get(), caster, mode, currentSpell);
+
+	auto m = currentSpell->battleMechanics(&cast);
+	spells::detail::ProblemImpl problem; //todo: display problem in status bar
+
+	return m->canBeCastAt(target, problem);
+}
+
+bool BattleActionsController::canStackMoveHere(const CStack * stackToMove, BattleHex myNumber) const
+{
+	std::vector<BattleHex> acc = owner.getBattle()->battleGetAvailableHexes(stackToMove, false);
+	BattleHex shiftedDest = myNumber.cloneInDirection(stackToMove->destShiftDir(), false);
+
+	if (vstd::contains(acc, myNumber))
+		return true;
+	else if (stackToMove->doubleWide() && vstd::contains(acc, shiftedDest))
+		return true;
+	else
+		return false;
+}
+
+void BattleActionsController::activateStack()
+{
+	const CStack * s = owner.stacksController->getActiveStack();
+	if(s)
+	{
+		tryActivateStackSpellcasting(s);
+
+		possibleActions = getPossibleActionsForStack(s);
+		std::list<PossiblePlayerBattleAction> actionsToSelect;
+		if(!possibleActions.empty())
+		{
+			switch(possibleActions.front().get())
+			{
+				case PossiblePlayerBattleAction::SHOOT:
+					actionsToSelect.push_back(possibleActions.front());
+					actionsToSelect.push_back(PossiblePlayerBattleAction::ATTACK);
+					break;
+					
+				case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
+					actionsToSelect.push_back(possibleActions.front());
+					actionsToSelect.push_back(PossiblePlayerBattleAction::WALK_AND_ATTACK);
+					break;
+					
+				case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
+					actionsToSelect.push_back(possibleActions.front());
+					actionsToSelect.push_back(PossiblePlayerBattleAction::ATTACK);
+					break;
+				case PossiblePlayerBattleAction::ANY_LOCATION:
+					actionsToSelect.push_back(possibleActions.front());
+					actionsToSelect.push_back(PossiblePlayerBattleAction::ATTACK);
+					break;
+			}
+		}
+		owner.windowObject->setAlternativeActions(actionsToSelect);
+	}
+}
+
+void BattleActionsController::onHexRightClicked(BattleHex clickedHex)
+{
+	auto spellcastActionPredicate = [](PossiblePlayerBattleAction & action)
+	{
+		return action.spellcast();
+	};
+
+	bool isCurrentStackInSpellcastMode = std::all_of(possibleActions.begin(), possibleActions.end(), spellcastActionPredicate);
+
+	if (spellcastingModeActive() || isCurrentStackInSpellcastMode)
+	{
+		endCastingSpell();
+		CRClickPopup::createAndPush(CGI->generaltexth->translate("core.genrltxt.731")); // spell cancelled
+		return;
+	}
+
+	auto selectedStack = owner.getBattle()->battleGetStackByPos(clickedHex, true);
+
+	if (selectedStack != nullptr)
+		GH.windows().createAndPushWindow<CStackWindow>(selectedStack, true);
+
+	if (clickedHex == BattleHex::HERO_ATTACKER && owner.attackingHero)
+		owner.attackingHero->heroRightClicked();
+
+	if (clickedHex == BattleHex::HERO_DEFENDER && owner.defendingHero)
+		owner.defendingHero->heroRightClicked();
+}
+
+bool BattleActionsController::spellcastingModeActive() const
+{
+	return heroSpellToCast != nullptr;
+}
+
+bool BattleActionsController::currentActionSpellcasting(BattleHex hoveredHex)
+{
+	if (heroSpellToCast)
+		return true;
+
+	if (!owner.stacksController->getActiveStack())
+		return false;
+
+	auto action = selectAction(hoveredHex);
+
+	return action.spellcast();
+}
+
+const std::vector<PossiblePlayerBattleAction> & BattleActionsController::getPossibleActions() const
+{
+	return possibleActions;
+}
+
+void BattleActionsController::removePossibleAction(PossiblePlayerBattleAction action)
+{
+	vstd::erase(possibleActions, action);
+}
+
+void BattleActionsController::pushFrontPossibleAction(PossiblePlayerBattleAction action)
+{
+	possibleActions.insert(possibleActions.begin(), action);
+}

+ 125 - 125
client/battle/BattleActionsController.h

@@ -1,125 +1,125 @@
-/*
- * BattleActionsController.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/battle/CBattleInfoCallback.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-class BattleAction;
-namespace spells {
-class Caster;
-enum class Mode;
-}
-
-VCMI_LIB_NAMESPACE_END
-
-class BattleInterface;
-
-/// Class that controls actions that can be performed by player, e.g. moving stacks, attacking, etc
-/// As well as all relevant feedback for these actions in user interface
-class BattleActionsController
-{
-	BattleInterface & owner;
-	
-	/// all actions possible to call at the moment by player
-	std::vector<PossiblePlayerBattleAction> possibleActions;
-
-	/// spell for which player's hero is choosing destination
-	std::shared_ptr<BattleAction> heroSpellToCast;
-
-	/// cached message that was set by this class in status bar
-	std::string currentConsoleMsg;
-
-	/// if true, active stack could possibly cast some target spell
-	std::vector<const CSpell *> creatureSpells;
-
-	/// stack that has been selected as first target for multi-target spells (Teleport & Sacrifice)
-	const CStack * selectedStack;
-
-	bool isCastingPossibleHere (const CSpell * spell, const CStack *shere, BattleHex myNumber);
-	bool canStackMoveHere (const CStack *sactive, BattleHex MyNumber) const; //TODO: move to BattleState / callback
-	std::vector<PossiblePlayerBattleAction> getPossibleActionsForStack (const CStack *stack) const; //called when stack gets its turn
-	void reorderPossibleActionsPriority(const CStack * stack, const CStack * targetStack);
-
-	bool actionIsLegal(PossiblePlayerBattleAction action, BattleHex hoveredHex);
-
-	void actionSetCursor(PossiblePlayerBattleAction action, BattleHex hoveredHex);
-	void actionSetCursorBlocked(PossiblePlayerBattleAction action, BattleHex hoveredHex);
-
-	std::string actionGetStatusMessage(PossiblePlayerBattleAction action, BattleHex hoveredHex);
-	std::string actionGetStatusMessageBlocked(PossiblePlayerBattleAction action, BattleHex hoveredHex);
-
-	void actionRealize(PossiblePlayerBattleAction action, BattleHex hoveredHex);
-
-	PossiblePlayerBattleAction selectAction(BattleHex myNumber);
-
-	const CStack * getStackForHex(BattleHex myNumber) ;
-
-	/// attempts to initialize spellcasting action for stack
-	/// will silently return if stack is not a spellcaster
-	void tryActivateStackSpellcasting(const CStack *casterStack);
-
-	/// returns spell that is currently being cast by hero or nullptr if none
-	const CSpell * getHeroSpellToCast() const;
-
-	/// if current stack is spellcaster, returns spell being cast, or null othervice
-	const CSpell * getStackSpellToCast(BattleHex hoveredHex);
-
-	/// returns true if current stack is a spellcaster
-	bool isActiveStackSpellcaster() const;
-
-public:
-	BattleActionsController(BattleInterface & owner);
-
-	/// initialize list of potential actions for new active stack
-	void activateStack();
-
-	/// returns true if UI is currently in target selection mode
-	bool spellcastingModeActive() const;
-
-	/// returns true if one of the following is true:
-	/// - we are casting spell by hero
-	/// - we are casting spell by creature in targeted mode (F hotkey)
-	/// - current creature is spellcaster and preferred action for current hex is spellcast
-	bool currentActionSpellcasting(BattleHex hoveredHex);
-
-	/// enter targeted spellcasting mode for creature, e.g. via "F" hotkey
-	void enterCreatureCastingMode();
-
-	/// initialize hero spellcasting mode, e.g. on selecting spell in spellbook
-	void castThisSpell(SpellID spellID);
-
-	/// ends casting spell (eg. when spell has been cast or canceled)
-	void endCastingSpell();
-
-	/// update cursor and status bar according to new active hex
-	void onHexHovered(BattleHex hoveredHex);
-
-	/// called when cursor is no longer over battlefield and cursor/battle log should be reset
-	void onHoverEnded();
-
-	/// performs action according to selected hex
-	void onHexLeftClicked(BattleHex clickedHex);
-
-	/// performs action according to selected hex
-	void onHexRightClicked(BattleHex clickedHex);
-
-	const spells::Caster * getCurrentSpellcaster() const;
-	const CSpell * getCurrentSpell(BattleHex hoveredHex);
-	spells::Mode getCurrentCastMode() const;
-
-	/// methods to work with array of possible actions, needed to control special creatures abilities
-	const std::vector<PossiblePlayerBattleAction> & getPossibleActions() const;
-	void removePossibleAction(PossiblePlayerBattleAction);
-	
-	/// inserts possible action in the beggining in order to prioritize it
-	void pushFrontPossibleAction(PossiblePlayerBattleAction);
-};
+/*
+ * BattleActionsController.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/battle/CBattleInfoCallback.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class BattleAction;
+namespace spells {
+class Caster;
+enum class Mode;
+}
+
+VCMI_LIB_NAMESPACE_END
+
+class BattleInterface;
+
+/// Class that controls actions that can be performed by player, e.g. moving stacks, attacking, etc
+/// As well as all relevant feedback for these actions in user interface
+class BattleActionsController
+{
+	BattleInterface & owner;
+	
+	/// all actions possible to call at the moment by player
+	std::vector<PossiblePlayerBattleAction> possibleActions;
+
+	/// spell for which player's hero is choosing destination
+	std::shared_ptr<BattleAction> heroSpellToCast;
+
+	/// cached message that was set by this class in status bar
+	std::string currentConsoleMsg;
+
+	/// if true, active stack could possibly cast some target spell
+	std::vector<const CSpell *> creatureSpells;
+
+	/// stack that has been selected as first target for multi-target spells (Teleport & Sacrifice)
+	const CStack * selectedStack;
+
+	bool isCastingPossibleHere (const CSpell * spell, const CStack *shere, BattleHex myNumber);
+	bool canStackMoveHere (const CStack *sactive, BattleHex MyNumber) const; //TODO: move to BattleState / callback
+	std::vector<PossiblePlayerBattleAction> getPossibleActionsForStack (const CStack *stack) const; //called when stack gets its turn
+	void reorderPossibleActionsPriority(const CStack * stack, const CStack * targetStack);
+
+	bool actionIsLegal(PossiblePlayerBattleAction action, BattleHex hoveredHex);
+
+	void actionSetCursor(PossiblePlayerBattleAction action, BattleHex hoveredHex);
+	void actionSetCursorBlocked(PossiblePlayerBattleAction action, BattleHex hoveredHex);
+
+	std::string actionGetStatusMessage(PossiblePlayerBattleAction action, BattleHex hoveredHex);
+	std::string actionGetStatusMessageBlocked(PossiblePlayerBattleAction action, BattleHex hoveredHex);
+
+	void actionRealize(PossiblePlayerBattleAction action, BattleHex hoveredHex);
+
+	PossiblePlayerBattleAction selectAction(BattleHex myNumber);
+
+	const CStack * getStackForHex(BattleHex myNumber) ;
+
+	/// attempts to initialize spellcasting action for stack
+	/// will silently return if stack is not a spellcaster
+	void tryActivateStackSpellcasting(const CStack *casterStack);
+
+	/// returns spell that is currently being cast by hero or nullptr if none
+	const CSpell * getHeroSpellToCast() const;
+
+	/// if current stack is spellcaster, returns spell being cast, or null othervice
+	const CSpell * getStackSpellToCast(BattleHex hoveredHex);
+
+	/// returns true if current stack is a spellcaster
+	bool isActiveStackSpellcaster() const;
+
+public:
+	BattleActionsController(BattleInterface & owner);
+
+	/// initialize list of potential actions for new active stack
+	void activateStack();
+
+	/// returns true if UI is currently in target selection mode
+	bool spellcastingModeActive() const;
+
+	/// returns true if one of the following is true:
+	/// - we are casting spell by hero
+	/// - we are casting spell by creature in targeted mode (F hotkey)
+	/// - current creature is spellcaster and preferred action for current hex is spellcast
+	bool currentActionSpellcasting(BattleHex hoveredHex);
+
+	/// enter targeted spellcasting mode for creature, e.g. via "F" hotkey
+	void enterCreatureCastingMode();
+
+	/// initialize hero spellcasting mode, e.g. on selecting spell in spellbook
+	void castThisSpell(SpellID spellID);
+
+	/// ends casting spell (eg. when spell has been cast or canceled)
+	void endCastingSpell();
+
+	/// update cursor and status bar according to new active hex
+	void onHexHovered(BattleHex hoveredHex);
+
+	/// called when cursor is no longer over battlefield and cursor/battle log should be reset
+	void onHoverEnded();
+
+	/// performs action according to selected hex
+	void onHexLeftClicked(BattleHex clickedHex);
+
+	/// performs action according to selected hex
+	void onHexRightClicked(BattleHex clickedHex);
+
+	const spells::Caster * getCurrentSpellcaster() const;
+	const CSpell * getCurrentSpell(BattleHex hoveredHex);
+	spells::Mode getCurrentCastMode() const;
+
+	/// methods to work with array of possible actions, needed to control special creatures abilities
+	const std::vector<PossiblePlayerBattleAction> & getPossibleActions() const;
+	void removePossibleAction(PossiblePlayerBattleAction);
+	
+	/// inserts possible action in the beggining in order to prioritize it
+	void pushFrontPossibleAction(PossiblePlayerBattleAction);
+};

+ 1137 - 1137
client/battle/BattleAnimationClasses.cpp

@@ -1,1137 +1,1137 @@
-/*
- * BattleAnimationClasses.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 "BattleAnimationClasses.h"
-
-#include "BattleInterface.h"
-#include "BattleInterfaceClasses.h"
-#include "BattleProjectileController.h"
-#include "BattleSiegeController.h"
-#include "BattleFieldController.h"
-#include "BattleEffectsController.h"
-#include "BattleStacksController.h"
-#include "CreatureAnimation.h"
-
-#include "../CGameInfo.h"
-#include "../CMusicHandler.h"
-#include "../CPlayerInterface.h"
-#include "../gui/CursorHandler.h"
-#include "../gui/CGuiHandler.h"
-#include "../render/IRenderHandler.h"
-
-#include "../../CCallback.h"
-#include "../../lib/CStack.h"
-
-BattleAnimation::BattleAnimation(BattleInterface & owner)
-	: owner(owner),
-	  ID(owner.stacksController->animIDhelper++),
-	  initialized(false)
-{
-	logAnim->trace("Animation #%d created", ID);
-}
-
-bool BattleAnimation::tryInitialize()
-{
-	assert(!initialized);
-
-	if ( init() )
-	{
-		initialized = true;
-		return true;
-	}
-	return false;
-}
-
-bool BattleAnimation::isInitialized()
-{
-	return initialized;
-}
-
-BattleAnimation::~BattleAnimation()
-{
-	logAnim->trace("Animation #%d ended, type is %s", ID, typeid(this).name());
-	for(auto & elem : pendingAnimations())
-	{
-		if(elem == this)
-			elem = nullptr;
-	}
-	logAnim->trace("Animation #%d deleted", ID);
-}
-
-std::vector<BattleAnimation *> & BattleAnimation::pendingAnimations()
-{
-	return owner.stacksController->currentAnimations;
-}
-
-std::shared_ptr<CreatureAnimation> BattleAnimation::stackAnimation(const CStack * stack) const
-{
-	return owner.stacksController->stackAnimation[stack->unitId()];
-}
-
-bool BattleAnimation::stackFacingRight(const CStack * stack)
-{
-	return owner.stacksController->stackFacingRight[stack->unitId()];
-}
-
-void BattleAnimation::setStackFacingRight(const CStack * stack, bool facingRight)
-{
-	owner.stacksController->stackFacingRight[stack->unitId()] = facingRight;
-}
-
-BattleStackAnimation::BattleStackAnimation(BattleInterface & owner, const CStack * stack)
-	: BattleAnimation(owner),
-	  myAnim(stackAnimation(stack)),
-	  stack(stack)
-{
-	assert(myAnim);
-}
-
-StackActionAnimation::StackActionAnimation(BattleInterface & owner, const CStack * stack)
-	: BattleStackAnimation(owner, stack)
-	, nextGroup(ECreatureAnimType::HOLDING)
-	, currGroup(ECreatureAnimType::HOLDING)
-{
-}
-
-ECreatureAnimType StackActionAnimation::getGroup() const
-{
-	return currGroup;
-}
-
-void StackActionAnimation::setNextGroup( ECreatureAnimType group )
-{
-	nextGroup = group;
-}
-
-void StackActionAnimation::setGroup( ECreatureAnimType group )
-{
-	currGroup = group;
-}
-
-void StackActionAnimation::setSound( const AudioPath & sound )
-{
-	this->sound = sound;
-}
-
-bool StackActionAnimation::init()
-{
-	if (!sound.empty())
-		CCS->soundh->playSound(sound);
-
-	if (myAnim->framesInGroup(currGroup) > 0)
-	{
-		myAnim->playOnce(currGroup);
-		myAnim->onAnimationReset += [&](){ delete this; };
-	}
-	else
-		delete this;
-
-	return true;
-}
-
-StackActionAnimation::~StackActionAnimation()
-{
-	if (stack->isFrozen() && currGroup != ECreatureAnimType::DEATH && currGroup != ECreatureAnimType::DEATH_RANGED)
-		myAnim->setType(ECreatureAnimType::HOLDING);
-	else
-		myAnim->setType(nextGroup);
-
-}
-
-ECreatureAnimType AttackAnimation::findValidGroup( const std::vector<ECreatureAnimType> candidates ) const
-{
-	for ( auto group : candidates)
-	{
-		if(myAnim->framesInGroup(group) > 0)
-			return group;
-	}
-
-	assert(0);
-	return ECreatureAnimType::HOLDING;
-}
-
-const CCreature * AttackAnimation::getCreature() const
-{
-	if (attackingStack->unitType()->getId() == CreatureID::ARROW_TOWERS)
-		return owner.siegeController->getTurretCreature();
-	else
-		return attackingStack->unitType();
-}
-
-
-AttackAnimation::AttackAnimation(BattleInterface & owner, const CStack *attacker, BattleHex _dest, const CStack *defender)
-	: StackActionAnimation(owner, attacker),
-	  dest(_dest),
-	  defendingStack(defender),
-	  attackingStack(attacker)
-{
-	assert(attackingStack && "attackingStack is nullptr in CBattleAttack::CBattleAttack !\n");
-	attackingStackPosBeforeReturn = attackingStack->getPosition();
-}
-
-HittedAnimation::HittedAnimation(BattleInterface & owner, const CStack * stack)
-	: StackActionAnimation(owner, stack)
-{
-	setGroup(ECreatureAnimType::HITTED);
-	setSound(stack->unitType()->sounds.wince);
-	logAnim->debug("Created HittedAnimation for %s", stack->getName());
-}
-
-DefenceAnimation::DefenceAnimation(BattleInterface & owner, const CStack * stack)
-	: StackActionAnimation(owner, stack)
-{
-	setGroup(ECreatureAnimType::DEFENCE);
-	setSound(stack->unitType()->sounds.defend);
-	logAnim->debug("Created DefenceAnimation for %s", stack->getName());
-}
-
-DeathAnimation::DeathAnimation(BattleInterface & owner, const CStack * stack, bool ranged):
-	StackActionAnimation(owner, stack)
-{
-	setSound(stack->unitType()->sounds.killed);
-
-	if(ranged && myAnim->framesInGroup(ECreatureAnimType::DEATH_RANGED) > 0)
-		setGroup(ECreatureAnimType::DEATH_RANGED);
-	else
-		setGroup(ECreatureAnimType::DEATH);
-
-	if(ranged && myAnim->framesInGroup(ECreatureAnimType::DEAD_RANGED) > 0)
-		setNextGroup(ECreatureAnimType::DEAD_RANGED);
-	else
-		setNextGroup(ECreatureAnimType::DEAD);
-
-	logAnim->debug("Created DeathAnimation for %s", stack->getName());
-}
-
-DummyAnimation::DummyAnimation(BattleInterface & owner, int howManyFrames)
-	: BattleAnimation(owner),
-	  counter(0),
-	  howMany(howManyFrames)
-{
-	logAnim->debug("Created dummy animation for %d frames", howManyFrames);
-}
-
-bool DummyAnimation::init()
-{
-	return true;
-}
-
-void DummyAnimation::tick(uint32_t msPassed)
-{
-	counter++;
-	if(counter > howMany)
-		delete this;
-}
-
-ECreatureAnimType MeleeAttackAnimation::getUpwardsGroup(bool multiAttack) const
-{
-	if (!multiAttack)
-		return ECreatureAnimType::ATTACK_UP;
-
-	return findValidGroup({
-		ECreatureAnimType::GROUP_ATTACK_UP,
-		ECreatureAnimType::SPECIAL_UP,
-		ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3
-		ECreatureAnimType::ATTACK_UP
-	});
-}
-
-ECreatureAnimType MeleeAttackAnimation::getForwardGroup(bool multiAttack) const
-{
-	if (!multiAttack)
-		return ECreatureAnimType::ATTACK_FRONT;
-
-	return findValidGroup({
-		ECreatureAnimType::GROUP_ATTACK_FRONT,
-		ECreatureAnimType::SPECIAL_FRONT,
-		ECreatureAnimType::ATTACK_FRONT
-	});
-}
-
-ECreatureAnimType MeleeAttackAnimation::getDownwardsGroup(bool multiAttack) const
-{
-	if (!multiAttack)
-		return ECreatureAnimType::ATTACK_DOWN;
-
-	return findValidGroup({
-		ECreatureAnimType::GROUP_ATTACK_DOWN,
-		ECreatureAnimType::SPECIAL_DOWN,
-		ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3
-		ECreatureAnimType::ATTACK_DOWN
-	});
-}
-
-ECreatureAnimType MeleeAttackAnimation::selectGroup(bool multiAttack)
-{
-	const ECreatureAnimType mutPosToGroup[] =
-	{
-		getUpwardsGroup  (multiAttack),
-		getUpwardsGroup  (multiAttack),
-		getForwardGroup  (multiAttack),
-		getDownwardsGroup(multiAttack),
-		getDownwardsGroup(multiAttack),
-		getForwardGroup  (multiAttack)
-	};
-
-	int revShiftattacker = (attackingStack->unitSide() == BattleSide::ATTACKER ? -1 : 1);
-
-	int mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, dest);
-	if(mutPos == -1 && attackingStack->doubleWide())
-	{
-		mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, defendingStack->getPosition());
-	}
-	if (mutPos == -1 && defendingStack->doubleWide())
-	{
-		mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, defendingStack->occupiedHex());
-	}
-	if (mutPos == -1 && defendingStack->doubleWide() && attackingStack->doubleWide())
-	{
-		mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, defendingStack->occupiedHex());
-	}
-
-	assert(mutPos >= 0 && mutPos <=5);
-
-	return mutPosToGroup[mutPos];
-}
-
-void MeleeAttackAnimation::tick(uint32_t msPassed)
-{
-	size_t currentFrame = stackAnimation(attackingStack)->getCurrentFrame();
-	size_t totalFrames = stackAnimation(attackingStack)->framesInGroup(getGroup());
-
-	if ( currentFrame * 2 >= totalFrames )
-		owner.executeAnimationStage(EAnimationEvents::HIT);
-
-	AttackAnimation::tick(msPassed);
-}
-
-MeleeAttackAnimation::MeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool multiAttack)
-	: AttackAnimation(owner, attacker, _dest, _attacked)
-{
-	logAnim->debug("Created MeleeAttackAnimation for %s", attacker->getName());
-	setSound(getCreature()->sounds.attack);
-	setGroup(selectGroup(multiAttack));
-}
-
-StackMoveAnimation::StackMoveAnimation(BattleInterface & owner, const CStack * _stack, BattleHex prevHex, BattleHex nextHex):
-	BattleStackAnimation(owner, _stack),
-	prevHex(prevHex),
-	nextHex(nextHex)
-{
-}
-
-bool MovementAnimation::init()
-{
-	assert(stack);
-	assert(!myAnim->isDeadOrDying());
-	assert(stackAnimation(stack)->framesInGroup(ECreatureAnimType::MOVING) > 0);
-
-	if(stackAnimation(stack)->framesInGroup(ECreatureAnimType::MOVING) == 0)
-	{
-		//no movement, end immediately
-		delete this;
-		return false;
-	}
-
-	logAnim->debug("CMovementAnimation::init: stack %s moves %d -> %d", stack->getName(), prevHex, nextHex);
-
-	//reverse unit if necessary
-	if(owner.stacksController->shouldRotate(stack, prevHex, nextHex))
-	{
-		// it seems that H3 does NOT plays full rotation animation during movement
-		// Logical since it takes quite a lot of time
-		rotateStack(prevHex);
-	}
-
-	if(myAnim->getType() != ECreatureAnimType::MOVING)
-	{
-		myAnim->setType(ECreatureAnimType::MOVING);
-	}
-
-	if (moveSoundHander == -1)
-	{
-		moveSoundHander = CCS->soundh->playSound(stack->unitType()->sounds.move, -1);
-	}
-
-	Point begPosition = owner.stacksController->getStackPositionAtHex(prevHex, stack);
-	Point endPosition = owner.stacksController->getStackPositionAtHex(nextHex, stack);
-
-	progressPerSecond = AnimationControls::getMovementDistance(stack->unitType());
-
-	begX = begPosition.x;
-	begY = begPosition.y;
-	//progress = 0;
-	distanceX = endPosition.x - begPosition.x;
-	distanceY = endPosition.y - begPosition.y;
-
-	if (stack->hasBonus(Selector::type()(BonusType::FLYING)))
-	{
-		float distance = static_cast<float>(sqrt(distanceX * distanceX + distanceY * distanceY));
-		progressPerSecond =  AnimationControls::getFlightDistance(stack->unitType()) / distance;
-	}
-
-	return true;
-}
-
-void MovementAnimation::tick(uint32_t msPassed)
-{
-	progress += float(msPassed) / 1000 * progressPerSecond;
-
-	//moving instructions
-	myAnim->pos.x = begX + distanceX * progress;
-	myAnim->pos.y = begY + distanceY * progress;
-
-	BattleAnimation::tick(msPassed);
-
-	if(progress >= 1.0)
-	{
-		progress -= 1.0;
-		// Sets the position of the creature animation sprites
-		Point coords = owner.stacksController->getStackPositionAtHex(nextHex, stack);
-		myAnim->pos.moveTo(coords);
-
-		// true if creature haven't reached the final destination hex
-		if ((curentMoveIndex + 1) < destTiles.size())
-		{
-			// update the next hex field which has to be reached by the stack
-			curentMoveIndex++;
-			prevHex = nextHex;
-			nextHex = destTiles[curentMoveIndex];
-
-			// request re-initialization
-			initialized = false;
-		}
-		else
-			delete this;
-	}
-}
-
-MovementAnimation::~MovementAnimation()
-{
-	assert(stack);
-
-	if(moveSoundHander != -1)
-		CCS->soundh->stopSound(moveSoundHander);
-}
-
-MovementAnimation::MovementAnimation(BattleInterface & owner, const CStack *stack, std::vector<BattleHex> _destTiles, int _distance)
-	: StackMoveAnimation(owner, stack, stack->getPosition(), _destTiles.front()),
-	  destTiles(_destTiles),
-	  curentMoveIndex(0),
-	  begX(0), begY(0),
-	  distanceX(0), distanceY(0),
-	  progressPerSecond(0.0),
-	  moveSoundHander(-1),
-	  progress(0.0)
-{
-	logAnim->debug("Created MovementAnimation for %s", stack->getName());
-}
-
-MovementEndAnimation::MovementEndAnimation(BattleInterface & owner, const CStack * _stack, BattleHex destTile)
-: StackMoveAnimation(owner, _stack, destTile, destTile)
-{
-	logAnim->debug("Created MovementEndAnimation for %s", stack->getName());
-}
-
-bool MovementEndAnimation::init()
-{
-	assert(stack);
-	assert(!myAnim->isDeadOrDying());
-
-	if(!stack || myAnim->isDeadOrDying())
-	{
-		delete this;
-		return false;
-	}
-
-	logAnim->debug("CMovementEndAnimation::init: stack %s", stack->getName());
-	myAnim->pos.moveTo(owner.stacksController->getStackPositionAtHex(nextHex, stack));
-
-	CCS->soundh->playSound(stack->unitType()->sounds.endMoving);
-
-	if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_END))
-	{
-		delete this;
-		return false;
-	}
-
-
-	myAnim->setType(ECreatureAnimType::MOVE_END);
-	myAnim->onAnimationReset += [&](){ delete this; };
-
-	return true;
-}
-
-MovementEndAnimation::~MovementEndAnimation()
-{
-	if(myAnim->getType() != ECreatureAnimType::DEAD)
-		myAnim->setType(ECreatureAnimType::HOLDING); //resetting to default
-
-	CCS->curh->show();
-}
-
-MovementStartAnimation::MovementStartAnimation(BattleInterface & owner, const CStack * _stack)
-	: StackMoveAnimation(owner, _stack, _stack->getPosition(), _stack->getPosition())
-{
-	logAnim->debug("Created MovementStartAnimation for %s", stack->getName());
-}
-
-bool MovementStartAnimation::init()
-{
-	assert(stack);
-	assert(!myAnim->isDeadOrDying());
-
-	if(!stack || myAnim->isDeadOrDying())
-	{
-		delete this;
-		return false;
-	}
-
-	logAnim->debug("CMovementStartAnimation::init: stack %s", stack->getName());
-	CCS->soundh->playSound(stack->unitType()->sounds.startMoving);
-
-	if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_START))
-	{
-		delete this;
-		return false;
-	}
-
-	myAnim->setType(ECreatureAnimType::MOVE_START);
-	myAnim->onAnimationReset += [&](){ delete this; };
-	return true;
-}
-
-ReverseAnimation::ReverseAnimation(BattleInterface & owner, const CStack * stack, BattleHex dest)
-	: StackMoveAnimation(owner, stack, dest, dest)
-{
-	logAnim->debug("Created ReverseAnimation for %s", stack->getName());
-}
-
-bool ReverseAnimation::init()
-{
-	assert(myAnim);
-	assert(!myAnim->isDeadOrDying());
-
-	if(myAnim == nullptr || myAnim->isDeadOrDying())
-	{
-		delete this;
-		return false; //there is no such creature
-	}
-
-	logAnim->debug("CReverseAnimation::init: stack %s", stack->getName());
-	if(myAnim->framesInGroup(ECreatureAnimType::TURN_L))
-	{
-		myAnim->playOnce(ECreatureAnimType::TURN_L);
-		myAnim->onAnimationReset += std::bind(&ReverseAnimation::setupSecondPart, this);
-	}
-	else
-	{
-		setupSecondPart();
-	}
-	return true;
-}
-
-void BattleStackAnimation::rotateStack(BattleHex hex)
-{
-	setStackFacingRight(stack, !stackFacingRight(stack));
-
-	stackAnimation(stack)->pos.moveTo(owner.stacksController->getStackPositionAtHex(hex, stack));
-}
-
-void ReverseAnimation::setupSecondPart()
-{
-	assert(stack);
-
-	if(!stack)
-	{
-		delete this;
-		return;
-	}
-
-	rotateStack(nextHex);
-
-	if(myAnim->framesInGroup(ECreatureAnimType::TURN_R))
-	{
-		myAnim->playOnce(ECreatureAnimType::TURN_R);
-		myAnim->onAnimationReset += [&](){ delete this; };
-	}
-	else
-		delete this;
-}
-
-ResurrectionAnimation::ResurrectionAnimation(BattleInterface & owner, const CStack * _stack):
-	StackActionAnimation(owner, _stack)
-{
-	setGroup(ECreatureAnimType::RESURRECTION);
-	logAnim->debug("Created ResurrectionAnimation for %s", stack->getName());
-}
-
-bool ColorTransformAnimation::init()
-{
-	return true;
-}
-
-void ColorTransformAnimation::tick(uint32_t msPassed)
-{
-	float elapsed  = msPassed / 1000.f;
-	float fullTime = AnimationControls::getFadeInDuration();
-	float delta    = elapsed / fullTime;
-	totalProgress += delta;
-
-	size_t index = 0;
-
-	while (index < timePoints.size() && timePoints[index] < totalProgress )
-		++index;
-
-	if (index == timePoints.size())
-	{
-		//end of animation. Apply ColorShifter using final values and die
-		const auto & shifter = steps[index - 1];
-		owner.stacksController->setStackColorFilter(shifter, stack, spell, false);
-		delete this;
-		return;
-	}
-
-	assert(index != 0);
-
-	const auto & prevShifter = steps[index - 1];
-	const auto & nextShifter = steps[index];
-
-	float prevPoint = timePoints[index-1];
-	float nextPoint = timePoints[index];
-	float localProgress = totalProgress - prevPoint;
-	float stepDuration = (nextPoint - prevPoint);
-	float factor = localProgress / stepDuration;
-
-	auto shifter = ColorFilter::genInterpolated(prevShifter, nextShifter, factor);
-
-	owner.stacksController->setStackColorFilter(shifter, stack, spell, true);
-}
-
-ColorTransformAnimation::ColorTransformAnimation(BattleInterface & owner, const CStack * _stack, const std::string & colorFilterName, const CSpell * spell):
-	BattleStackAnimation(owner, _stack),
-	spell(spell),
-	totalProgress(0.f)
-{
-	auto effect = owner.effectsController->getMuxerEffect(colorFilterName);
-	steps = effect.filters;
-	timePoints = effect.timePoints;
-
-	assert(!steps.empty() && steps.size() == timePoints.size());
-
-	logAnim->debug("Created ColorTransformAnimation for %s", stack->getName());
-}
-
-RangedAttackAnimation::RangedAttackAnimation(BattleInterface & owner_, const CStack * attacker, BattleHex dest_, const CStack * defender)
-	: AttackAnimation(owner_, attacker, dest_, defender),
-	  projectileEmitted(false)
-{
-	setSound(getCreature()->sounds.shoot);
-}
-
-bool RangedAttackAnimation::init()
-{
-	setAnimationGroup();
-	initializeProjectile();
-
-	return AttackAnimation::init();
-}
-
-void RangedAttackAnimation::setAnimationGroup()
-{
-	Point shooterPos = stackAnimation(attackingStack)->pos.topLeft();
-	Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack);
-
-	//maximal angle in radians between straight horizontal line and shooting line for which shot is considered to be straight (absoulte value)
-	static const double straightAngle = 0.2;
-
-	double projectileAngle = -atan2(shotTarget.y - shooterPos.y, std::abs(shotTarget.x - shooterPos.x));
-
-	// Calculate projectile start position. Offsets are read out of the CRANIM.TXT.
-	if (projectileAngle > straightAngle)
-		setGroup(getUpwardsGroup());
-	else if (projectileAngle < -straightAngle)
-		setGroup(getDownwardsGroup());
-	else
-		setGroup(getForwardGroup());
-}
-
-void RangedAttackAnimation::initializeProjectile()
-{
-	const CCreature *shooterInfo = getCreature();
-	Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack) + Point(225, 225);
-	Point shotOrigin = stackAnimation(attackingStack)->pos.topLeft() + Point(222, 265);
-	int multiplier = stackFacingRight(attackingStack) ? 1 : -1;
-
-	if (getGroup() == getUpwardsGroup())
-	{
-		shotOrigin.x += ( -25 + shooterInfo->animation.upperRightMissleOffsetX ) * multiplier;
-		shotOrigin.y += shooterInfo->animation.upperRightMissleOffsetY;
-	}
-	else if (getGroup() == getDownwardsGroup())
-	{
-		shotOrigin.x += ( -25 + shooterInfo->animation.lowerRightMissleOffsetX ) * multiplier;
-		shotOrigin.y += shooterInfo->animation.lowerRightMissleOffsetY;
-	}
-	else if (getGroup() == getForwardGroup())
-	{
-		shotOrigin.x += ( -25 + shooterInfo->animation.rightMissleOffsetX ) * multiplier;
-		shotOrigin.y += shooterInfo->animation.rightMissleOffsetY;
-	}
-	else
-	{
-		assert(0);
-	}
-
-	createProjectile(shotOrigin, shotTarget);
-}
-
-void RangedAttackAnimation::emitProjectile()
-{
-	logAnim->debug("Ranged attack projectile emitted");
-	owner.projectilesController->emitStackProjectile(attackingStack);
-	projectileEmitted = true;
-}
-
-void RangedAttackAnimation::tick(uint32_t msPassed)
-{
-	// animation should be paused if there is an active projectile
-	if (projectileEmitted)
-	{
-		if (!owner.projectilesController->hasActiveProjectile(attackingStack, false))
-			owner.executeAnimationStage(EAnimationEvents::HIT);
-
-	}
-
-	bool stackHasProjectile = owner.projectilesController->hasActiveProjectile(stack, true);
-
-	if (!projectileEmitted || stackHasProjectile)
-		stackAnimation(attackingStack)->playUntil(getAttackClimaxFrame());
-	else
-		stackAnimation(attackingStack)->playUntil(static_cast<size_t>(-1));
-
-	AttackAnimation::tick(msPassed);
-
-	if (!projectileEmitted)
-	{
-		// emit projectile once animation playback reached "climax" frame
-		if ( stackAnimation(attackingStack)->getCurrentFrame() >= getAttackClimaxFrame() )
-		{
-			emitProjectile();
-			return;
-		}
-	}
-}
-
-RangedAttackAnimation::~RangedAttackAnimation()
-{
-	assert(!owner.projectilesController->hasActiveProjectile(attackingStack, false));
-	assert(projectileEmitted);
-
-	// FIXME: is this possible? Animation is over but we're yet to fire projectile?
-	if (!projectileEmitted)
-	{
-		logAnim->warn("Shooting animation has finished but projectile was not emitted! Emitting it now...");
-		emitProjectile();
-	}
-}
-
-ShootingAnimation::ShootingAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked)
-	: RangedAttackAnimation(owner, attacker, _dest, _attacked)
-{
-	logAnim->debug("Created ShootingAnimation for %s", stack->getName());
-}
-
-void ShootingAnimation::createProjectile(const Point & from, const Point & dest) const
-{
-	owner.projectilesController->createProjectile(attackingStack, from, dest);
-}
-
-uint32_t ShootingAnimation::getAttackClimaxFrame() const
-{
-	const CCreature *shooterInfo = getCreature();
-
-	uint32_t maxFrames = stackAnimation(attackingStack)->framesInGroup(getGroup());
-	uint32_t climaxFrame = shooterInfo->animation.attackClimaxFrame;
-	uint32_t selectedFrame = std::clamp<int>(shooterInfo->animation.attackClimaxFrame, 1, maxFrames);
-
-	if (climaxFrame != selectedFrame)
-		logGlobal->warn("Shooter %s has ranged attack climax frame set to %d, but only %d available!", shooterInfo->getNamePluralTranslated(), climaxFrame, maxFrames);
-
-	return selectedFrame - 1; // H3 counts frames from 1
-}
-
-ECreatureAnimType ShootingAnimation::getUpwardsGroup() const
-{
-	return ECreatureAnimType::SHOOT_UP;
-}
-
-ECreatureAnimType ShootingAnimation::getForwardGroup() const
-{
-	return ECreatureAnimType::SHOOT_FRONT;
-}
-
-ECreatureAnimType ShootingAnimation::getDownwardsGroup() const
-{
-	return ECreatureAnimType::SHOOT_DOWN;
-}
-
-CatapultAnimation::CatapultAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, int _catapultDmg)
-	: ShootingAnimation(owner, attacker, _dest, _attacked),
-	catapultDamage(_catapultDmg),
-	explosionEmitted(false)
-{
-	logAnim->debug("Created shooting anim for %s", stack->getName());
-}
-
-void CatapultAnimation::tick(uint32_t msPassed)
-{
-	ShootingAnimation::tick(msPassed);
-
-	if ( explosionEmitted)
-		return;
-
-	if ( !projectileEmitted)
-		return;
-
-	if (owner.projectilesController->hasActiveProjectile(attackingStack, false))
-		return;
-
-	explosionEmitted = true;
-	Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack) + Point(225, 225) - Point(126, 105);
-
-	auto soundFilename  = AudioPath::builtin((catapultDamage > 0) ? "WALLHIT" : "WALLMISS");
-	AnimationPath effectFilename = AnimationPath::builtin((catapultDamage > 0) ? "SGEXPL" : "CSGRCK");
-
-	CCS->soundh->playSound( soundFilename );
-	owner.stacksController->addNewAnim( new EffectAnimation(owner, effectFilename, shotTarget));
-}
-
-void CatapultAnimation::createProjectile(const Point & from, const Point & dest) const
-{
-	owner.projectilesController->createCatapultProjectile(attackingStack, from, dest);
-}
-
-CastAnimation::CastAnimation(BattleInterface & owner_, const CStack * attacker, BattleHex dest, const CStack * defender, const CSpell * spell)
-	: RangedAttackAnimation(owner_, attacker, dest, defender),
-	  spell(spell)
-{
-	if(!dest.isValid())
-	{
-		assert(spell->animationInfo.projectile.empty());
-
-		if (defender)
-			dest = defender->getPosition();
-		else
-			dest = attacker->getPosition();
-	}
-}
-
-ECreatureAnimType CastAnimation::getUpwardsGroup() const
-{
-	return findValidGroup({
-		ECreatureAnimType::CAST_UP,
-		ECreatureAnimType::SPECIAL_UP,
-		ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3
-		ECreatureAnimType::SHOOT_UP,
-		ECreatureAnimType::ATTACK_UP
-	});
-}
-
-ECreatureAnimType CastAnimation::getForwardGroup() const
-{
-	return findValidGroup({
-		ECreatureAnimType::CAST_FRONT,
-		ECreatureAnimType::SPECIAL_FRONT,
-		ECreatureAnimType::SHOOT_FRONT,
-		ECreatureAnimType::ATTACK_FRONT
-	});
-}
-
-ECreatureAnimType CastAnimation::getDownwardsGroup() const
-{
-	return findValidGroup({
-		ECreatureAnimType::CAST_DOWN,
-		ECreatureAnimType::SPECIAL_DOWN,
-		ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3
-		ECreatureAnimType::SHOOT_DOWN,
-		ECreatureAnimType::ATTACK_DOWN
-	});
-}
-
-void CastAnimation::createProjectile(const Point & from, const Point & dest) const
-{
-	if (!spell->animationInfo.projectile.empty())
-		owner.projectilesController->createSpellProjectile(attackingStack, from, dest, spell);
-}
-
-uint32_t CastAnimation::getAttackClimaxFrame() const
-{
-	//TODO: allow defining this parameter in config file, separately from attackClimaxFrame of missile attacks
-	uint32_t maxFrames = stackAnimation(attackingStack)->framesInGroup(getGroup());
-
-	return maxFrames / 2;
-}
-
-EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects, bool reversed):
-	BattleAnimation(owner),
-	animation(GH.renderHandler().loadAnimation(animationName)),
-	effectFlags(effects),
-	effectFinished(false),
-	reversed(reversed)
-{
-	logAnim->debug("CPointEffectAnimation::init: effect %s", animationName.getName());
-}
-
-EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector<BattleHex> hex, int effects, bool reversed):
-	EffectAnimation(owner, animationName, effects, reversed)
-{
-	battlehexes = hex;
-}
-
-EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex, int effects, bool reversed):
-	EffectAnimation(owner, animationName, effects, reversed)
-{
-	assert(hex.isValid());
-	battlehexes.push_back(hex);
-}
-
-EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector<Point> pos, int effects, bool reversed):
-	EffectAnimation(owner, animationName, effects, reversed)
-{
-	positions = pos;
-}
-
-EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, int effects, bool reversed):
-	EffectAnimation(owner, animationName, effects, reversed)
-{
-	positions.push_back(pos);
-}
-
-EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, BattleHex hex, int effects, bool reversed):
-	EffectAnimation(owner, animationName, effects, reversed)
-{
-	assert(hex.isValid());
-	battlehexes.push_back(hex);
-	positions.push_back(pos);
-}
-
-bool EffectAnimation::init()
-{
-	animation->preload();
-
-	auto first = animation->getImage(0, 0, true);
-	if(!first)
-	{
-		delete this;
-		return false;
-	}
-
-	for (size_t i = 0; i < animation->size(size_t(BattleEffect::AnimType::DEFAULT)); ++i)
-	{
-		size_t current = animation->size(size_t(BattleEffect::AnimType::DEFAULT)) - 1 - i;
-
-		animation->duplicateImage(size_t(BattleEffect::AnimType::DEFAULT), current, size_t(BattleEffect::AnimType::REVERSE));
-	}
-
-	if (screenFill())
-	{
-		for(int i=0; i * first->width() < owner.fieldController->pos.w ; ++i)
-			for(int j=0; j * first->height() < owner.fieldController->pos.h ; ++j)
-				positions.push_back(Point( i * first->width(), j * first->height()));
-	}
-
-	BattleEffect be;
-	be.effectID = ID;
-	be.animation = animation;
-	be.currentFrame = 0;
-	be.type = reversed ? BattleEffect::AnimType::REVERSE : BattleEffect::AnimType::DEFAULT;
-
-	for (size_t i = 0; i < std::max(battlehexes.size(), positions.size()); ++i)
-	{
-		bool hasTile = i < battlehexes.size();
-		bool hasPosition = i < positions.size();
-
-		if (hasTile && !forceOnTop())
-			be.tile = battlehexes[i];
-		else
-			be.tile = BattleHex::INVALID;
-
-		if (hasPosition)
-		{
-			be.pos.x = positions[i].x;
-			be.pos.y = positions[i].y;
-		}
-		else
-		{
-			const auto * destStack = owner.getBattle()->battleGetUnitByPos(battlehexes[i], false);
-			Rect tilePos = owner.fieldController->hexPositionLocal(battlehexes[i]);
-
-			be.pos.x = tilePos.x + tilePos.w/2 - first->width()/2;
-
-			if(destStack && destStack->doubleWide()) // Correction for 2-hex creatures.
-				be.pos.x += (destStack->unitSide() == BattleSide::ATTACKER ? -1 : 1)*tilePos.w/2;
-
-			if (alignToBottom())
-				be.pos.y = tilePos.y + tilePos.h - first->height();
-			else
-				be.pos.y = tilePos.y - first->height()/2;
-		}
-		owner.effectsController->battleEffects.push_back(be);
-	}
-	return true;
-}
-
-void EffectAnimation::tick(uint32_t msPassed)
-{
-	playEffect(msPassed);
-
-	if (effectFinished)
-	{
-		//remove visual effect itself only if sound has finished as well - necessary for obstacles like force field
-		clearEffect();
-		delete this;
-	}
-}
-
-bool EffectAnimation::alignToBottom() const
-{
-	return effectFlags & ALIGN_TO_BOTTOM;
-}
-
-bool EffectAnimation::forceOnTop() const
-{
-	return effectFlags & FORCE_ON_TOP;
-}
-
-bool EffectAnimation::screenFill() const
-{
-	return effectFlags & SCREEN_FILL;
-}
-
-void EffectAnimation::onEffectFinished()
-{
-	effectFinished = true;
-}
-
-void EffectAnimation::playEffect(uint32_t msPassed)
-{
-	if ( effectFinished )
-		return;
-
-	for(auto & elem : owner.effectsController->battleEffects)
-	{
-		if(elem.effectID == ID)
-		{
-			elem.currentFrame += AnimationControls::getSpellEffectSpeed() * msPassed / 1000;
-
-			if(elem.currentFrame >= elem.animation->size())
-			{
-				elem.currentFrame = elem.animation->size() - 1;
-				onEffectFinished();
-				break;
-			}
-		}
-	}
-}
-
-void EffectAnimation::clearEffect()
-{
-	auto & effects = owner.effectsController->battleEffects;
-
-	vstd::erase_if(effects, [&](const BattleEffect & effect){
-		return effect.effectID == ID;
-	});
-}
-
-EffectAnimation::~EffectAnimation()
-{
-	assert(effectFinished);
-}
-
-HeroCastAnimation::HeroCastAnimation(BattleInterface & owner, std::shared_ptr<BattleHero> hero, BattleHex dest, const CStack * defender, const CSpell * spell):
-	BattleAnimation(owner),
-	projectileEmitted(false),
-	hero(hero),
-	target(defender),
-	tile(dest),
-	spell(spell)
-{
-}
-
-bool HeroCastAnimation::init()
-{
-	hero->setPhase(EHeroAnimType::CAST_SPELL);
-
-	hero->onPhaseFinished([&](){
-		delete this;
-	});
-
-	initializeProjectile();
-
-	return true;
-}
-
-void HeroCastAnimation::initializeProjectile()
-{
-	// spell has no projectile to play, ignore this step
-	if (spell->animationInfo.projectile.empty())
-		return;
-
-	// targeted spells should have well, target
-	assert(tile.isValid());
-
-	Point srccoord = hero->pos.center() - hero->parent->pos.topLeft();
-	Point destcoord = owner.stacksController->getStackPositionAtHex(tile, target); //position attacked by projectile
-
-	destcoord += Point(222, 265); // FIXME: what are these constants?
-	owner.projectilesController->createSpellProjectile( nullptr, srccoord, destcoord, spell);
-}
-
-void HeroCastAnimation::emitProjectile()
-{
-	if (projectileEmitted)
-		return;
-
-	//spell has no projectile to play, skip this step and immediately play hit animations
-	if (spell->animationInfo.projectile.empty())
-		emitAnimationEvent();
-	else
-		owner.projectilesController->emitStackProjectile( nullptr );
-
-	projectileEmitted = true;
-}
-
-void HeroCastAnimation::emitAnimationEvent()
-{
-	owner.executeAnimationStage(EAnimationEvents::HIT);
-}
-
-void HeroCastAnimation::tick(uint32_t msPassed)
-{
-	float frame = hero->getFrame();
-
-	if (frame < 4.0f) // middle point of animation //TODO: un-hardcode
-		return;
-
-	if (!projectileEmitted)
-	{
-		emitProjectile();
-		hero->pause();
-		return;
-	}
-
-	if (!owner.projectilesController->hasActiveProjectile(nullptr, false))
-	{
-		emitAnimationEvent();
-		//TODO: check H3 - it is possible that hero animation should be paused until hit effect is over, not just projectile
-		hero->play();
-	}
-}
+/*
+ * BattleAnimationClasses.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 "BattleAnimationClasses.h"
+
+#include "BattleInterface.h"
+#include "BattleInterfaceClasses.h"
+#include "BattleProjectileController.h"
+#include "BattleSiegeController.h"
+#include "BattleFieldController.h"
+#include "BattleEffectsController.h"
+#include "BattleStacksController.h"
+#include "CreatureAnimation.h"
+
+#include "../CGameInfo.h"
+#include "../CMusicHandler.h"
+#include "../CPlayerInterface.h"
+#include "../gui/CursorHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../render/IRenderHandler.h"
+
+#include "../../CCallback.h"
+#include "../../lib/CStack.h"
+
+BattleAnimation::BattleAnimation(BattleInterface & owner)
+	: owner(owner),
+	  ID(owner.stacksController->animIDhelper++),
+	  initialized(false)
+{
+	logAnim->trace("Animation #%d created", ID);
+}
+
+bool BattleAnimation::tryInitialize()
+{
+	assert(!initialized);
+
+	if ( init() )
+	{
+		initialized = true;
+		return true;
+	}
+	return false;
+}
+
+bool BattleAnimation::isInitialized()
+{
+	return initialized;
+}
+
+BattleAnimation::~BattleAnimation()
+{
+	logAnim->trace("Animation #%d ended, type is %s", ID, typeid(this).name());
+	for(auto & elem : pendingAnimations())
+	{
+		if(elem == this)
+			elem = nullptr;
+	}
+	logAnim->trace("Animation #%d deleted", ID);
+}
+
+std::vector<BattleAnimation *> & BattleAnimation::pendingAnimations()
+{
+	return owner.stacksController->currentAnimations;
+}
+
+std::shared_ptr<CreatureAnimation> BattleAnimation::stackAnimation(const CStack * stack) const
+{
+	return owner.stacksController->stackAnimation[stack->unitId()];
+}
+
+bool BattleAnimation::stackFacingRight(const CStack * stack)
+{
+	return owner.stacksController->stackFacingRight[stack->unitId()];
+}
+
+void BattleAnimation::setStackFacingRight(const CStack * stack, bool facingRight)
+{
+	owner.stacksController->stackFacingRight[stack->unitId()] = facingRight;
+}
+
+BattleStackAnimation::BattleStackAnimation(BattleInterface & owner, const CStack * stack)
+	: BattleAnimation(owner),
+	  myAnim(stackAnimation(stack)),
+	  stack(stack)
+{
+	assert(myAnim);
+}
+
+StackActionAnimation::StackActionAnimation(BattleInterface & owner, const CStack * stack)
+	: BattleStackAnimation(owner, stack)
+	, nextGroup(ECreatureAnimType::HOLDING)
+	, currGroup(ECreatureAnimType::HOLDING)
+{
+}
+
+ECreatureAnimType StackActionAnimation::getGroup() const
+{
+	return currGroup;
+}
+
+void StackActionAnimation::setNextGroup( ECreatureAnimType group )
+{
+	nextGroup = group;
+}
+
+void StackActionAnimation::setGroup( ECreatureAnimType group )
+{
+	currGroup = group;
+}
+
+void StackActionAnimation::setSound( const AudioPath & sound )
+{
+	this->sound = sound;
+}
+
+bool StackActionAnimation::init()
+{
+	if (!sound.empty())
+		CCS->soundh->playSound(sound);
+
+	if (myAnim->framesInGroup(currGroup) > 0)
+	{
+		myAnim->playOnce(currGroup);
+		myAnim->onAnimationReset += [&](){ delete this; };
+	}
+	else
+		delete this;
+
+	return true;
+}
+
+StackActionAnimation::~StackActionAnimation()
+{
+	if (stack->isFrozen() && currGroup != ECreatureAnimType::DEATH && currGroup != ECreatureAnimType::DEATH_RANGED)
+		myAnim->setType(ECreatureAnimType::HOLDING);
+	else
+		myAnim->setType(nextGroup);
+
+}
+
+ECreatureAnimType AttackAnimation::findValidGroup( const std::vector<ECreatureAnimType> candidates ) const
+{
+	for ( auto group : candidates)
+	{
+		if(myAnim->framesInGroup(group) > 0)
+			return group;
+	}
+
+	assert(0);
+	return ECreatureAnimType::HOLDING;
+}
+
+const CCreature * AttackAnimation::getCreature() const
+{
+	if (attackingStack->unitType()->getId() == CreatureID::ARROW_TOWERS)
+		return owner.siegeController->getTurretCreature();
+	else
+		return attackingStack->unitType();
+}
+
+
+AttackAnimation::AttackAnimation(BattleInterface & owner, const CStack *attacker, BattleHex _dest, const CStack *defender)
+	: StackActionAnimation(owner, attacker),
+	  dest(_dest),
+	  defendingStack(defender),
+	  attackingStack(attacker)
+{
+	assert(attackingStack && "attackingStack is nullptr in CBattleAttack::CBattleAttack !\n");
+	attackingStackPosBeforeReturn = attackingStack->getPosition();
+}
+
+HittedAnimation::HittedAnimation(BattleInterface & owner, const CStack * stack)
+	: StackActionAnimation(owner, stack)
+{
+	setGroup(ECreatureAnimType::HITTED);
+	setSound(stack->unitType()->sounds.wince);
+	logAnim->debug("Created HittedAnimation for %s", stack->getName());
+}
+
+DefenceAnimation::DefenceAnimation(BattleInterface & owner, const CStack * stack)
+	: StackActionAnimation(owner, stack)
+{
+	setGroup(ECreatureAnimType::DEFENCE);
+	setSound(stack->unitType()->sounds.defend);
+	logAnim->debug("Created DefenceAnimation for %s", stack->getName());
+}
+
+DeathAnimation::DeathAnimation(BattleInterface & owner, const CStack * stack, bool ranged):
+	StackActionAnimation(owner, stack)
+{
+	setSound(stack->unitType()->sounds.killed);
+
+	if(ranged && myAnim->framesInGroup(ECreatureAnimType::DEATH_RANGED) > 0)
+		setGroup(ECreatureAnimType::DEATH_RANGED);
+	else
+		setGroup(ECreatureAnimType::DEATH);
+
+	if(ranged && myAnim->framesInGroup(ECreatureAnimType::DEAD_RANGED) > 0)
+		setNextGroup(ECreatureAnimType::DEAD_RANGED);
+	else
+		setNextGroup(ECreatureAnimType::DEAD);
+
+	logAnim->debug("Created DeathAnimation for %s", stack->getName());
+}
+
+DummyAnimation::DummyAnimation(BattleInterface & owner, int howManyFrames)
+	: BattleAnimation(owner),
+	  counter(0),
+	  howMany(howManyFrames)
+{
+	logAnim->debug("Created dummy animation for %d frames", howManyFrames);
+}
+
+bool DummyAnimation::init()
+{
+	return true;
+}
+
+void DummyAnimation::tick(uint32_t msPassed)
+{
+	counter++;
+	if(counter > howMany)
+		delete this;
+}
+
+ECreatureAnimType MeleeAttackAnimation::getUpwardsGroup(bool multiAttack) const
+{
+	if (!multiAttack)
+		return ECreatureAnimType::ATTACK_UP;
+
+	return findValidGroup({
+		ECreatureAnimType::GROUP_ATTACK_UP,
+		ECreatureAnimType::SPECIAL_UP,
+		ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3
+		ECreatureAnimType::ATTACK_UP
+	});
+}
+
+ECreatureAnimType MeleeAttackAnimation::getForwardGroup(bool multiAttack) const
+{
+	if (!multiAttack)
+		return ECreatureAnimType::ATTACK_FRONT;
+
+	return findValidGroup({
+		ECreatureAnimType::GROUP_ATTACK_FRONT,
+		ECreatureAnimType::SPECIAL_FRONT,
+		ECreatureAnimType::ATTACK_FRONT
+	});
+}
+
+ECreatureAnimType MeleeAttackAnimation::getDownwardsGroup(bool multiAttack) const
+{
+	if (!multiAttack)
+		return ECreatureAnimType::ATTACK_DOWN;
+
+	return findValidGroup({
+		ECreatureAnimType::GROUP_ATTACK_DOWN,
+		ECreatureAnimType::SPECIAL_DOWN,
+		ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3
+		ECreatureAnimType::ATTACK_DOWN
+	});
+}
+
+ECreatureAnimType MeleeAttackAnimation::selectGroup(bool multiAttack)
+{
+	const ECreatureAnimType mutPosToGroup[] =
+	{
+		getUpwardsGroup  (multiAttack),
+		getUpwardsGroup  (multiAttack),
+		getForwardGroup  (multiAttack),
+		getDownwardsGroup(multiAttack),
+		getDownwardsGroup(multiAttack),
+		getForwardGroup  (multiAttack)
+	};
+
+	int revShiftattacker = (attackingStack->unitSide() == BattleSide::ATTACKER ? -1 : 1);
+
+	int mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, dest);
+	if(mutPos == -1 && attackingStack->doubleWide())
+	{
+		mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, defendingStack->getPosition());
+	}
+	if (mutPos == -1 && defendingStack->doubleWide())
+	{
+		mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, defendingStack->occupiedHex());
+	}
+	if (mutPos == -1 && defendingStack->doubleWide() && attackingStack->doubleWide())
+	{
+		mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, defendingStack->occupiedHex());
+	}
+
+	assert(mutPos >= 0 && mutPos <=5);
+
+	return mutPosToGroup[mutPos];
+}
+
+void MeleeAttackAnimation::tick(uint32_t msPassed)
+{
+	size_t currentFrame = stackAnimation(attackingStack)->getCurrentFrame();
+	size_t totalFrames = stackAnimation(attackingStack)->framesInGroup(getGroup());
+
+	if ( currentFrame * 2 >= totalFrames )
+		owner.executeAnimationStage(EAnimationEvents::HIT);
+
+	AttackAnimation::tick(msPassed);
+}
+
+MeleeAttackAnimation::MeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool multiAttack)
+	: AttackAnimation(owner, attacker, _dest, _attacked)
+{
+	logAnim->debug("Created MeleeAttackAnimation for %s", attacker->getName());
+	setSound(getCreature()->sounds.attack);
+	setGroup(selectGroup(multiAttack));
+}
+
+StackMoveAnimation::StackMoveAnimation(BattleInterface & owner, const CStack * _stack, BattleHex prevHex, BattleHex nextHex):
+	BattleStackAnimation(owner, _stack),
+	prevHex(prevHex),
+	nextHex(nextHex)
+{
+}
+
+bool MovementAnimation::init()
+{
+	assert(stack);
+	assert(!myAnim->isDeadOrDying());
+	assert(stackAnimation(stack)->framesInGroup(ECreatureAnimType::MOVING) > 0);
+
+	if(stackAnimation(stack)->framesInGroup(ECreatureAnimType::MOVING) == 0)
+	{
+		//no movement, end immediately
+		delete this;
+		return false;
+	}
+
+	logAnim->debug("CMovementAnimation::init: stack %s moves %d -> %d", stack->getName(), prevHex, nextHex);
+
+	//reverse unit if necessary
+	if(owner.stacksController->shouldRotate(stack, prevHex, nextHex))
+	{
+		// it seems that H3 does NOT plays full rotation animation during movement
+		// Logical since it takes quite a lot of time
+		rotateStack(prevHex);
+	}
+
+	if(myAnim->getType() != ECreatureAnimType::MOVING)
+	{
+		myAnim->setType(ECreatureAnimType::MOVING);
+	}
+
+	if (moveSoundHander == -1)
+	{
+		moveSoundHander = CCS->soundh->playSound(stack->unitType()->sounds.move, -1);
+	}
+
+	Point begPosition = owner.stacksController->getStackPositionAtHex(prevHex, stack);
+	Point endPosition = owner.stacksController->getStackPositionAtHex(nextHex, stack);
+
+	progressPerSecond = AnimationControls::getMovementDistance(stack->unitType());
+
+	begX = begPosition.x;
+	begY = begPosition.y;
+	//progress = 0;
+	distanceX = endPosition.x - begPosition.x;
+	distanceY = endPosition.y - begPosition.y;
+
+	if (stack->hasBonus(Selector::type()(BonusType::FLYING)))
+	{
+		float distance = static_cast<float>(sqrt(distanceX * distanceX + distanceY * distanceY));
+		progressPerSecond =  AnimationControls::getFlightDistance(stack->unitType()) / distance;
+	}
+
+	return true;
+}
+
+void MovementAnimation::tick(uint32_t msPassed)
+{
+	progress += float(msPassed) / 1000 * progressPerSecond;
+
+	//moving instructions
+	myAnim->pos.x = begX + distanceX * progress;
+	myAnim->pos.y = begY + distanceY * progress;
+
+	BattleAnimation::tick(msPassed);
+
+	if(progress >= 1.0)
+	{
+		progress -= 1.0;
+		// Sets the position of the creature animation sprites
+		Point coords = owner.stacksController->getStackPositionAtHex(nextHex, stack);
+		myAnim->pos.moveTo(coords);
+
+		// true if creature haven't reached the final destination hex
+		if ((curentMoveIndex + 1) < destTiles.size())
+		{
+			// update the next hex field which has to be reached by the stack
+			curentMoveIndex++;
+			prevHex = nextHex;
+			nextHex = destTiles[curentMoveIndex];
+
+			// request re-initialization
+			initialized = false;
+		}
+		else
+			delete this;
+	}
+}
+
+MovementAnimation::~MovementAnimation()
+{
+	assert(stack);
+
+	if(moveSoundHander != -1)
+		CCS->soundh->stopSound(moveSoundHander);
+}
+
+MovementAnimation::MovementAnimation(BattleInterface & owner, const CStack *stack, std::vector<BattleHex> _destTiles, int _distance)
+	: StackMoveAnimation(owner, stack, stack->getPosition(), _destTiles.front()),
+	  destTiles(_destTiles),
+	  curentMoveIndex(0),
+	  begX(0), begY(0),
+	  distanceX(0), distanceY(0),
+	  progressPerSecond(0.0),
+	  moveSoundHander(-1),
+	  progress(0.0)
+{
+	logAnim->debug("Created MovementAnimation for %s", stack->getName());
+}
+
+MovementEndAnimation::MovementEndAnimation(BattleInterface & owner, const CStack * _stack, BattleHex destTile)
+: StackMoveAnimation(owner, _stack, destTile, destTile)
+{
+	logAnim->debug("Created MovementEndAnimation for %s", stack->getName());
+}
+
+bool MovementEndAnimation::init()
+{
+	assert(stack);
+	assert(!myAnim->isDeadOrDying());
+
+	if(!stack || myAnim->isDeadOrDying())
+	{
+		delete this;
+		return false;
+	}
+
+	logAnim->debug("CMovementEndAnimation::init: stack %s", stack->getName());
+	myAnim->pos.moveTo(owner.stacksController->getStackPositionAtHex(nextHex, stack));
+
+	CCS->soundh->playSound(stack->unitType()->sounds.endMoving);
+
+	if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_END))
+	{
+		delete this;
+		return false;
+	}
+
+
+	myAnim->setType(ECreatureAnimType::MOVE_END);
+	myAnim->onAnimationReset += [&](){ delete this; };
+
+	return true;
+}
+
+MovementEndAnimation::~MovementEndAnimation()
+{
+	if(myAnim->getType() != ECreatureAnimType::DEAD)
+		myAnim->setType(ECreatureAnimType::HOLDING); //resetting to default
+
+	CCS->curh->show();
+}
+
+MovementStartAnimation::MovementStartAnimation(BattleInterface & owner, const CStack * _stack)
+	: StackMoveAnimation(owner, _stack, _stack->getPosition(), _stack->getPosition())
+{
+	logAnim->debug("Created MovementStartAnimation for %s", stack->getName());
+}
+
+bool MovementStartAnimation::init()
+{
+	assert(stack);
+	assert(!myAnim->isDeadOrDying());
+
+	if(!stack || myAnim->isDeadOrDying())
+	{
+		delete this;
+		return false;
+	}
+
+	logAnim->debug("CMovementStartAnimation::init: stack %s", stack->getName());
+	CCS->soundh->playSound(stack->unitType()->sounds.startMoving);
+
+	if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_START))
+	{
+		delete this;
+		return false;
+	}
+
+	myAnim->setType(ECreatureAnimType::MOVE_START);
+	myAnim->onAnimationReset += [&](){ delete this; };
+	return true;
+}
+
+ReverseAnimation::ReverseAnimation(BattleInterface & owner, const CStack * stack, BattleHex dest)
+	: StackMoveAnimation(owner, stack, dest, dest)
+{
+	logAnim->debug("Created ReverseAnimation for %s", stack->getName());
+}
+
+bool ReverseAnimation::init()
+{
+	assert(myAnim);
+	assert(!myAnim->isDeadOrDying());
+
+	if(myAnim == nullptr || myAnim->isDeadOrDying())
+	{
+		delete this;
+		return false; //there is no such creature
+	}
+
+	logAnim->debug("CReverseAnimation::init: stack %s", stack->getName());
+	if(myAnim->framesInGroup(ECreatureAnimType::TURN_L))
+	{
+		myAnim->playOnce(ECreatureAnimType::TURN_L);
+		myAnim->onAnimationReset += std::bind(&ReverseAnimation::setupSecondPart, this);
+	}
+	else
+	{
+		setupSecondPart();
+	}
+	return true;
+}
+
+void BattleStackAnimation::rotateStack(BattleHex hex)
+{
+	setStackFacingRight(stack, !stackFacingRight(stack));
+
+	stackAnimation(stack)->pos.moveTo(owner.stacksController->getStackPositionAtHex(hex, stack));
+}
+
+void ReverseAnimation::setupSecondPart()
+{
+	assert(stack);
+
+	if(!stack)
+	{
+		delete this;
+		return;
+	}
+
+	rotateStack(nextHex);
+
+	if(myAnim->framesInGroup(ECreatureAnimType::TURN_R))
+	{
+		myAnim->playOnce(ECreatureAnimType::TURN_R);
+		myAnim->onAnimationReset += [&](){ delete this; };
+	}
+	else
+		delete this;
+}
+
+ResurrectionAnimation::ResurrectionAnimation(BattleInterface & owner, const CStack * _stack):
+	StackActionAnimation(owner, _stack)
+{
+	setGroup(ECreatureAnimType::RESURRECTION);
+	logAnim->debug("Created ResurrectionAnimation for %s", stack->getName());
+}
+
+bool ColorTransformAnimation::init()
+{
+	return true;
+}
+
+void ColorTransformAnimation::tick(uint32_t msPassed)
+{
+	float elapsed  = msPassed / 1000.f;
+	float fullTime = AnimationControls::getFadeInDuration();
+	float delta    = elapsed / fullTime;
+	totalProgress += delta;
+
+	size_t index = 0;
+
+	while (index < timePoints.size() && timePoints[index] < totalProgress )
+		++index;
+
+	if (index == timePoints.size())
+	{
+		//end of animation. Apply ColorShifter using final values and die
+		const auto & shifter = steps[index - 1];
+		owner.stacksController->setStackColorFilter(shifter, stack, spell, false);
+		delete this;
+		return;
+	}
+
+	assert(index != 0);
+
+	const auto & prevShifter = steps[index - 1];
+	const auto & nextShifter = steps[index];
+
+	float prevPoint = timePoints[index-1];
+	float nextPoint = timePoints[index];
+	float localProgress = totalProgress - prevPoint;
+	float stepDuration = (nextPoint - prevPoint);
+	float factor = localProgress / stepDuration;
+
+	auto shifter = ColorFilter::genInterpolated(prevShifter, nextShifter, factor);
+
+	owner.stacksController->setStackColorFilter(shifter, stack, spell, true);
+}
+
+ColorTransformAnimation::ColorTransformAnimation(BattleInterface & owner, const CStack * _stack, const std::string & colorFilterName, const CSpell * spell):
+	BattleStackAnimation(owner, _stack),
+	spell(spell),
+	totalProgress(0.f)
+{
+	auto effect = owner.effectsController->getMuxerEffect(colorFilterName);
+	steps = effect.filters;
+	timePoints = effect.timePoints;
+
+	assert(!steps.empty() && steps.size() == timePoints.size());
+
+	logAnim->debug("Created ColorTransformAnimation for %s", stack->getName());
+}
+
+RangedAttackAnimation::RangedAttackAnimation(BattleInterface & owner_, const CStack * attacker, BattleHex dest_, const CStack * defender)
+	: AttackAnimation(owner_, attacker, dest_, defender),
+	  projectileEmitted(false)
+{
+	setSound(getCreature()->sounds.shoot);
+}
+
+bool RangedAttackAnimation::init()
+{
+	setAnimationGroup();
+	initializeProjectile();
+
+	return AttackAnimation::init();
+}
+
+void RangedAttackAnimation::setAnimationGroup()
+{
+	Point shooterPos = stackAnimation(attackingStack)->pos.topLeft();
+	Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack);
+
+	//maximal angle in radians between straight horizontal line and shooting line for which shot is considered to be straight (absoulte value)
+	static const double straightAngle = 0.2;
+
+	double projectileAngle = -atan2(shotTarget.y - shooterPos.y, std::abs(shotTarget.x - shooterPos.x));
+
+	// Calculate projectile start position. Offsets are read out of the CRANIM.TXT.
+	if (projectileAngle > straightAngle)
+		setGroup(getUpwardsGroup());
+	else if (projectileAngle < -straightAngle)
+		setGroup(getDownwardsGroup());
+	else
+		setGroup(getForwardGroup());
+}
+
+void RangedAttackAnimation::initializeProjectile()
+{
+	const CCreature *shooterInfo = getCreature();
+	Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack) + Point(225, 225);
+	Point shotOrigin = stackAnimation(attackingStack)->pos.topLeft() + Point(222, 265);
+	int multiplier = stackFacingRight(attackingStack) ? 1 : -1;
+
+	if (getGroup() == getUpwardsGroup())
+	{
+		shotOrigin.x += ( -25 + shooterInfo->animation.upperRightMissleOffsetX ) * multiplier;
+		shotOrigin.y += shooterInfo->animation.upperRightMissleOffsetY;
+	}
+	else if (getGroup() == getDownwardsGroup())
+	{
+		shotOrigin.x += ( -25 + shooterInfo->animation.lowerRightMissleOffsetX ) * multiplier;
+		shotOrigin.y += shooterInfo->animation.lowerRightMissleOffsetY;
+	}
+	else if (getGroup() == getForwardGroup())
+	{
+		shotOrigin.x += ( -25 + shooterInfo->animation.rightMissleOffsetX ) * multiplier;
+		shotOrigin.y += shooterInfo->animation.rightMissleOffsetY;
+	}
+	else
+	{
+		assert(0);
+	}
+
+	createProjectile(shotOrigin, shotTarget);
+}
+
+void RangedAttackAnimation::emitProjectile()
+{
+	logAnim->debug("Ranged attack projectile emitted");
+	owner.projectilesController->emitStackProjectile(attackingStack);
+	projectileEmitted = true;
+}
+
+void RangedAttackAnimation::tick(uint32_t msPassed)
+{
+	// animation should be paused if there is an active projectile
+	if (projectileEmitted)
+	{
+		if (!owner.projectilesController->hasActiveProjectile(attackingStack, false))
+			owner.executeAnimationStage(EAnimationEvents::HIT);
+
+	}
+
+	bool stackHasProjectile = owner.projectilesController->hasActiveProjectile(stack, true);
+
+	if (!projectileEmitted || stackHasProjectile)
+		stackAnimation(attackingStack)->playUntil(getAttackClimaxFrame());
+	else
+		stackAnimation(attackingStack)->playUntil(static_cast<size_t>(-1));
+
+	AttackAnimation::tick(msPassed);
+
+	if (!projectileEmitted)
+	{
+		// emit projectile once animation playback reached "climax" frame
+		if ( stackAnimation(attackingStack)->getCurrentFrame() >= getAttackClimaxFrame() )
+		{
+			emitProjectile();
+			return;
+		}
+	}
+}
+
+RangedAttackAnimation::~RangedAttackAnimation()
+{
+	assert(!owner.projectilesController->hasActiveProjectile(attackingStack, false));
+	assert(projectileEmitted);
+
+	// FIXME: is this possible? Animation is over but we're yet to fire projectile?
+	if (!projectileEmitted)
+	{
+		logAnim->warn("Shooting animation has finished but projectile was not emitted! Emitting it now...");
+		emitProjectile();
+	}
+}
+
+ShootingAnimation::ShootingAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked)
+	: RangedAttackAnimation(owner, attacker, _dest, _attacked)
+{
+	logAnim->debug("Created ShootingAnimation for %s", stack->getName());
+}
+
+void ShootingAnimation::createProjectile(const Point & from, const Point & dest) const
+{
+	owner.projectilesController->createProjectile(attackingStack, from, dest);
+}
+
+uint32_t ShootingAnimation::getAttackClimaxFrame() const
+{
+	const CCreature *shooterInfo = getCreature();
+
+	uint32_t maxFrames = stackAnimation(attackingStack)->framesInGroup(getGroup());
+	uint32_t climaxFrame = shooterInfo->animation.attackClimaxFrame;
+	uint32_t selectedFrame = std::clamp<int>(shooterInfo->animation.attackClimaxFrame, 1, maxFrames);
+
+	if (climaxFrame != selectedFrame)
+		logGlobal->warn("Shooter %s has ranged attack climax frame set to %d, but only %d available!", shooterInfo->getNamePluralTranslated(), climaxFrame, maxFrames);
+
+	return selectedFrame - 1; // H3 counts frames from 1
+}
+
+ECreatureAnimType ShootingAnimation::getUpwardsGroup() const
+{
+	return ECreatureAnimType::SHOOT_UP;
+}
+
+ECreatureAnimType ShootingAnimation::getForwardGroup() const
+{
+	return ECreatureAnimType::SHOOT_FRONT;
+}
+
+ECreatureAnimType ShootingAnimation::getDownwardsGroup() const
+{
+	return ECreatureAnimType::SHOOT_DOWN;
+}
+
+CatapultAnimation::CatapultAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, int _catapultDmg)
+	: ShootingAnimation(owner, attacker, _dest, _attacked),
+	catapultDamage(_catapultDmg),
+	explosionEmitted(false)
+{
+	logAnim->debug("Created shooting anim for %s", stack->getName());
+}
+
+void CatapultAnimation::tick(uint32_t msPassed)
+{
+	ShootingAnimation::tick(msPassed);
+
+	if ( explosionEmitted)
+		return;
+
+	if ( !projectileEmitted)
+		return;
+
+	if (owner.projectilesController->hasActiveProjectile(attackingStack, false))
+		return;
+
+	explosionEmitted = true;
+	Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack) + Point(225, 225) - Point(126, 105);
+
+	auto soundFilename  = AudioPath::builtin((catapultDamage > 0) ? "WALLHIT" : "WALLMISS");
+	AnimationPath effectFilename = AnimationPath::builtin((catapultDamage > 0) ? "SGEXPL" : "CSGRCK");
+
+	CCS->soundh->playSound( soundFilename );
+	owner.stacksController->addNewAnim( new EffectAnimation(owner, effectFilename, shotTarget));
+}
+
+void CatapultAnimation::createProjectile(const Point & from, const Point & dest) const
+{
+	owner.projectilesController->createCatapultProjectile(attackingStack, from, dest);
+}
+
+CastAnimation::CastAnimation(BattleInterface & owner_, const CStack * attacker, BattleHex dest, const CStack * defender, const CSpell * spell)
+	: RangedAttackAnimation(owner_, attacker, dest, defender),
+	  spell(spell)
+{
+	if(!dest.isValid())
+	{
+		assert(spell->animationInfo.projectile.empty());
+
+		if (defender)
+			dest = defender->getPosition();
+		else
+			dest = attacker->getPosition();
+	}
+}
+
+ECreatureAnimType CastAnimation::getUpwardsGroup() const
+{
+	return findValidGroup({
+		ECreatureAnimType::CAST_UP,
+		ECreatureAnimType::SPECIAL_UP,
+		ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3
+		ECreatureAnimType::SHOOT_UP,
+		ECreatureAnimType::ATTACK_UP
+	});
+}
+
+ECreatureAnimType CastAnimation::getForwardGroup() const
+{
+	return findValidGroup({
+		ECreatureAnimType::CAST_FRONT,
+		ECreatureAnimType::SPECIAL_FRONT,
+		ECreatureAnimType::SHOOT_FRONT,
+		ECreatureAnimType::ATTACK_FRONT
+	});
+}
+
+ECreatureAnimType CastAnimation::getDownwardsGroup() const
+{
+	return findValidGroup({
+		ECreatureAnimType::CAST_DOWN,
+		ECreatureAnimType::SPECIAL_DOWN,
+		ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3
+		ECreatureAnimType::SHOOT_DOWN,
+		ECreatureAnimType::ATTACK_DOWN
+	});
+}
+
+void CastAnimation::createProjectile(const Point & from, const Point & dest) const
+{
+	if (!spell->animationInfo.projectile.empty())
+		owner.projectilesController->createSpellProjectile(attackingStack, from, dest, spell);
+}
+
+uint32_t CastAnimation::getAttackClimaxFrame() const
+{
+	//TODO: allow defining this parameter in config file, separately from attackClimaxFrame of missile attacks
+	uint32_t maxFrames = stackAnimation(attackingStack)->framesInGroup(getGroup());
+
+	return maxFrames / 2;
+}
+
+EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects, bool reversed):
+	BattleAnimation(owner),
+	animation(GH.renderHandler().loadAnimation(animationName)),
+	effectFlags(effects),
+	effectFinished(false),
+	reversed(reversed)
+{
+	logAnim->debug("CPointEffectAnimation::init: effect %s", animationName.getName());
+}
+
+EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector<BattleHex> hex, int effects, bool reversed):
+	EffectAnimation(owner, animationName, effects, reversed)
+{
+	battlehexes = hex;
+}
+
+EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex, int effects, bool reversed):
+	EffectAnimation(owner, animationName, effects, reversed)
+{
+	assert(hex.isValid());
+	battlehexes.push_back(hex);
+}
+
+EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector<Point> pos, int effects, bool reversed):
+	EffectAnimation(owner, animationName, effects, reversed)
+{
+	positions = pos;
+}
+
+EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, int effects, bool reversed):
+	EffectAnimation(owner, animationName, effects, reversed)
+{
+	positions.push_back(pos);
+}
+
+EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, BattleHex hex, int effects, bool reversed):
+	EffectAnimation(owner, animationName, effects, reversed)
+{
+	assert(hex.isValid());
+	battlehexes.push_back(hex);
+	positions.push_back(pos);
+}
+
+bool EffectAnimation::init()
+{
+	animation->preload();
+
+	auto first = animation->getImage(0, 0, true);
+	if(!first)
+	{
+		delete this;
+		return false;
+	}
+
+	for (size_t i = 0; i < animation->size(size_t(BattleEffect::AnimType::DEFAULT)); ++i)
+	{
+		size_t current = animation->size(size_t(BattleEffect::AnimType::DEFAULT)) - 1 - i;
+
+		animation->duplicateImage(size_t(BattleEffect::AnimType::DEFAULT), current, size_t(BattleEffect::AnimType::REVERSE));
+	}
+
+	if (screenFill())
+	{
+		for(int i=0; i * first->width() < owner.fieldController->pos.w ; ++i)
+			for(int j=0; j * first->height() < owner.fieldController->pos.h ; ++j)
+				positions.push_back(Point( i * first->width(), j * first->height()));
+	}
+
+	BattleEffect be;
+	be.effectID = ID;
+	be.animation = animation;
+	be.currentFrame = 0;
+	be.type = reversed ? BattleEffect::AnimType::REVERSE : BattleEffect::AnimType::DEFAULT;
+
+	for (size_t i = 0; i < std::max(battlehexes.size(), positions.size()); ++i)
+	{
+		bool hasTile = i < battlehexes.size();
+		bool hasPosition = i < positions.size();
+
+		if (hasTile && !forceOnTop())
+			be.tile = battlehexes[i];
+		else
+			be.tile = BattleHex::INVALID;
+
+		if (hasPosition)
+		{
+			be.pos.x = positions[i].x;
+			be.pos.y = positions[i].y;
+		}
+		else
+		{
+			const auto * destStack = owner.getBattle()->battleGetUnitByPos(battlehexes[i], false);
+			Rect tilePos = owner.fieldController->hexPositionLocal(battlehexes[i]);
+
+			be.pos.x = tilePos.x + tilePos.w/2 - first->width()/2;
+
+			if(destStack && destStack->doubleWide()) // Correction for 2-hex creatures.
+				be.pos.x += (destStack->unitSide() == BattleSide::ATTACKER ? -1 : 1)*tilePos.w/2;
+
+			if (alignToBottom())
+				be.pos.y = tilePos.y + tilePos.h - first->height();
+			else
+				be.pos.y = tilePos.y - first->height()/2;
+		}
+		owner.effectsController->battleEffects.push_back(be);
+	}
+	return true;
+}
+
+void EffectAnimation::tick(uint32_t msPassed)
+{
+	playEffect(msPassed);
+
+	if (effectFinished)
+	{
+		//remove visual effect itself only if sound has finished as well - necessary for obstacles like force field
+		clearEffect();
+		delete this;
+	}
+}
+
+bool EffectAnimation::alignToBottom() const
+{
+	return effectFlags & ALIGN_TO_BOTTOM;
+}
+
+bool EffectAnimation::forceOnTop() const
+{
+	return effectFlags & FORCE_ON_TOP;
+}
+
+bool EffectAnimation::screenFill() const
+{
+	return effectFlags & SCREEN_FILL;
+}
+
+void EffectAnimation::onEffectFinished()
+{
+	effectFinished = true;
+}
+
+void EffectAnimation::playEffect(uint32_t msPassed)
+{
+	if ( effectFinished )
+		return;
+
+	for(auto & elem : owner.effectsController->battleEffects)
+	{
+		if(elem.effectID == ID)
+		{
+			elem.currentFrame += AnimationControls::getSpellEffectSpeed() * msPassed / 1000;
+
+			if(elem.currentFrame >= elem.animation->size())
+			{
+				elem.currentFrame = elem.animation->size() - 1;
+				onEffectFinished();
+				break;
+			}
+		}
+	}
+}
+
+void EffectAnimation::clearEffect()
+{
+	auto & effects = owner.effectsController->battleEffects;
+
+	vstd::erase_if(effects, [&](const BattleEffect & effect){
+		return effect.effectID == ID;
+	});
+}
+
+EffectAnimation::~EffectAnimation()
+{
+	assert(effectFinished);
+}
+
+HeroCastAnimation::HeroCastAnimation(BattleInterface & owner, std::shared_ptr<BattleHero> hero, BattleHex dest, const CStack * defender, const CSpell * spell):
+	BattleAnimation(owner),
+	projectileEmitted(false),
+	hero(hero),
+	target(defender),
+	tile(dest),
+	spell(spell)
+{
+}
+
+bool HeroCastAnimation::init()
+{
+	hero->setPhase(EHeroAnimType::CAST_SPELL);
+
+	hero->onPhaseFinished([&](){
+		delete this;
+	});
+
+	initializeProjectile();
+
+	return true;
+}
+
+void HeroCastAnimation::initializeProjectile()
+{
+	// spell has no projectile to play, ignore this step
+	if (spell->animationInfo.projectile.empty())
+		return;
+
+	// targeted spells should have well, target
+	assert(tile.isValid());
+
+	Point srccoord = hero->pos.center() - hero->parent->pos.topLeft();
+	Point destcoord = owner.stacksController->getStackPositionAtHex(tile, target); //position attacked by projectile
+
+	destcoord += Point(222, 265); // FIXME: what are these constants?
+	owner.projectilesController->createSpellProjectile( nullptr, srccoord, destcoord, spell);
+}
+
+void HeroCastAnimation::emitProjectile()
+{
+	if (projectileEmitted)
+		return;
+
+	//spell has no projectile to play, skip this step and immediately play hit animations
+	if (spell->animationInfo.projectile.empty())
+		emitAnimationEvent();
+	else
+		owner.projectilesController->emitStackProjectile( nullptr );
+
+	projectileEmitted = true;
+}
+
+void HeroCastAnimation::emitAnimationEvent()
+{
+	owner.executeAnimationStage(EAnimationEvents::HIT);
+}
+
+void HeroCastAnimation::tick(uint32_t msPassed)
+{
+	float frame = hero->getFrame();
+
+	if (frame < 4.0f) // middle point of animation //TODO: un-hardcode
+		return;
+
+	if (!projectileEmitted)
+	{
+		emitProjectile();
+		hero->pause();
+		return;
+	}
+
+	if (!owner.projectilesController->hasActiveProjectile(nullptr, false))
+	{
+		emitAnimationEvent();
+		//TODO: check H3 - it is possible that hero animation should be paused until hit effect is over, not just projectile
+		hero->play();
+	}
+}

+ 372 - 372
client/battle/BattleAnimationClasses.h

@@ -1,372 +1,372 @@
-/*
- * BattleAnimations.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/battle/BattleHex.h"
-#include "../../lib/filesystem/ResourcePath.h"
-#include "BattleConstants.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-class CStack;
-class CCreature;
-class CSpell;
-class Point;
-
-VCMI_LIB_NAMESPACE_END
-
-class ColorFilter;
-class BattleHero;
-class CAnimation;
-class BattleInterface;
-class CreatureAnimation;
-struct StackAttackedInfo;
-
-/// Base class of battle animations
-class BattleAnimation
-{
-protected:
-	BattleInterface & owner;
-	bool initialized;
-
-	std::vector<BattleAnimation *> & pendingAnimations();
-	std::shared_ptr<CreatureAnimation> stackAnimation(const CStack * stack) const;
-	bool stackFacingRight(const CStack * stack);
-	void setStackFacingRight(const CStack * stack, bool facingRight);
-
-	virtual bool init() = 0; //to be called - if returned false, call again until returns true
-
-public:
-	ui32 ID; //unique identifier
-
-	bool isInitialized();
-	bool tryInitialize();
-	virtual void tick(uint32_t msPassed) {} //call every new frame
-	virtual ~BattleAnimation();
-
-	BattleAnimation(BattleInterface & owner);
-};
-
-/// Sub-class which is responsible for managing the battle stack animation.
-class BattleStackAnimation : public BattleAnimation
-{
-public:
-	std::shared_ptr<CreatureAnimation> myAnim; //animation for our stack, managed by BattleInterface
-	const CStack * stack; //id of stack whose animation it is
-
-	BattleStackAnimation(BattleInterface & owner, const CStack * _stack);
-	void rotateStack(BattleHex hex);
-};
-
-class StackActionAnimation : public BattleStackAnimation
-{
-	ECreatureAnimType nextGroup;
-	ECreatureAnimType currGroup;
-	AudioPath sound;
-public:
-	void setNextGroup( ECreatureAnimType group );
-	void setGroup( ECreatureAnimType group );
-	void setSound( const AudioPath & sound );
-
-	ECreatureAnimType getGroup() const;
-
-	StackActionAnimation(BattleInterface & owner, const CStack * _stack);
-	~StackActionAnimation();
-
-	bool init() override;
-};
-
-/// Animation of a defending unit
-class DefenceAnimation : public StackActionAnimation
-{
-public:
-	DefenceAnimation(BattleInterface & owner, const CStack * stack);
-};
-
-/// Animation of a hit unit
-class HittedAnimation : public StackActionAnimation
-{
-public:
-	HittedAnimation(BattleInterface & owner, const CStack * stack);
-};
-
-/// Animation of a dying unit
-class DeathAnimation : public StackActionAnimation
-{
-public:
-	DeathAnimation(BattleInterface & owner, const CStack * stack, bool ranged);
-};
-
-/// Resurrects stack from dead state
-class ResurrectionAnimation : public StackActionAnimation
-{
-public:
-	ResurrectionAnimation(BattleInterface & owner, const CStack * _stack);
-};
-
-class ColorTransformAnimation : public BattleStackAnimation
-{
-	std::vector<ColorFilter> steps;
-	std::vector<float> timePoints;
-	const CSpell * spell;
-
-	float totalProgress;
-
-	bool init() override;
-	void tick(uint32_t msPassed) override;
-
-public:
-	ColorTransformAnimation(BattleInterface & owner, const CStack * _stack, const std::string & colorFilterName, const CSpell * spell);
-};
-
-/// Base class for all animations that play during stack movement
-class StackMoveAnimation : public BattleStackAnimation
-{
-public:
-	BattleHex nextHex;
-	BattleHex prevHex;
-
-protected:
-	StackMoveAnimation(BattleInterface & owner, const CStack * _stack, BattleHex prevHex, BattleHex nextHex);
-};
-
-/// Move animation of a creature
-class MovementAnimation : public StackMoveAnimation
-{
-private:
-	int moveSoundHander; // sound handler used when moving a unit
-
-	std::vector<BattleHex> destTiles; //full path, includes already passed hexes
-	ui32 curentMoveIndex; // index of nextHex in destTiles
-
-	double begX, begY; // starting position
-	double distanceX, distanceY; // full movement distance, may be negative if creture moves topleft
-
-	/// progress gain per second
-	double progressPerSecond;
-
-	/// range 0 -> 1, indicates move progrees. 0 = movement starts, 1 = move ends
-	double progress;
-
-public:
-	bool init() override;
-	void tick(uint32_t msPassed) override;
-
-	MovementAnimation(BattleInterface & owner, const CStack *_stack, std::vector<BattleHex> _destTiles, int _distance);
-	~MovementAnimation();
-};
-
-/// Move end animation of a creature
-class MovementEndAnimation : public StackMoveAnimation
-{
-public:
-	bool init() override;
-
-	MovementEndAnimation(BattleInterface & owner, const CStack * _stack, BattleHex destTile);
-	~MovementEndAnimation();
-};
-
-/// Move start animation of a creature
-class MovementStartAnimation : public StackMoveAnimation
-{
-public:
-	bool init() override;
-
-	MovementStartAnimation(BattleInterface & owner, const CStack * _stack);
-};
-
-/// Class responsible for animation of stack chaning direction (left <-> right)
-class ReverseAnimation : public StackMoveAnimation
-{
-	void setupSecondPart();
-public:
-	bool init() override;
-
-	ReverseAnimation(BattleInterface & owner, const CStack * stack, BattleHex dest);
-};
-
-/// This class is responsible for managing the battle attack animation
-class AttackAnimation : public StackActionAnimation
-{
-protected:
-	BattleHex dest; //attacked hex
-	const CStack *defendingStack;
-	const CStack *attackingStack;
-	int attackingStackPosBeforeReturn; //for stacks with return_after_strike feature
-
-	const CCreature * getCreature() const;
-	ECreatureAnimType findValidGroup( const std::vector<ECreatureAnimType> candidates ) const;
-
-public:
-	AttackAnimation(BattleInterface & owner, const CStack *attacker, BattleHex _dest, const CStack *defender);
-};
-
-/// Hand-to-hand attack
-class MeleeAttackAnimation : public AttackAnimation
-{
-	ECreatureAnimType getUpwardsGroup(bool multiAttack) const;
-	ECreatureAnimType getForwardGroup(bool multiAttack) const;
-	ECreatureAnimType getDownwardsGroup(bool multiAttack) const;
-
-	ECreatureAnimType selectGroup(bool multiAttack);
-
-public:
-	MeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool multiAttack);
-
-	void tick(uint32_t msPassed) override;
-};
-
-
-class RangedAttackAnimation : public AttackAnimation
-{
-	void setAnimationGroup();
-	void initializeProjectile();
-	void emitProjectile();
-	void emitExplosion();
-
-protected:
-	bool projectileEmitted;
-
-	virtual ECreatureAnimType getUpwardsGroup() const = 0;
-	virtual ECreatureAnimType getForwardGroup() const = 0;
-	virtual ECreatureAnimType getDownwardsGroup() const = 0;
-
-	virtual void createProjectile(const Point & from, const Point & dest) const = 0;
-	virtual uint32_t getAttackClimaxFrame() const = 0;
-
-public:
-	RangedAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender);
-	~RangedAttackAnimation();
-
-	bool init() override;
-	void tick(uint32_t msPassed) override;
-};
-
-/// Shooting attack
-class ShootingAnimation : public RangedAttackAnimation
-{
-	ECreatureAnimType getUpwardsGroup() const override;
-	ECreatureAnimType getForwardGroup() const override;
-	ECreatureAnimType getDownwardsGroup() const override;
-
-	void createProjectile(const Point & from, const Point & dest) const override;
-	uint32_t getAttackClimaxFrame() const override;
-
-public:
-	ShootingAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender);
-
-};
-
-/// Catapult attack
-class CatapultAnimation : public ShootingAnimation
-{
-private:
-	bool explosionEmitted;
-	int catapultDamage;
-
-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 tick(uint32_t msPassed) override;
-};
-
-class CastAnimation : public RangedAttackAnimation
-{
-	const CSpell * spell;
-
-	ECreatureAnimType getUpwardsGroup() const override;
-	ECreatureAnimType getForwardGroup() const override;
-	ECreatureAnimType getDownwardsGroup() const override;
-
-	void createProjectile(const Point & from, const Point & dest) const override;
-	uint32_t getAttackClimaxFrame() const override;
-
-public:
-	CastAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest_, const CStack * defender, const CSpell * spell);
-};
-
-class DummyAnimation : public BattleAnimation
-{
-private:
-	int counter;
-	int howMany;
-public:
-	bool init() override;
-	void tick(uint32_t msPassed) override;
-
-	DummyAnimation(BattleInterface & owner, int howManyFrames);
-};
-
-/// Class that plays effect at one or more positions along with (single) sound effect
-class EffectAnimation : public BattleAnimation
-{
-	std::string soundName;
-	bool effectFinished;
-	bool reversed;
-	int effectFlags;
-
-	std::shared_ptr<CAnimation>	animation;
-	std::vector<Point> positions;
-	std::vector<BattleHex> battlehexes;
-
-	bool alignToBottom() const;
-	bool waitForSound() const;
-	bool forceOnTop() const;
-	bool screenFill() const;
-
-	void onEffectFinished();
-	void clearEffect();
-	void playEffect(uint32_t msPassed);
-
-public:
-	enum EEffectFlags
-	{
-		ALIGN_TO_BOTTOM = 1,
-		FORCE_ON_TOP    = 2,
-		SCREEN_FILL     = 4,
-	};
-
-	/// Create animation with screen-wide effect
-	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects = 0, bool reversed = false);
-
-	/// Create animation positioned at point(s). Note that positions must be are absolute, including battleint position offset
-	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos                 , int effects = 0, bool reversed = false);
-	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector<Point> pos    , int effects = 0, bool reversed = false);
-
-	/// Create animation positioned at certain hex(es)
-	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex             , int effects = 0, bool reversed = false);
-	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector<BattleHex> hex, int effects = 0, bool reversed = false);
-
-	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, BattleHex hex,   int effects = 0, bool reversed = false);
-	 ~EffectAnimation();
-
-	bool init() override;
-	void tick(uint32_t msPassed) override;
-};
-
-class HeroCastAnimation : public BattleAnimation
-{
-	std::shared_ptr<BattleHero> hero;
-	const CStack * target;
-	const CSpell * spell;
-	BattleHex tile;
-	bool projectileEmitted;
-
-	void initializeProjectile();
-	void emitProjectile();
-	void emitAnimationEvent();
-
-public:
-	HeroCastAnimation(BattleInterface & owner, std::shared_ptr<BattleHero> hero, BattleHex dest, const CStack * defender, const CSpell * spell);
-
-	void tick(uint32_t msPassed) override;
-	bool init() override;
-};
+/*
+ * BattleAnimations.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/battle/BattleHex.h"
+#include "../../lib/filesystem/ResourcePath.h"
+#include "BattleConstants.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class CStack;
+class CCreature;
+class CSpell;
+class Point;
+
+VCMI_LIB_NAMESPACE_END
+
+class ColorFilter;
+class BattleHero;
+class CAnimation;
+class BattleInterface;
+class CreatureAnimation;
+struct StackAttackedInfo;
+
+/// Base class of battle animations
+class BattleAnimation
+{
+protected:
+	BattleInterface & owner;
+	bool initialized;
+
+	std::vector<BattleAnimation *> & pendingAnimations();
+	std::shared_ptr<CreatureAnimation> stackAnimation(const CStack * stack) const;
+	bool stackFacingRight(const CStack * stack);
+	void setStackFacingRight(const CStack * stack, bool facingRight);
+
+	virtual bool init() = 0; //to be called - if returned false, call again until returns true
+
+public:
+	ui32 ID; //unique identifier
+
+	bool isInitialized();
+	bool tryInitialize();
+	virtual void tick(uint32_t msPassed) {} //call every new frame
+	virtual ~BattleAnimation();
+
+	BattleAnimation(BattleInterface & owner);
+};
+
+/// Sub-class which is responsible for managing the battle stack animation.
+class BattleStackAnimation : public BattleAnimation
+{
+public:
+	std::shared_ptr<CreatureAnimation> myAnim; //animation for our stack, managed by BattleInterface
+	const CStack * stack; //id of stack whose animation it is
+
+	BattleStackAnimation(BattleInterface & owner, const CStack * _stack);
+	void rotateStack(BattleHex hex);
+};
+
+class StackActionAnimation : public BattleStackAnimation
+{
+	ECreatureAnimType nextGroup;
+	ECreatureAnimType currGroup;
+	AudioPath sound;
+public:
+	void setNextGroup( ECreatureAnimType group );
+	void setGroup( ECreatureAnimType group );
+	void setSound( const AudioPath & sound );
+
+	ECreatureAnimType getGroup() const;
+
+	StackActionAnimation(BattleInterface & owner, const CStack * _stack);
+	~StackActionAnimation();
+
+	bool init() override;
+};
+
+/// Animation of a defending unit
+class DefenceAnimation : public StackActionAnimation
+{
+public:
+	DefenceAnimation(BattleInterface & owner, const CStack * stack);
+};
+
+/// Animation of a hit unit
+class HittedAnimation : public StackActionAnimation
+{
+public:
+	HittedAnimation(BattleInterface & owner, const CStack * stack);
+};
+
+/// Animation of a dying unit
+class DeathAnimation : public StackActionAnimation
+{
+public:
+	DeathAnimation(BattleInterface & owner, const CStack * stack, bool ranged);
+};
+
+/// Resurrects stack from dead state
+class ResurrectionAnimation : public StackActionAnimation
+{
+public:
+	ResurrectionAnimation(BattleInterface & owner, const CStack * _stack);
+};
+
+class ColorTransformAnimation : public BattleStackAnimation
+{
+	std::vector<ColorFilter> steps;
+	std::vector<float> timePoints;
+	const CSpell * spell;
+
+	float totalProgress;
+
+	bool init() override;
+	void tick(uint32_t msPassed) override;
+
+public:
+	ColorTransformAnimation(BattleInterface & owner, const CStack * _stack, const std::string & colorFilterName, const CSpell * spell);
+};
+
+/// Base class for all animations that play during stack movement
+class StackMoveAnimation : public BattleStackAnimation
+{
+public:
+	BattleHex nextHex;
+	BattleHex prevHex;
+
+protected:
+	StackMoveAnimation(BattleInterface & owner, const CStack * _stack, BattleHex prevHex, BattleHex nextHex);
+};
+
+/// Move animation of a creature
+class MovementAnimation : public StackMoveAnimation
+{
+private:
+	int moveSoundHander; // sound handler used when moving a unit
+
+	std::vector<BattleHex> destTiles; //full path, includes already passed hexes
+	ui32 curentMoveIndex; // index of nextHex in destTiles
+
+	double begX, begY; // starting position
+	double distanceX, distanceY; // full movement distance, may be negative if creture moves topleft
+
+	/// progress gain per second
+	double progressPerSecond;
+
+	/// range 0 -> 1, indicates move progrees. 0 = movement starts, 1 = move ends
+	double progress;
+
+public:
+	bool init() override;
+	void tick(uint32_t msPassed) override;
+
+	MovementAnimation(BattleInterface & owner, const CStack *_stack, std::vector<BattleHex> _destTiles, int _distance);
+	~MovementAnimation();
+};
+
+/// Move end animation of a creature
+class MovementEndAnimation : public StackMoveAnimation
+{
+public:
+	bool init() override;
+
+	MovementEndAnimation(BattleInterface & owner, const CStack * _stack, BattleHex destTile);
+	~MovementEndAnimation();
+};
+
+/// Move start animation of a creature
+class MovementStartAnimation : public StackMoveAnimation
+{
+public:
+	bool init() override;
+
+	MovementStartAnimation(BattleInterface & owner, const CStack * _stack);
+};
+
+/// Class responsible for animation of stack chaning direction (left <-> right)
+class ReverseAnimation : public StackMoveAnimation
+{
+	void setupSecondPart();
+public:
+	bool init() override;
+
+	ReverseAnimation(BattleInterface & owner, const CStack * stack, BattleHex dest);
+};
+
+/// This class is responsible for managing the battle attack animation
+class AttackAnimation : public StackActionAnimation
+{
+protected:
+	BattleHex dest; //attacked hex
+	const CStack *defendingStack;
+	const CStack *attackingStack;
+	int attackingStackPosBeforeReturn; //for stacks with return_after_strike feature
+
+	const CCreature * getCreature() const;
+	ECreatureAnimType findValidGroup( const std::vector<ECreatureAnimType> candidates ) const;
+
+public:
+	AttackAnimation(BattleInterface & owner, const CStack *attacker, BattleHex _dest, const CStack *defender);
+};
+
+/// Hand-to-hand attack
+class MeleeAttackAnimation : public AttackAnimation
+{
+	ECreatureAnimType getUpwardsGroup(bool multiAttack) const;
+	ECreatureAnimType getForwardGroup(bool multiAttack) const;
+	ECreatureAnimType getDownwardsGroup(bool multiAttack) const;
+
+	ECreatureAnimType selectGroup(bool multiAttack);
+
+public:
+	MeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool multiAttack);
+
+	void tick(uint32_t msPassed) override;
+};
+
+
+class RangedAttackAnimation : public AttackAnimation
+{
+	void setAnimationGroup();
+	void initializeProjectile();
+	void emitProjectile();
+	void emitExplosion();
+
+protected:
+	bool projectileEmitted;
+
+	virtual ECreatureAnimType getUpwardsGroup() const = 0;
+	virtual ECreatureAnimType getForwardGroup() const = 0;
+	virtual ECreatureAnimType getDownwardsGroup() const = 0;
+
+	virtual void createProjectile(const Point & from, const Point & dest) const = 0;
+	virtual uint32_t getAttackClimaxFrame() const = 0;
+
+public:
+	RangedAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender);
+	~RangedAttackAnimation();
+
+	bool init() override;
+	void tick(uint32_t msPassed) override;
+};
+
+/// Shooting attack
+class ShootingAnimation : public RangedAttackAnimation
+{
+	ECreatureAnimType getUpwardsGroup() const override;
+	ECreatureAnimType getForwardGroup() const override;
+	ECreatureAnimType getDownwardsGroup() const override;
+
+	void createProjectile(const Point & from, const Point & dest) const override;
+	uint32_t getAttackClimaxFrame() const override;
+
+public:
+	ShootingAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender);
+
+};
+
+/// Catapult attack
+class CatapultAnimation : public ShootingAnimation
+{
+private:
+	bool explosionEmitted;
+	int catapultDamage;
+
+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 tick(uint32_t msPassed) override;
+};
+
+class CastAnimation : public RangedAttackAnimation
+{
+	const CSpell * spell;
+
+	ECreatureAnimType getUpwardsGroup() const override;
+	ECreatureAnimType getForwardGroup() const override;
+	ECreatureAnimType getDownwardsGroup() const override;
+
+	void createProjectile(const Point & from, const Point & dest) const override;
+	uint32_t getAttackClimaxFrame() const override;
+
+public:
+	CastAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest_, const CStack * defender, const CSpell * spell);
+};
+
+class DummyAnimation : public BattleAnimation
+{
+private:
+	int counter;
+	int howMany;
+public:
+	bool init() override;
+	void tick(uint32_t msPassed) override;
+
+	DummyAnimation(BattleInterface & owner, int howManyFrames);
+};
+
+/// Class that plays effect at one or more positions along with (single) sound effect
+class EffectAnimation : public BattleAnimation
+{
+	std::string soundName;
+	bool effectFinished;
+	bool reversed;
+	int effectFlags;
+
+	std::shared_ptr<CAnimation>	animation;
+	std::vector<Point> positions;
+	std::vector<BattleHex> battlehexes;
+
+	bool alignToBottom() const;
+	bool waitForSound() const;
+	bool forceOnTop() const;
+	bool screenFill() const;
+
+	void onEffectFinished();
+	void clearEffect();
+	void playEffect(uint32_t msPassed);
+
+public:
+	enum EEffectFlags
+	{
+		ALIGN_TO_BOTTOM = 1,
+		FORCE_ON_TOP    = 2,
+		SCREEN_FILL     = 4,
+	};
+
+	/// Create animation with screen-wide effect
+	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects = 0, bool reversed = false);
+
+	/// Create animation positioned at point(s). Note that positions must be are absolute, including battleint position offset
+	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos                 , int effects = 0, bool reversed = false);
+	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector<Point> pos    , int effects = 0, bool reversed = false);
+
+	/// Create animation positioned at certain hex(es)
+	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex             , int effects = 0, bool reversed = false);
+	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector<BattleHex> hex, int effects = 0, bool reversed = false);
+
+	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, BattleHex hex,   int effects = 0, bool reversed = false);
+	 ~EffectAnimation();
+
+	bool init() override;
+	void tick(uint32_t msPassed) override;
+};
+
+class HeroCastAnimation : public BattleAnimation
+{
+	std::shared_ptr<BattleHero> hero;
+	const CStack * target;
+	const CSpell * spell;
+	BattleHex tile;
+	bool projectileEmitted;
+
+	void initializeProjectile();
+	void emitProjectile();
+	void emitAnimationEvent();
+
+public:
+	HeroCastAnimation(BattleInterface & owner, std::shared_ptr<BattleHero> hero, BattleHex dest, const CStack * defender, const CSpell * spell);
+
+	void tick(uint32_t msPassed) override;
+	bool init() override;
+};

+ 100 - 100
client/battle/BattleConstants.h

@@ -1,100 +1,100 @@
-/*
- * BattleConstants.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 EBattleEffect
-{
-	// list of battle effects that have hardcoded triggers
-	MAGIC_MIRROR = 3,
-	FIRE_SHIELD  = 11,
-	FEAR         = 15,
-	GOOD_LUCK    = 18,
-	GOOD_MORALE  = 20,
-	BAD_MORALE   = 30,
-	BAD_LUCK     = 48,
-	RESURRECT    = 50,
-	DRAIN_LIFE   = 52,
-	POISON       = 67,
-	DEATH_BLOW   = 73,
-	REGENERATION = 74,
-	MANA_DRAIN   = 77,
-	RESISTANCE   = 78,
-
-	INVALID      = -1,
-};
-
-enum class EAnimationEvents
-{
-	// any action
-	ROTATE,      // stacks rotate before action
-
-	// movement action
-	MOVE_START,  // stack starts movement
-	MOVEMENT,    // movement animation loop starts
-	MOVE_END,    // stack end movement
-
-	// attack/spellcast action
-	BEFORE_HIT,  // attack and defence effects play, e.g. luck/death blow
-	ATTACK,      // attack and defence animations are playing
-	HIT,         // hit & death animations are playing
-	AFTER_HIT,   // post-attack effect, e.g. phoenix rebirth
-
-	COUNT
-};
-
-enum class EHeroAnimType
-{
-	HOLDING    = 0,
-	IDLE       = 1, // idling movement that happens from time to time
-	DEFEAT     = 2, // played when army loses stack or on friendly fire
-	VICTORY    = 3, // when enemy stack killed or huge damage is dealt
-	CAST_SPELL = 4  // spellcasting
-};
-
-enum class ECreatureAnimType
-{
-	INVALID         = -1,
-
-	MOVING          = 0,
-	MOUSEON         = 1,
-	HOLDING         = 2,  // base idling animation
-	HITTED          = 3,  // base animation for when stack is taking damage
-	DEFENCE         = 4,  // alternative animation for defending in melee if stack spent its action on defending
-	DEATH           = 5,
-	DEATH_RANGED    = 6,  // Optional, alternative animation for when stack is killed by ranged attack
-	TURN_L          = 7,
-	TURN_R          = 8,
-	//TURN_L2       = 9,  //unused - identical to TURN_L
-	//TURN_R2       = 10, //unused - identical to TURN_R
-	ATTACK_UP       = 11,
-	ATTACK_FRONT    = 12,
-	ATTACK_DOWN     = 13,
-	SHOOT_UP        = 14, // Shooters only
-	SHOOT_FRONT     = 15, // Shooters only
-	SHOOT_DOWN      = 16, // Shooters only
-	SPECIAL_UP      = 17, // If empty, fallback to SPECIAL_FRONT
-	SPECIAL_FRONT   = 18, // Used for any special moves - dragon breath, spellcasting, Pit Lord/Ogre Mage ability
-	SPECIAL_DOWN    = 19, // If empty, fallback to SPECIAL_FRONT
-	MOVE_START      = 20, // small animation to be played before MOVING
-	MOVE_END        = 21, // small animation to be played after MOVING
-
-	DEAD            = 22, // new group, used to show dead stacks. If empty - last frame from "DEATH" will be copied here
-	DEAD_RANGED     = 23, // new group, used to show dead stacks (if DEATH_RANGED was used). If empty - last frame from "DEATH_RANGED" will be copied here
-	RESURRECTION    = 24, // new group, used for animating resurrection, if empty - reversed "DEATH" animation will be copied here
-	FROZEN          = 25, // new group, used when stack animation is paused (e.g. petrified). If empty - consist of first frame from HOLDING animation
-
-	CAST_UP            = 30,
-	CAST_FRONT         = 31,
-	CAST_DOWN          = 32,
-
-	GROUP_ATTACK_UP    = 40,
-	GROUP_ATTACK_FRONT = 41,
-	GROUP_ATTACK_DOWN  = 42
-};
+/*
+ * BattleConstants.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 EBattleEffect
+{
+	// list of battle effects that have hardcoded triggers
+	MAGIC_MIRROR = 3,
+	FIRE_SHIELD  = 11,
+	FEAR         = 15,
+	GOOD_LUCK    = 18,
+	GOOD_MORALE  = 20,
+	BAD_MORALE   = 30,
+	BAD_LUCK     = 48,
+	RESURRECT    = 50,
+	DRAIN_LIFE   = 52,
+	POISON       = 67,
+	DEATH_BLOW   = 73,
+	REGENERATION = 74,
+	MANA_DRAIN   = 77,
+	RESISTANCE   = 78,
+
+	INVALID      = -1,
+};
+
+enum class EAnimationEvents
+{
+	// any action
+	ROTATE,      // stacks rotate before action
+
+	// movement action
+	MOVE_START,  // stack starts movement
+	MOVEMENT,    // movement animation loop starts
+	MOVE_END,    // stack end movement
+
+	// attack/spellcast action
+	BEFORE_HIT,  // attack and defence effects play, e.g. luck/death blow
+	ATTACK,      // attack and defence animations are playing
+	HIT,         // hit & death animations are playing
+	AFTER_HIT,   // post-attack effect, e.g. phoenix rebirth
+
+	COUNT
+};
+
+enum class EHeroAnimType
+{
+	HOLDING    = 0,
+	IDLE       = 1, // idling movement that happens from time to time
+	DEFEAT     = 2, // played when army loses stack or on friendly fire
+	VICTORY    = 3, // when enemy stack killed or huge damage is dealt
+	CAST_SPELL = 4  // spellcasting
+};
+
+enum class ECreatureAnimType
+{
+	INVALID         = -1,
+
+	MOVING          = 0,
+	MOUSEON         = 1,
+	HOLDING         = 2,  // base idling animation
+	HITTED          = 3,  // base animation for when stack is taking damage
+	DEFENCE         = 4,  // alternative animation for defending in melee if stack spent its action on defending
+	DEATH           = 5,
+	DEATH_RANGED    = 6,  // Optional, alternative animation for when stack is killed by ranged attack
+	TURN_L          = 7,
+	TURN_R          = 8,
+	//TURN_L2       = 9,  //unused - identical to TURN_L
+	//TURN_R2       = 10, //unused - identical to TURN_R
+	ATTACK_UP       = 11,
+	ATTACK_FRONT    = 12,
+	ATTACK_DOWN     = 13,
+	SHOOT_UP        = 14, // Shooters only
+	SHOOT_FRONT     = 15, // Shooters only
+	SHOOT_DOWN      = 16, // Shooters only
+	SPECIAL_UP      = 17, // If empty, fallback to SPECIAL_FRONT
+	SPECIAL_FRONT   = 18, // Used for any special moves - dragon breath, spellcasting, Pit Lord/Ogre Mage ability
+	SPECIAL_DOWN    = 19, // If empty, fallback to SPECIAL_FRONT
+	MOVE_START      = 20, // small animation to be played before MOVING
+	MOVE_END        = 21, // small animation to be played after MOVING
+
+	DEAD            = 22, // new group, used to show dead stacks. If empty - last frame from "DEATH" will be copied here
+	DEAD_RANGED     = 23, // new group, used to show dead stacks (if DEATH_RANGED was used). If empty - last frame from "DEATH_RANGED" will be copied here
+	RESURRECTION    = 24, // new group, used for animating resurrection, if empty - reversed "DEATH" animation will be copied here
+	FROZEN          = 25, // new group, used when stack animation is paused (e.g. petrified). If empty - consist of first frame from HOLDING animation
+
+	CAST_UP            = 30,
+	CAST_FRONT         = 31,
+	CAST_DOWN          = 32,
+
+	GROUP_ATTACK_UP    = 40,
+	GROUP_ATTACK_FRONT = 41,
+	GROUP_ATTACK_DOWN  = 42
+};

+ 160 - 160
client/battle/BattleEffectsController.cpp

@@ -1,160 +1,160 @@
-/*
- * BattleEffectsController.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 "BattleEffectsController.h"
-
-#include "BattleAnimationClasses.h"
-#include "BattleWindow.h"
-#include "BattleInterface.h"
-#include "BattleInterfaceClasses.h"
-#include "BattleFieldController.h"
-#include "BattleStacksController.h"
-#include "BattleRenderer.h"
-
-#include "../CMusicHandler.h"
-#include "../CGameInfo.h"
-#include "../CPlayerInterface.h"
-#include "../render/Canvas.h"
-#include "../render/CAnimation.h"
-#include "../render/Graphics.h"
-
-#include "../../CCallback.h"
-#include "../../lib/battle/BattleAction.h"
-#include "../../lib/filesystem/ResourcePath.h"
-#include "../../lib/NetPacks.h"
-#include "../../lib/CStack.h"
-#include "../../lib/IGameEventsReceiver.h"
-#include "../../lib/CGeneralTextHandler.h"
-
-BattleEffectsController::BattleEffectsController(BattleInterface & owner):
-	owner(owner)
-{
-	loadColorMuxers();
-}
-
-void BattleEffectsController::displayEffect(EBattleEffect effect, const BattleHex & destTile)
-{
-	displayEffect(effect, AudioPath(), destTile);
-}
-
-void BattleEffectsController::displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile)
-{
-	size_t effectID = static_cast<size_t>(effect);
-
-	AnimationPath customAnim = AnimationPath::builtinTODO(graphics->battleACToDef[effectID][0]);
-
-	CCS->soundh->playSound( soundFile );
-
-	owner.stacksController->addNewAnim(new EffectAnimation(owner, customAnim, destTile));
-}
-
-void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bte)
-{
-	owner.checkForAnimations();
-
-	const CStack * stack = owner.getBattle()->battleGetStackByID(bte.stackID);
-	if(!stack)
-	{
-		logGlobal->error("Invalid stack ID %d", bte.stackID);
-		return;
-	}
-	//don't show animation when no HP is regenerated
-	switch(static_cast<BonusType>(bte.effect))
-	{
-		case BonusType::HP_REGENERATION:
-			displayEffect(EBattleEffect::REGENERATION, AudioPath::builtin("REGENER"), stack->getPosition());
-			break;
-		case BonusType::MANA_DRAIN:
-			displayEffect(EBattleEffect::MANA_DRAIN, AudioPath::builtin("MANADRAI"), stack->getPosition());
-			break;
-		case BonusType::POISON:
-			displayEffect(EBattleEffect::POISON, AudioPath::builtin("POISON"), stack->getPosition());
-			break;
-		case BonusType::FEAR:
-			displayEffect(EBattleEffect::FEAR, AudioPath::builtin("FEAR"), stack->getPosition());
-			break;
-		case BonusType::MORALE:
-		{
-			std::string hlp = CGI->generaltexth->allTexts[33];
-			boost::algorithm::replace_first(hlp,"%s",(stack->getName()));
-			displayEffect(EBattleEffect::GOOD_MORALE, AudioPath::builtin("GOODMRLE"), stack->getPosition());
-			owner.appendBattleLog(hlp);
-			break;
-		}
-		default:
-			return;
-	}
-	owner.waitForAnimations();
-}
-
-void BattleEffectsController::startAction(const BattleAction & action)
-{
-	owner.checkForAnimations();
-
-	const CStack *stack = owner.getBattle()->battleGetStackByID(action.stackNumber);
-
-	switch(action.actionType)
-	{
-	case EActionType::WAIT:
-		owner.appendBattleLog(stack->formatGeneralMessage(136));
-		break;
-	case EActionType::BAD_MORALE:
-		owner.appendBattleLog(stack->formatGeneralMessage(-34));
-		displayEffect(EBattleEffect::BAD_MORALE, AudioPath::builtin("BADMRLE"), stack->getPosition());
-		break;
-	}
-
-	owner.waitForAnimations();
-}
-
-void BattleEffectsController::collectRenderableObjects(BattleRenderer & renderer)
-{
-	for (auto & elem : battleEffects)
-	{
-		renderer.insert( EBattleFieldLayer::EFFECTS, elem.tile, [&elem](BattleRenderer::RendererRef canvas)
-		{
-			int currentFrame = static_cast<int>(floor(elem.currentFrame));
-			currentFrame %= elem.animation->size();
-
-			auto img = elem.animation->getImage(currentFrame, static_cast<size_t>(elem.type));
-
-			canvas.draw(img, elem.pos);
-		});
-	}
-}
-
-void BattleEffectsController::loadColorMuxers()
-{
-	const JsonNode config(JsonPath::builtin("config/battleEffects.json"));
-
-	for(auto & muxer : config["colorMuxers"].Struct())
-	{
-		ColorMuxerEffect effect;
-		std::string identifier = muxer.first;
-
-		for (const JsonNode & entry : muxer.second.Vector() )
-		{
-			effect.timePoints.push_back(entry["time"].Float());
-			effect.filters.push_back(ColorFilter::genFromJson(entry));
-		}
-		colorMuxerEffects[identifier] = effect;
-	}
-}
-
-const ColorMuxerEffect & BattleEffectsController::getMuxerEffect(const std::string & name)
-{
-	static const ColorMuxerEffect emptyEffect;
-
-	if (colorMuxerEffects.count(name))
-		return colorMuxerEffects[name];
-
-	logAnim->error("Failed to find color muxer effect named '%s'!", name);
-	return emptyEffect;
-}
+/*
+ * BattleEffectsController.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 "BattleEffectsController.h"
+
+#include "BattleAnimationClasses.h"
+#include "BattleWindow.h"
+#include "BattleInterface.h"
+#include "BattleInterfaceClasses.h"
+#include "BattleFieldController.h"
+#include "BattleStacksController.h"
+#include "BattleRenderer.h"
+
+#include "../CMusicHandler.h"
+#include "../CGameInfo.h"
+#include "../CPlayerInterface.h"
+#include "../render/Canvas.h"
+#include "../render/CAnimation.h"
+#include "../render/Graphics.h"
+
+#include "../../CCallback.h"
+#include "../../lib/battle/BattleAction.h"
+#include "../../lib/filesystem/ResourcePath.h"
+#include "../../lib/NetPacks.h"
+#include "../../lib/CStack.h"
+#include "../../lib/IGameEventsReceiver.h"
+#include "../../lib/CGeneralTextHandler.h"
+
+BattleEffectsController::BattleEffectsController(BattleInterface & owner):
+	owner(owner)
+{
+	loadColorMuxers();
+}
+
+void BattleEffectsController::displayEffect(EBattleEffect effect, const BattleHex & destTile)
+{
+	displayEffect(effect, AudioPath(), destTile);
+}
+
+void BattleEffectsController::displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile)
+{
+	size_t effectID = static_cast<size_t>(effect);
+
+	AnimationPath customAnim = AnimationPath::builtinTODO(graphics->battleACToDef[effectID][0]);
+
+	CCS->soundh->playSound( soundFile );
+
+	owner.stacksController->addNewAnim(new EffectAnimation(owner, customAnim, destTile));
+}
+
+void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bte)
+{
+	owner.checkForAnimations();
+
+	const CStack * stack = owner.getBattle()->battleGetStackByID(bte.stackID);
+	if(!stack)
+	{
+		logGlobal->error("Invalid stack ID %d", bte.stackID);
+		return;
+	}
+	//don't show animation when no HP is regenerated
+	switch(static_cast<BonusType>(bte.effect))
+	{
+		case BonusType::HP_REGENERATION:
+			displayEffect(EBattleEffect::REGENERATION, AudioPath::builtin("REGENER"), stack->getPosition());
+			break;
+		case BonusType::MANA_DRAIN:
+			displayEffect(EBattleEffect::MANA_DRAIN, AudioPath::builtin("MANADRAI"), stack->getPosition());
+			break;
+		case BonusType::POISON:
+			displayEffect(EBattleEffect::POISON, AudioPath::builtin("POISON"), stack->getPosition());
+			break;
+		case BonusType::FEAR:
+			displayEffect(EBattleEffect::FEAR, AudioPath::builtin("FEAR"), stack->getPosition());
+			break;
+		case BonusType::MORALE:
+		{
+			std::string hlp = CGI->generaltexth->allTexts[33];
+			boost::algorithm::replace_first(hlp,"%s",(stack->getName()));
+			displayEffect(EBattleEffect::GOOD_MORALE, AudioPath::builtin("GOODMRLE"), stack->getPosition());
+			owner.appendBattleLog(hlp);
+			break;
+		}
+		default:
+			return;
+	}
+	owner.waitForAnimations();
+}
+
+void BattleEffectsController::startAction(const BattleAction & action)
+{
+	owner.checkForAnimations();
+
+	const CStack *stack = owner.getBattle()->battleGetStackByID(action.stackNumber);
+
+	switch(action.actionType)
+	{
+	case EActionType::WAIT:
+		owner.appendBattleLog(stack->formatGeneralMessage(136));
+		break;
+	case EActionType::BAD_MORALE:
+		owner.appendBattleLog(stack->formatGeneralMessage(-34));
+		displayEffect(EBattleEffect::BAD_MORALE, AudioPath::builtin("BADMRLE"), stack->getPosition());
+		break;
+	}
+
+	owner.waitForAnimations();
+}
+
+void BattleEffectsController::collectRenderableObjects(BattleRenderer & renderer)
+{
+	for (auto & elem : battleEffects)
+	{
+		renderer.insert( EBattleFieldLayer::EFFECTS, elem.tile, [&elem](BattleRenderer::RendererRef canvas)
+		{
+			int currentFrame = static_cast<int>(floor(elem.currentFrame));
+			currentFrame %= elem.animation->size();
+
+			auto img = elem.animation->getImage(currentFrame, static_cast<size_t>(elem.type));
+
+			canvas.draw(img, elem.pos);
+		});
+	}
+}
+
+void BattleEffectsController::loadColorMuxers()
+{
+	const JsonNode config(JsonPath::builtin("config/battleEffects.json"));
+
+	for(auto & muxer : config["colorMuxers"].Struct())
+	{
+		ColorMuxerEffect effect;
+		std::string identifier = muxer.first;
+
+		for (const JsonNode & entry : muxer.second.Vector() )
+		{
+			effect.timePoints.push_back(entry["time"].Float());
+			effect.filters.push_back(ColorFilter::genFromJson(entry));
+		}
+		colorMuxerEffects[identifier] = effect;
+	}
+}
+
+const ColorMuxerEffect & BattleEffectsController::getMuxerEffect(const std::string & name)
+{
+	static const ColorMuxerEffect emptyEffect;
+
+	if (colorMuxerEffects.count(name))
+		return colorMuxerEffects[name];
+
+	logAnim->error("Failed to find color muxer effect named '%s'!", name);
+	return emptyEffect;
+}

+ 75 - 75
client/battle/BattleEffectsController.h

@@ -1,75 +1,75 @@
-/*
- * BattleEffectsController.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/battle/BattleHex.h"
-#include "../../lib/Point.h"
-#include "../../lib/filesystem/ResourcePath.h"
-#include "BattleConstants.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-class BattleAction;
-struct BattleTriggerEffect;
-
-VCMI_LIB_NAMESPACE_END
-
-struct ColorMuxerEffect;
-class CAnimation;
-class Canvas;
-class BattleInterface;
-class BattleRenderer;
-class EffectAnimation;
-
-/// Struct for battle effect animation e.g. morale, prayer, armageddon, bless,...
-struct BattleEffect
-{
-	enum class AnimType : ui8 
-	{
-		DEFAULT = 0, //If we have such animation
-		REVERSE = 1 //Reverse DEFAULT will be used
-	};
-
-	AnimType type;
-	Point pos; //position on the screen
-	float currentFrame;
-	std::shared_ptr<CAnimation> animation;
-	int effectID; //uniqueID equal ot ID of appropriate CSpellEffectAnim
-	BattleHex tile; //Indicates if effect which hex the effect is drawn on
-};
-
-/// Controls rendering of effects in battle, e.g. from spells, abilities and various other actions like morale
-class BattleEffectsController
-{
-	BattleInterface & owner;
-
-	/// list of current effects that are being displayed on screen (spells & creature abilities)
-	std::vector<BattleEffect> battleEffects;
-
-	std::map<std::string, ColorMuxerEffect> colorMuxerEffects;
-
-	void loadColorMuxers();
-public:
-	const ColorMuxerEffect &getMuxerEffect(const std::string & name);
-
-	BattleEffectsController(BattleInterface & owner);
-
-	void startAction(const BattleAction & action);
-
-	//displays custom effect on the battlefield
-	void displayEffect(EBattleEffect effect, const BattleHex & destTile);
-	void displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile);
-
-	void battleTriggerEffect(const BattleTriggerEffect & bte);
-
-	void collectRenderableObjects(BattleRenderer & renderer);
-
-	friend class EffectAnimation;
-};
+/*
+ * BattleEffectsController.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/battle/BattleHex.h"
+#include "../../lib/Point.h"
+#include "../../lib/filesystem/ResourcePath.h"
+#include "BattleConstants.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class BattleAction;
+struct BattleTriggerEffect;
+
+VCMI_LIB_NAMESPACE_END
+
+struct ColorMuxerEffect;
+class CAnimation;
+class Canvas;
+class BattleInterface;
+class BattleRenderer;
+class EffectAnimation;
+
+/// Struct for battle effect animation e.g. morale, prayer, armageddon, bless,...
+struct BattleEffect
+{
+	enum class AnimType : ui8 
+	{
+		DEFAULT = 0, //If we have such animation
+		REVERSE = 1 //Reverse DEFAULT will be used
+	};
+
+	AnimType type;
+	Point pos; //position on the screen
+	float currentFrame;
+	std::shared_ptr<CAnimation> animation;
+	int effectID; //uniqueID equal ot ID of appropriate CSpellEffectAnim
+	BattleHex tile; //Indicates if effect which hex the effect is drawn on
+};
+
+/// Controls rendering of effects in battle, e.g. from spells, abilities and various other actions like morale
+class BattleEffectsController
+{
+	BattleInterface & owner;
+
+	/// list of current effects that are being displayed on screen (spells & creature abilities)
+	std::vector<BattleEffect> battleEffects;
+
+	std::map<std::string, ColorMuxerEffect> colorMuxerEffects;
+
+	void loadColorMuxers();
+public:
+	const ColorMuxerEffect &getMuxerEffect(const std::string & name);
+
+	BattleEffectsController(BattleInterface & owner);
+
+	void startAction(const BattleAction & action);
+
+	//displays custom effect on the battlefield
+	void displayEffect(EBattleEffect effect, const BattleHex & destTile);
+	void displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile);
+
+	void battleTriggerEffect(const BattleTriggerEffect & bte);
+
+	void collectRenderableObjects(BattleRenderer & renderer);
+
+	friend class EffectAnimation;
+};

+ 919 - 919
client/battle/BattleFieldController.cpp

@@ -1,919 +1,919 @@
-/*
- * BattleFieldController.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 "BattleFieldController.h"
-
-#include "BattleInterface.h"
-#include "BattleActionsController.h"
-#include "BattleInterfaceClasses.h"
-#include "BattleEffectsController.h"
-#include "BattleSiegeController.h"
-#include "BattleStacksController.h"
-#include "BattleObstacleController.h"
-#include "BattleProjectileController.h"
-#include "BattleRenderer.h"
-
-#include "../CGameInfo.h"
-#include "../CPlayerInterface.h"
-#include "../render/CAnimation.h"
-#include "../render/Canvas.h"
-#include "../render/IImage.h"
-#include "../renderSDL/SDL_Extensions.h"
-#include "../render/IRenderHandler.h"
-#include "../gui/CGuiHandler.h"
-#include "../gui/CursorHandler.h"
-#include "../adventureMap/CInGameConsole.h"
-#include "../client/render/CAnimation.h"
-
-#include "../../CCallback.h"
-#include "../../lib/BattleFieldHandler.h"
-#include "../../lib/CConfigHandler.h"
-#include "../../lib/CStack.h"
-#include "../../lib/spells/ISpellMechanics.h"
-
-namespace HexMasks
-{
-	// mask definitions that has set to 1 the edges present in the hex edges highlight image
-	/*
-	    /\
-	   0  1
-	  /    \
-	 |      |
-	 5      2
-	 |      |
-	  \    /
-	   4  3
-	    \/
-	*/
-	enum HexEdgeMasks {
-		empty                 = 0b000000, // empty used when wanting to keep indexes the same but no highlight should be displayed
-		topLeft               = 0b000001,
-		topRight              = 0b000010,
-		right                 = 0b000100,
-		bottomRight           = 0b001000,
-		bottomLeft            = 0b010000,
-		left                  = 0b100000,
-						  
-		top                   = 0b000011,
-		bottom                = 0b011000,
-		topRightHalfCorner    = 0b000110,
-		bottomRightHalfCorner = 0b001100,
-		bottomLeftHalfCorner  = 0b110000,
-		topLeftHalfCorner     = 0b100001,
-
-		rightTopAndBottom     = 0b001010, // special case, right half can be drawn instead of only top and bottom
-		leftTopAndBottom      = 0b010001, // special case, left half can be drawn instead of only top and bottom
-						  
-		rightHalf             = 0b001110,
-		leftHalf              = 0b110001,
-						  
-		topRightCorner        = 0b000111,
-		bottomRightCorner     = 0b011100,
-		bottomLeftCorner      = 0b111000,
-		topLeftCorner         = 0b100011
-	};
-}
-
-std::map<int, int> hexEdgeMaskToFrameIndex;
-
-// Maps HexEdgesMask to "Frame" indexes for range highligt images
-void initializeHexEdgeMaskToFrameIndex()
-{
-	hexEdgeMaskToFrameIndex[HexMasks::empty] = 0;
-
-    hexEdgeMaskToFrameIndex[HexMasks::topLeft] = 1;
-    hexEdgeMaskToFrameIndex[HexMasks::topRight] = 2;
-    hexEdgeMaskToFrameIndex[HexMasks::right] = 3;
-    hexEdgeMaskToFrameIndex[HexMasks::bottomRight] = 4;
-    hexEdgeMaskToFrameIndex[HexMasks::bottomLeft] = 5;
-    hexEdgeMaskToFrameIndex[HexMasks::left] = 6;
-
-    hexEdgeMaskToFrameIndex[HexMasks::top] = 7;
-    hexEdgeMaskToFrameIndex[HexMasks::bottom] = 8;
-
-    hexEdgeMaskToFrameIndex[HexMasks::topRightHalfCorner] = 9;
-    hexEdgeMaskToFrameIndex[HexMasks::bottomRightHalfCorner] = 10;
-    hexEdgeMaskToFrameIndex[HexMasks::bottomLeftHalfCorner] = 11;
-    hexEdgeMaskToFrameIndex[HexMasks::topLeftHalfCorner] = 12;
-
-    hexEdgeMaskToFrameIndex[HexMasks::rightTopAndBottom] = 13;
-    hexEdgeMaskToFrameIndex[HexMasks::leftTopAndBottom] = 14;
-	
-    hexEdgeMaskToFrameIndex[HexMasks::rightHalf] = 13;
-    hexEdgeMaskToFrameIndex[HexMasks::leftHalf] = 14;
-
-    hexEdgeMaskToFrameIndex[HexMasks::topRightCorner] = 15;
-    hexEdgeMaskToFrameIndex[HexMasks::bottomRightCorner] = 16;
-    hexEdgeMaskToFrameIndex[HexMasks::bottomLeftCorner] = 17;
-    hexEdgeMaskToFrameIndex[HexMasks::topLeftCorner] = 18;
-}
-
-BattleFieldController::BattleFieldController(BattleInterface & owner):
-	owner(owner)
-{
-	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
-
-	//preparing cells and hexes
-	cellBorder = GH.renderHandler().loadImage(ImagePath::builtin("CCELLGRD.BMP"), EImageBlitMode::COLORKEY);
-	cellShade = GH.renderHandler().loadImage(ImagePath::builtin("CCELLSHD.BMP"));
-	cellUnitMovementHighlight = GH.renderHandler().loadImage(ImagePath::builtin("UnitMovementHighlight.PNG"), EImageBlitMode::COLORKEY);
-	cellUnitMaxMovementHighlight = GH.renderHandler().loadImage(ImagePath::builtin("UnitMaxMovementHighlight.PNG"), EImageBlitMode::COLORKEY);
-
-	attackCursors = GH.renderHandler().loadAnimation(AnimationPath::builtin("CRCOMBAT"));
-	attackCursors->preload();
-
-	spellCursors = GH.renderHandler().loadAnimation(AnimationPath::builtin("CRSPELL"));
-	spellCursors->preload();
-
-	initializeHexEdgeMaskToFrameIndex();
-
-	rangedFullDamageLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsGreen.json"));
-	rangedFullDamageLimitImages->preload();
-
-	shootingRangeLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsRed.json"));
-	shootingRangeLimitImages->preload();
-
-	flipRangeLimitImagesIntoPositions(rangedFullDamageLimitImages);
-	flipRangeLimitImagesIntoPositions(shootingRangeLimitImages);
-
-	if(!owner.siegeController)
-	{
-		auto bfieldType = owner.getBattle()->battleGetBattlefieldType();
-
-		if(bfieldType == BattleField::NONE)
-			logGlobal->error("Invalid battlefield returned for current battle");
-		else
-			background = GH.renderHandler().loadImage(bfieldType.getInfo()->graphics, EImageBlitMode::OPAQUE);
-	}
-	else
-	{
-		auto backgroundName = owner.siegeController->getBattleBackgroundName();
-		background = GH.renderHandler().loadImage(backgroundName, EImageBlitMode::OPAQUE);
-	}
-
-	pos.w = background->width();
-	pos.h = background->height();
-
-	backgroundWithHexes = std::make_unique<Canvas>(Point(background->width(), background->height()));
-
-	updateAccessibleHexes();
-	addUsedEvents(LCLICK | SHOW_POPUP | MOVE | TIME | GESTURE);
-}
-
-void BattleFieldController::activate()
-{
-	LOCPLINT->cingconsole->pos = this->pos;
-	CIntObject::activate();
-}
-
-void BattleFieldController::createHeroes()
-{
-	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
-
-	// create heroes as part of our constructor for correct positioning inside battlefield
-	if(owner.attackingHeroInstance)
-		owner.attackingHero = std::make_shared<BattleHero>(owner, owner.attackingHeroInstance, false);
-
-	if(owner.defendingHeroInstance)
-		owner.defendingHero = std::make_shared<BattleHero>(owner, owner.defendingHeroInstance, true);
-}
-
-void BattleFieldController::gesture(bool on, const Point & initialPosition, const Point & finalPosition)
-{
-	if (!on && pos.isInside(finalPosition))
-		clickPressed(finalPosition);
-}
-
-void BattleFieldController::gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance)
-{
-	Point distance = currentPosition - initialPosition;
-
-	if (distance.length() < settings["battle"]["swipeAttackDistance"].Float())
-		hoveredHex = getHexAtPosition(initialPosition);
-	else
-		hoveredHex = BattleHex::INVALID;
-
-	currentAttackOriginPoint = currentPosition;
-
-	if (pos.isInside(initialPosition))
-		owner.actionsController->onHexHovered(getHoveredHex());
-}
-
-void BattleFieldController::mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance)
-{
-	hoveredHex = getHexAtPosition(cursorPosition);
-	currentAttackOriginPoint = cursorPosition;
-
-	if (pos.isInside(cursorPosition))
-		owner.actionsController->onHexHovered(getHoveredHex());
-	else
-		owner.actionsController->onHoverEnded();
-}
-
-void BattleFieldController::clickPressed(const Point & cursorPosition)
-{
-	BattleHex selectedHex = getHoveredHex();
-
-	if (selectedHex != BattleHex::INVALID)
-		owner.actionsController->onHexLeftClicked(selectedHex);
-}
-
-void BattleFieldController::showPopupWindow(const Point & cursorPosition)
-{
-	BattleHex selectedHex = getHoveredHex();
-
-	if (selectedHex != BattleHex::INVALID)
-		owner.actionsController->onHexRightClicked(selectedHex);
-}
-
-void BattleFieldController::renderBattlefield(Canvas & canvas)
-{
-	Canvas clippedCanvas(canvas, pos);
-
-	showBackground(clippedCanvas);
-
-	BattleRenderer renderer(owner);
-
-	renderer.execute(clippedCanvas);
-
-	owner.projectilesController->render(clippedCanvas);
-}
-
-void BattleFieldController::showBackground(Canvas & canvas)
-{
-	if (owner.stacksController->getActiveStack() != nullptr )
-		showBackgroundImageWithHexes(canvas);
-	else
-		showBackgroundImage(canvas);
-
-	showHighlightedHexes(canvas);
-}
-
-void BattleFieldController::showBackgroundImage(Canvas & canvas)
-{
-	canvas.draw(background, Point(0, 0));
-
-	owner.obstacleController->showAbsoluteObstacles(canvas);
-	if ( owner.siegeController )
-		owner.siegeController->showAbsoluteObstacles(canvas);
-
-	if (settings["battle"]["cellBorders"].Bool())
-	{
-		for (int i=0; i<GameConstants::BFIELD_SIZE; ++i)
-		{
-			if ( i % GameConstants::BFIELD_WIDTH == 0)
-				continue;
-			if ( i % GameConstants::BFIELD_WIDTH == GameConstants::BFIELD_WIDTH - 1)
-				continue;
-
-			canvas.draw(cellBorder, hexPositionLocal(i).topLeft());
-		}
-	}
-}
-
-void BattleFieldController::showBackgroundImageWithHexes(Canvas & canvas)
-{
-	canvas.draw(*backgroundWithHexes, Point(0, 0));
-}
-
-void BattleFieldController::redrawBackgroundWithHexes()
-{
-	const CStack *activeStack = owner.stacksController->getActiveStack();
-	std::vector<BattleHex> attackableHexes;
-	if(activeStack)
-		occupiableHexes = owner.getBattle()->battleGetAvailableHexes(activeStack, false, true, &attackableHexes);
-
-	// prepare background graphic with hexes and shaded hexes
-	backgroundWithHexes->draw(background, Point(0,0));
-	owner.obstacleController->showAbsoluteObstacles(*backgroundWithHexes);
-	if(owner.siegeController)
-		owner.siegeController->showAbsoluteObstacles(*backgroundWithHexes);
-
-	// show shaded hexes for active's stack valid movement and the hexes that it can attack
-	if(settings["battle"]["stackRange"].Bool())
-	{
-		std::vector<BattleHex> hexesToShade = occupiableHexes;
-		hexesToShade.insert(hexesToShade.end(), attackableHexes.begin(), attackableHexes.end());
-		for(BattleHex hex : hexesToShade)
-		{
-			showHighlightedHex(*backgroundWithHexes, cellShade, hex, false);
-		}
-	}
-
-	// draw cell borders
-	if(settings["battle"]["cellBorders"].Bool())
-	{
-		for(int i=0; i<GameConstants::BFIELD_SIZE; ++i)
-		{
-			if(i % GameConstants::BFIELD_WIDTH == 0)
-				continue;
-			if(i % GameConstants::BFIELD_WIDTH == GameConstants::BFIELD_WIDTH - 1)
-				continue;
-
-			backgroundWithHexes->draw(cellBorder, hexPositionLocal(i).topLeft());
-		}
-	}
-}
-
-void BattleFieldController::showHighlightedHex(Canvas & canvas, std::shared_ptr<IImage> highlight, BattleHex hex, bool darkBorder)
-{
-	Point hexPos = hexPositionLocal(hex).topLeft();
-
-	canvas.draw(highlight, hexPos);
-	if(!darkBorder && settings["battle"]["cellBorders"].Bool())
-		canvas.draw(cellBorder, hexPos);
-}
-
-std::set<BattleHex> BattleFieldController::getHighlightedHexesForActiveStack()
-{
-	std::set<BattleHex> result;
-
-	if(!owner.stacksController->getActiveStack())
-		return result;
-
-	if(!settings["battle"]["stackRange"].Bool())
-		return result;
-
-	auto hoveredHex = getHoveredHex();
-
-	std::set<BattleHex> set = owner.getBattle()->battleGetAttackedHexes(owner.stacksController->getActiveStack(), hoveredHex);
-	for(BattleHex hex : set)
-		result.insert(hex);
-
-	return result;
-}
-
-std::set<BattleHex> BattleFieldController::getMovementRangeForHoveredStack()
-{
-	std::set<BattleHex> result;
-
-	if (!owner.stacksController->getActiveStack())
-		return result;
-
-	if (!settings["battle"]["movementHighlightOnHover"].Bool() && !GH.isKeyboardShiftDown())
-		return result;
-
-	auto hoveredHex = getHoveredHex();
-
-	// add possible movement hexes for stack under mouse
-	const CStack * const hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true);
-	if(hoveredStack)
-	{
-		std::vector<BattleHex> v = owner.getBattle()->battleGetAvailableHexes(hoveredStack, true, true, nullptr);
-		for(BattleHex hex : v)
-			result.insert(hex);
-	}
-	return result;
-}
-
-std::set<BattleHex> BattleFieldController::getHighlightedHexesForSpellRange()
-{
-	std::set<BattleHex> result;
-	auto hoveredHex = getHoveredHex();
-
-	if(!settings["battle"]["mouseShadow"].Bool())
-		return result;
-
-	const spells::Caster *caster = nullptr;
-	const CSpell *spell = nullptr;
-
-	spells::Mode mode = owner.actionsController->getCurrentCastMode();
-	spell = owner.actionsController->getCurrentSpell(hoveredHex);
-	caster = owner.actionsController->getCurrentSpellcaster();
-
-	if(caster && spell) //when casting spell
-	{
-		// printing shaded hex(es)
-		spells::BattleCast event(owner.getBattle().get(), caster, mode, spell);
-		auto shadedHexes = spell->battleMechanics(&event)->rangeInHexes(hoveredHex);
-
-		for(BattleHex shadedHex : shadedHexes)
-		{
-			if((shadedHex.getX() != 0) && (shadedHex.getX() != GameConstants::BFIELD_WIDTH - 1))
-				result.insert(shadedHex);
-		}
-	}
-	return result;
-}
-
-std::set<BattleHex> BattleFieldController::getHighlightedHexesForMovementTarget()
-{
-	const CStack * stack = owner.stacksController->getActiveStack();
-	auto hoveredHex = getHoveredHex();
-
-	if(!stack)
-		return {};
-
-	std::vector<BattleHex> availableHexes = owner.getBattle()->battleGetAvailableHexes(stack, false, false, nullptr);
-
-	auto hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true);
-	if(owner.getBattle()->battleCanAttack(stack, hoveredStack, hoveredHex))
-	{
-		if(isTileAttackable(hoveredHex))
-		{
-			BattleHex attackFromHex = fromWhichHexAttack(hoveredHex);
-
-			if(stack->doubleWide())
-				return {attackFromHex, stack->occupiedHex(attackFromHex)};
-			else
-				return {attackFromHex};
-		}
-	}
-
-	if(vstd::contains(availableHexes, hoveredHex))
-	{
-		if(stack->doubleWide())
-			return {hoveredHex, stack->occupiedHex(hoveredHex)};
-		else
-			return {hoveredHex};
-	}
-
-	if(stack->doubleWide())
-	{
-		for(auto const & hex : availableHexes)
-		{
-			if(stack->occupiedHex(hex) == hoveredHex)
-				return {hoveredHex, hex};
-		}
-	}
-
-	return {};
-}
-
-// Range limit highlight helpers
-
-std::vector<BattleHex> BattleFieldController::getRangeHexes(BattleHex sourceHex, uint8_t distance)
-{
-	std::vector<BattleHex> rangeHexes;
-
-	if (!settings["battle"]["rangeLimitHighlightOnHover"].Bool() && !GH.isKeyboardShiftDown())
-		return rangeHexes;
-
-	// get only battlefield hexes that are within the given distance
-	for(auto i = 0; i < GameConstants::BFIELD_SIZE; i++)
-	{
-		BattleHex hex(i);
-		if(hex.isAvailable() && BattleHex::getDistance(sourceHex, hex) <= distance)
-			rangeHexes.push_back(hex);
-	}
-
-	return rangeHexes;
-}
-
-std::vector<BattleHex> BattleFieldController::getRangeLimitHexes(BattleHex hoveredHex, std::vector<BattleHex> rangeHexes, uint8_t distanceToLimit)
-{
-	std::vector<BattleHex> rangeLimitHexes;
-
-	// from range hexes get only the ones at the limit
-	for(auto & hex : rangeHexes)
-	{
-		if(BattleHex::getDistance(hoveredHex, hex) == distanceToLimit)
-			rangeLimitHexes.push_back(hex);
-	}
-
-	return rangeLimitHexes;
-}
-
-bool BattleFieldController::IsHexInRangeLimit(BattleHex hex, std::vector<BattleHex> & rangeLimitHexes, int * hexIndexInRangeLimit)
-{
-	bool  hexInRangeLimit = false;
-
-	if(!rangeLimitHexes.empty())
-	{
-		auto pos = std::find(rangeLimitHexes.begin(), rangeLimitHexes.end(), hex);
-		*hexIndexInRangeLimit = std::distance(rangeLimitHexes.begin(), pos);
-		hexInRangeLimit = pos != rangeLimitHexes.end();
-	}
-
-	return hexInRangeLimit;
-}
-
-std::vector<std::vector<BattleHex::EDir>> BattleFieldController::getOutsideNeighbourDirectionsForLimitHexes(std::vector<BattleHex> wholeRangeHexes, std::vector<BattleHex> rangeLimitHexes)
-{
-	std::vector<std::vector<BattleHex::EDir>> output;
-
-	if(wholeRangeHexes.empty())
-		return output;
-
-	for(auto & hex : rangeLimitHexes)
-	{
-		// get all neighbours and their directions
-		
-		auto neighbouringTiles = hex.allNeighbouringTiles();
-
-		std::vector<BattleHex::EDir> outsideNeighbourDirections;
-
-		// for each neighbour add to output only the valid ones and only that are not found in range Hexes
-		for(auto direction = 0; direction < 6; direction++)
-		{
-			if(!neighbouringTiles[direction].isAvailable())
-				continue;
-
-			auto it = std::find(wholeRangeHexes.begin(), wholeRangeHexes.end(), neighbouringTiles[direction]);
-
-			if(it == wholeRangeHexes.end())
-				outsideNeighbourDirections.push_back(BattleHex::EDir(direction)); // push direction
-		}
-
-		output.push_back(outsideNeighbourDirections);
-	}
-
-	return output;
-}
-
-std::vector<std::shared_ptr<IImage>> BattleFieldController::calculateRangeLimitHighlightImages(std::vector<std::vector<BattleHex::EDir>> hexesNeighbourDirections, std::shared_ptr<CAnimation> limitImages)
-{
-	std::vector<std::shared_ptr<IImage>> output; // if no image is to be shown an empty image is still added to help with traverssing the range
-
-	if(hexesNeighbourDirections.empty())
-		return output;
-
-	for(auto & directions : hexesNeighbourDirections)
-	{
-		std::bitset<6> mask;
-		
-		// convert directions to mask
-		for(auto direction : directions)
-			mask.set(direction);
-
-		uint8_t imageKey = static_cast<uint8_t>(mask.to_ulong());
-		output.push_back(limitImages->getImage(hexEdgeMaskToFrameIndex[imageKey]));
-	}
-
-	return output;
-}
-
-void BattleFieldController::calculateRangeLimitAndHighlightImages(uint8_t distance, std::shared_ptr<CAnimation> rangeLimitImages, std::vector<BattleHex> & rangeLimitHexes, std::vector<std::shared_ptr<IImage>> & rangeLimitHexesHighligts)
-{
-		std::vector<BattleHex> rangeHexes = getRangeHexes(hoveredHex, distance);
-		rangeLimitHexes = getRangeLimitHexes(hoveredHex, rangeHexes, distance);
-		std::vector<std::vector<BattleHex::EDir>> rangeLimitNeighbourDirections = getOutsideNeighbourDirectionsForLimitHexes(rangeHexes, rangeLimitHexes);
-		rangeLimitHexesHighligts = calculateRangeLimitHighlightImages(rangeLimitNeighbourDirections, rangeLimitImages);
-}
-
-void BattleFieldController::flipRangeLimitImagesIntoPositions(std::shared_ptr<CAnimation> images)
-{
-	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRight])->verticalFlip();
-	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::right])->verticalFlip();
-	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRight])->doubleFlip();
-	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeft])->horizontalFlip();
-
-	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottom])->horizontalFlip();
-
-	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRightHalfCorner])->verticalFlip();
-	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRightHalfCorner])->doubleFlip();
-	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeftHalfCorner])->horizontalFlip();
-
-	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::rightHalf])->verticalFlip();
-
-	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRightCorner])->verticalFlip();
-	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRightCorner])->doubleFlip();
-	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeftCorner])->horizontalFlip();
-}
-
-void BattleFieldController::showHighlightedHexes(Canvas & canvas)
-{
-	std::vector<BattleHex> rangedFullDamageLimitHexes;
-	std::vector<BattleHex> shootingRangeLimitHexes;
-
-	std::vector<std::shared_ptr<IImage>> rangedFullDamageLimitHexesHighligts;
-	std::vector<std::shared_ptr<IImage>> shootingRangeLimitHexesHighligts;
-
-	std::set<BattleHex> hoveredStackMovementRangeHexes = getMovementRangeForHoveredStack();
-	std::set<BattleHex> hoveredSpellHexes = getHighlightedHexesForSpellRange();
-	std::set<BattleHex> hoveredMoveHexes  = getHighlightedHexesForMovementTarget();
-
-	BattleHex hoveredHex = getHoveredHex();
-	if(hoveredHex == BattleHex::INVALID)
-		return;
-
-	const CStack * hoveredStack = getHoveredStack();
-
-	// skip range limit calculations if unit hovered is not a shooter
-	if(hoveredStack && hoveredStack->isShooter())
-	{
-		// calculate array with highlight images for ranged full damage limit
-		auto rangedFullDamageDistance = hoveredStack->getRangedFullDamageDistance();
-		calculateRangeLimitAndHighlightImages(rangedFullDamageDistance, rangedFullDamageLimitImages, rangedFullDamageLimitHexes, rangedFullDamageLimitHexesHighligts);
-
-		// calculate array with highlight images for shooting range limit
-		auto shootingRangeDistance = hoveredStack->getShootingRangeDistance();
-		calculateRangeLimitAndHighlightImages(shootingRangeDistance, shootingRangeLimitImages, shootingRangeLimitHexes, shootingRangeLimitHexesHighligts);
-	}
-
-	auto const & hoveredMouseHexes = owner.actionsController->currentActionSpellcasting(getHoveredHex()) ? hoveredSpellHexes : hoveredMoveHexes;
-
-	for(int hex = 0; hex < GameConstants::BFIELD_SIZE; ++hex)
-	{
-		bool stackMovement = hoveredStackMovementRangeHexes.count(hex);
-		bool mouse = hoveredMouseHexes.count(hex);
-
-		// calculate if hex is Ranged Full Damage Limit and its position in highlight array
-		int hexIndexInRangedFullDamageLimit = 0;
-		bool hexInRangedFullDamageLimit = IsHexInRangeLimit(hex, rangedFullDamageLimitHexes, &hexIndexInRangedFullDamageLimit);
-
-		// calculate if hex is Shooting Range Limit and its position in highlight array
-		int hexIndexInShootingRangeLimit = 0;
-		bool hexInShootingRangeLimit = IsHexInRangeLimit(hex, shootingRangeLimitHexes, &hexIndexInShootingRangeLimit);
-
-		if(stackMovement && mouse) // area where hovered stackMovement can move shown with highlight. Because also affected by mouse cursor, shade as well
-		{
-			showHighlightedHex(canvas, cellUnitMovementHighlight, hex, false);
-			showHighlightedHex(canvas, cellShade, hex, true);
-		}
-		if(!stackMovement && mouse) // hexes affected only at mouse cursor shown as shaded
-		{
-			showHighlightedHex(canvas, cellShade, hex, true);
-		}
-		if(stackMovement && !mouse) // hexes where hovered stackMovement can move shown with highlight
-		{
-			showHighlightedHex(canvas, cellUnitMovementHighlight, hex, false);
-		}
-		if(hexInRangedFullDamageLimit)
-		{
-			showHighlightedHex(canvas, rangedFullDamageLimitHexesHighligts[hexIndexInRangedFullDamageLimit], hex, false);
-		}
-		if(hexInShootingRangeLimit)
-		{
-			showHighlightedHex(canvas, shootingRangeLimitHexesHighligts[hexIndexInShootingRangeLimit], hex, false);
-		}
-	}
-}
-
-Rect BattleFieldController::hexPositionLocal(BattleHex hex) const
-{
-	int x = 14 + ((hex.getY())%2==0 ? 22 : 0) + 44*hex.getX();
-	int y = 86 + 42 *hex.getY();
-	int w = cellShade->width();
-	int h = cellShade->height();
-	return Rect(x, y, w, h);
-}
-
-Rect BattleFieldController::hexPositionAbsolute(BattleHex hex) const
-{
-	return hexPositionLocal(hex) + pos.topLeft();
-}
-
-bool BattleFieldController::isPixelInHex(Point const & position)
-{
-	return !cellShade->isTransparent(position);
-}
-
-BattleHex BattleFieldController::getHoveredHex()
-{
-	return hoveredHex;
-}
-
-const CStack* BattleFieldController::getHoveredStack()
-{
-	auto hoveredHex = getHoveredHex();
-	const CStack* hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true);
-
-	return hoveredStack;
-}
-
-BattleHex BattleFieldController::getHexAtPosition(Point hoverPos)
-{
-	if (owner.attackingHero)
-	{
-		if (owner.attackingHero->pos.isInside(hoverPos))
-			return BattleHex::HERO_ATTACKER;
-	}
-
-	if (owner.defendingHero)
-	{
-		if (owner.attackingHero->pos.isInside(hoverPos))
-			return BattleHex::HERO_DEFENDER;
-	}
-
-	for (int h = 0; h < GameConstants::BFIELD_SIZE; ++h)
-	{
-		Rect hexPosition = hexPositionAbsolute(h);
-
-		if (!hexPosition.isInside(hoverPos))
-			continue;
-
-		if (isPixelInHex(hoverPos - hexPosition.topLeft()))
-			return h;
-	}
-
-	return BattleHex::INVALID;
-}
-
-BattleHex::EDir BattleFieldController::selectAttackDirection(BattleHex myNumber)
-{
-	const bool doubleWide = owner.stacksController->getActiveStack()->doubleWide();
-	auto neighbours = myNumber.allNeighbouringTiles();
-	//   0 1
-	//  5 x 2
-	//   4 3
-
-	// if true - our current stack can move into this hex (and attack)
-	std::array<bool, 8> attackAvailability;
-
-	if (doubleWide)
-	{
-		// For double-hexes we need to ensure that both hexes needed for this direction are occupyable:
-		// |    -0-   |   -1-    |    -2-   |   -3-    |    -4-   |   -5-    |    -6-   |   -7-
-		// |  o o -   |   - o o  |    - -   |   - -    |    - -   |   - -    |    o o   |   - -
-		// |   - x -  |  - x -   |   - x o o|  - x -   |   - x -  |o o x -   |   - x -  |  - x -
-		// |    - -   |   - -    |    - -   |   - o o  |  o o -   |   - -    |    - -   |   o o
-
-		for (size_t i : { 1, 2, 3})
-			attackAvailability[i] = vstd::contains(occupiableHexes, neighbours[i]) && vstd::contains(occupiableHexes, neighbours[i].cloneInDirection(BattleHex::RIGHT, false));
-
-		for (size_t i : { 4, 5, 0})
-			attackAvailability[i] = vstd::contains(occupiableHexes, neighbours[i]) && vstd::contains(occupiableHexes, neighbours[i].cloneInDirection(BattleHex::LEFT, false));
-
-		attackAvailability[6] = vstd::contains(occupiableHexes, neighbours[0]) && vstd::contains(occupiableHexes, neighbours[1]);
-		attackAvailability[7] = vstd::contains(occupiableHexes, neighbours[3]) && vstd::contains(occupiableHexes, neighbours[4]);
-	}
-	else
-	{
-		for (size_t i = 0; i < 6; ++i)
-			attackAvailability[i] = vstd::contains(occupiableHexes, neighbours[i]);
-
-		attackAvailability[6] = false;
-		attackAvailability[7] = false;
-	}
-
-	// Zero available tiles to attack from
-	if ( vstd::find(attackAvailability, true) == attackAvailability.end())
-	{
-		logGlobal->error("Error: cannot find a hex to attack hex %d from!", myNumber);
-		return BattleHex::NONE;
-	}
-
-	// For each valid direction, select position to test against
-	std::array<Point, 8> testPoint;
-
-	for (size_t i = 0; i < 6; ++i)
-		if (attackAvailability[i])
-			testPoint[i] = hexPositionAbsolute(neighbours[i]).center();
-
-	// For bottom/top directions select central point, but move it a bit away from true center to reduce zones allocated to them
-	if (attackAvailability[6])
-		testPoint[6] = (hexPositionAbsolute(neighbours[0]).center() + hexPositionAbsolute(neighbours[1]).center()) / 2 + Point(0, -5);
-
-	if (attackAvailability[7])
-		testPoint[7] = (hexPositionAbsolute(neighbours[3]).center() + hexPositionAbsolute(neighbours[4]).center()) / 2 + Point(0,  5);
-
-	// Compute distance between tested position & cursor position and pick nearest
-	std::array<int, 8> distance2;
-
-	for (size_t i = 0; i < 8; ++i)
-		if (attackAvailability[i])
-			distance2[i] = (testPoint[i].y - currentAttackOriginPoint.y)*(testPoint[i].y - currentAttackOriginPoint.y) + (testPoint[i].x - currentAttackOriginPoint.x)*(testPoint[i].x - currentAttackOriginPoint.x);
-
-	size_t nearest = -1;
-	for (size_t i = 0; i < 8; ++i)
-		if (attackAvailability[i] && (nearest == -1 || distance2[i] < distance2[nearest]) )
-			nearest = i;
-
-	assert(nearest != -1);
-	return BattleHex::EDir(nearest);
-}
-
-BattleHex BattleFieldController::fromWhichHexAttack(BattleHex attackTarget)
-{
-	BattleHex::EDir direction = selectAttackDirection(getHoveredHex());
-
-	const CStack * attacker = owner.stacksController->getActiveStack();
-
-	assert(direction != BattleHex::NONE);
-	assert(attacker);
-
-	if (!attacker->doubleWide())
-	{
-		assert(direction != BattleHex::BOTTOM);
-		assert(direction != BattleHex::TOP);
-		return attackTarget.cloneInDirection(direction);
-	}
-	else
-	{
-		// We need to find position of right hex of double-hex creature (or left for defending side)
-		// | TOP_LEFT |TOP_RIGHT |   RIGHT  |BOTTOM_RIGHT|BOTTOM_LEFT|  LEFT    |    TOP   |BOTTOM
-		// |  o o -   |   - o o  |    - -   |   - -      |    - -    |   - -    |    o o   |   - -
-		// |   - x -  |  - x -   |   - x o o|  - x -     |   - x -   |o o x -   |   - x -  |  - x -
-		// |    - -   |   - -    |    - -   |   - o o    |  o o -    |   - -    |    - -   |   o o
-
-		switch (direction)
-		{
-		case BattleHex::TOP_LEFT:
-		case BattleHex::LEFT:
-		case BattleHex::BOTTOM_LEFT:
-		{
-			if ( attacker->unitSide() == BattleSide::ATTACKER )
-				return attackTarget.cloneInDirection(direction);
-			else
-				return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::LEFT);
-		}
-
-		case BattleHex::TOP_RIGHT:
-		case BattleHex::RIGHT:
-		case BattleHex::BOTTOM_RIGHT:
-		{
-			if ( attacker->unitSide() == BattleSide::ATTACKER )
-				return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::RIGHT);
-			else
-				return attackTarget.cloneInDirection(direction);
-		}
-
-		case BattleHex::TOP:
-		{
-			if ( attacker->unitSide() == BattleSide::ATTACKER )
-				return attackTarget.cloneInDirection(BattleHex::TOP_RIGHT);
-			else
-				return attackTarget.cloneInDirection(BattleHex::TOP_LEFT);
-		}
-
-		case BattleHex::BOTTOM:
-		{
-			if ( attacker->unitSide() == BattleSide::ATTACKER )
-				return attackTarget.cloneInDirection(BattleHex::BOTTOM_RIGHT);
-			else
-				return attackTarget.cloneInDirection(BattleHex::BOTTOM_LEFT);
-		}
-		default:
-			assert(0);
-			return BattleHex::INVALID;
-		}
-	}
-}
-
-bool BattleFieldController::isTileAttackable(const BattleHex & number) const
-{
-	for (auto & elem : occupiableHexes)
-	{
-		if (BattleHex::mutualPosition(elem, number) != -1 || elem == number)
-			return true;
-	}
-	return false;
-}
-
-void BattleFieldController::updateAccessibleHexes()
-{
-	auto accessibility = owner.getBattle()->getAccesibility();
-
-	for(int i = 0; i < accessibility.size(); i++)
-		stackCountOutsideHexes[i] = (accessibility[i] == EAccessibility::ACCESSIBLE || (accessibility[i] == EAccessibility::SIDE_COLUMN));
-}
-
-bool BattleFieldController::stackCountOutsideHex(const BattleHex & number) const
-{
-	return stackCountOutsideHexes[number];
-}
-
-void BattleFieldController::showAll(Canvas & to)
-{
-	show(to);
-}
-
-void BattleFieldController::tick(uint32_t msPassed)
-{
-	updateAccessibleHexes();
-	owner.stacksController->tick(msPassed);
-	owner.obstacleController->tick(msPassed);
-	owner.projectilesController->tick(msPassed);
-}
-
-void BattleFieldController::show(Canvas & to)
-{
-	CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos);
-
-	renderBattlefield(to);
-
-	if (isActive() && isGesturing() && getHoveredHex() != BattleHex::INVALID)
-	{
-		auto combatCursorIndex = CCS->curh->get<Cursor::Combat>();
-		if (combatCursorIndex)
-		{
-			auto combatImageIndex = static_cast<size_t>(*combatCursorIndex);
-			to.draw(attackCursors->getImage(combatImageIndex), hexPositionAbsolute(getHoveredHex()).center() - CCS->curh->getPivotOffsetCombat(combatImageIndex));
-			return;
-		}
-
-		auto spellCursorIndex = CCS->curh->get<Cursor::Spellcast>();
-		if (spellCursorIndex)
-		{
-			auto spellImageIndex = static_cast<size_t>(*spellCursorIndex);
-			to.draw(spellCursors->getImage(spellImageIndex), hexPositionAbsolute(getHoveredHex()).center() - CCS->curh->getPivotOffsetSpellcast());
-			return;
-		}
-
-	}
-}
-
-bool BattleFieldController::receiveEvent(const Point & position, int eventType) const
-{
-	if (eventType == HOVER)
-		return true;
-	return CIntObject::receiveEvent(position, eventType);
-}
+/*
+ * BattleFieldController.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 "BattleFieldController.h"
+
+#include "BattleInterface.h"
+#include "BattleActionsController.h"
+#include "BattleInterfaceClasses.h"
+#include "BattleEffectsController.h"
+#include "BattleSiegeController.h"
+#include "BattleStacksController.h"
+#include "BattleObstacleController.h"
+#include "BattleProjectileController.h"
+#include "BattleRenderer.h"
+
+#include "../CGameInfo.h"
+#include "../CPlayerInterface.h"
+#include "../render/CAnimation.h"
+#include "../render/Canvas.h"
+#include "../render/IImage.h"
+#include "../renderSDL/SDL_Extensions.h"
+#include "../render/IRenderHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/CursorHandler.h"
+#include "../adventureMap/CInGameConsole.h"
+#include "../client/render/CAnimation.h"
+
+#include "../../CCallback.h"
+#include "../../lib/BattleFieldHandler.h"
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/CStack.h"
+#include "../../lib/spells/ISpellMechanics.h"
+
+namespace HexMasks
+{
+	// mask definitions that has set to 1 the edges present in the hex edges highlight image
+	/*
+	    /\
+	   0  1
+	  /    \
+	 |      |
+	 5      2
+	 |      |
+	  \    /
+	   4  3
+	    \/
+	*/
+	enum HexEdgeMasks {
+		empty                 = 0b000000, // empty used when wanting to keep indexes the same but no highlight should be displayed
+		topLeft               = 0b000001,
+		topRight              = 0b000010,
+		right                 = 0b000100,
+		bottomRight           = 0b001000,
+		bottomLeft            = 0b010000,
+		left                  = 0b100000,
+						  
+		top                   = 0b000011,
+		bottom                = 0b011000,
+		topRightHalfCorner    = 0b000110,
+		bottomRightHalfCorner = 0b001100,
+		bottomLeftHalfCorner  = 0b110000,
+		topLeftHalfCorner     = 0b100001,
+
+		rightTopAndBottom     = 0b001010, // special case, right half can be drawn instead of only top and bottom
+		leftTopAndBottom      = 0b010001, // special case, left half can be drawn instead of only top and bottom
+						  
+		rightHalf             = 0b001110,
+		leftHalf              = 0b110001,
+						  
+		topRightCorner        = 0b000111,
+		bottomRightCorner     = 0b011100,
+		bottomLeftCorner      = 0b111000,
+		topLeftCorner         = 0b100011
+	};
+}
+
+std::map<int, int> hexEdgeMaskToFrameIndex;
+
+// Maps HexEdgesMask to "Frame" indexes for range highligt images
+void initializeHexEdgeMaskToFrameIndex()
+{
+	hexEdgeMaskToFrameIndex[HexMasks::empty] = 0;
+
+    hexEdgeMaskToFrameIndex[HexMasks::topLeft] = 1;
+    hexEdgeMaskToFrameIndex[HexMasks::topRight] = 2;
+    hexEdgeMaskToFrameIndex[HexMasks::right] = 3;
+    hexEdgeMaskToFrameIndex[HexMasks::bottomRight] = 4;
+    hexEdgeMaskToFrameIndex[HexMasks::bottomLeft] = 5;
+    hexEdgeMaskToFrameIndex[HexMasks::left] = 6;
+
+    hexEdgeMaskToFrameIndex[HexMasks::top] = 7;
+    hexEdgeMaskToFrameIndex[HexMasks::bottom] = 8;
+
+    hexEdgeMaskToFrameIndex[HexMasks::topRightHalfCorner] = 9;
+    hexEdgeMaskToFrameIndex[HexMasks::bottomRightHalfCorner] = 10;
+    hexEdgeMaskToFrameIndex[HexMasks::bottomLeftHalfCorner] = 11;
+    hexEdgeMaskToFrameIndex[HexMasks::topLeftHalfCorner] = 12;
+
+    hexEdgeMaskToFrameIndex[HexMasks::rightTopAndBottom] = 13;
+    hexEdgeMaskToFrameIndex[HexMasks::leftTopAndBottom] = 14;
+	
+    hexEdgeMaskToFrameIndex[HexMasks::rightHalf] = 13;
+    hexEdgeMaskToFrameIndex[HexMasks::leftHalf] = 14;
+
+    hexEdgeMaskToFrameIndex[HexMasks::topRightCorner] = 15;
+    hexEdgeMaskToFrameIndex[HexMasks::bottomRightCorner] = 16;
+    hexEdgeMaskToFrameIndex[HexMasks::bottomLeftCorner] = 17;
+    hexEdgeMaskToFrameIndex[HexMasks::topLeftCorner] = 18;
+}
+
+BattleFieldController::BattleFieldController(BattleInterface & owner):
+	owner(owner)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
+	//preparing cells and hexes
+	cellBorder = GH.renderHandler().loadImage(ImagePath::builtin("CCELLGRD.BMP"), EImageBlitMode::COLORKEY);
+	cellShade = GH.renderHandler().loadImage(ImagePath::builtin("CCELLSHD.BMP"));
+	cellUnitMovementHighlight = GH.renderHandler().loadImage(ImagePath::builtin("UnitMovementHighlight.PNG"), EImageBlitMode::COLORKEY);
+	cellUnitMaxMovementHighlight = GH.renderHandler().loadImage(ImagePath::builtin("UnitMaxMovementHighlight.PNG"), EImageBlitMode::COLORKEY);
+
+	attackCursors = GH.renderHandler().loadAnimation(AnimationPath::builtin("CRCOMBAT"));
+	attackCursors->preload();
+
+	spellCursors = GH.renderHandler().loadAnimation(AnimationPath::builtin("CRSPELL"));
+	spellCursors->preload();
+
+	initializeHexEdgeMaskToFrameIndex();
+
+	rangedFullDamageLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsGreen.json"));
+	rangedFullDamageLimitImages->preload();
+
+	shootingRangeLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsRed.json"));
+	shootingRangeLimitImages->preload();
+
+	flipRangeLimitImagesIntoPositions(rangedFullDamageLimitImages);
+	flipRangeLimitImagesIntoPositions(shootingRangeLimitImages);
+
+	if(!owner.siegeController)
+	{
+		auto bfieldType = owner.getBattle()->battleGetBattlefieldType();
+
+		if(bfieldType == BattleField::NONE)
+			logGlobal->error("Invalid battlefield returned for current battle");
+		else
+			background = GH.renderHandler().loadImage(bfieldType.getInfo()->graphics, EImageBlitMode::OPAQUE);
+	}
+	else
+	{
+		auto backgroundName = owner.siegeController->getBattleBackgroundName();
+		background = GH.renderHandler().loadImage(backgroundName, EImageBlitMode::OPAQUE);
+	}
+
+	pos.w = background->width();
+	pos.h = background->height();
+
+	backgroundWithHexes = std::make_unique<Canvas>(Point(background->width(), background->height()));
+
+	updateAccessibleHexes();
+	addUsedEvents(LCLICK | SHOW_POPUP | MOVE | TIME | GESTURE);
+}
+
+void BattleFieldController::activate()
+{
+	LOCPLINT->cingconsole->pos = this->pos;
+	CIntObject::activate();
+}
+
+void BattleFieldController::createHeroes()
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
+	// create heroes as part of our constructor for correct positioning inside battlefield
+	if(owner.attackingHeroInstance)
+		owner.attackingHero = std::make_shared<BattleHero>(owner, owner.attackingHeroInstance, false);
+
+	if(owner.defendingHeroInstance)
+		owner.defendingHero = std::make_shared<BattleHero>(owner, owner.defendingHeroInstance, true);
+}
+
+void BattleFieldController::gesture(bool on, const Point & initialPosition, const Point & finalPosition)
+{
+	if (!on && pos.isInside(finalPosition))
+		clickPressed(finalPosition);
+}
+
+void BattleFieldController::gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance)
+{
+	Point distance = currentPosition - initialPosition;
+
+	if (distance.length() < settings["battle"]["swipeAttackDistance"].Float())
+		hoveredHex = getHexAtPosition(initialPosition);
+	else
+		hoveredHex = BattleHex::INVALID;
+
+	currentAttackOriginPoint = currentPosition;
+
+	if (pos.isInside(initialPosition))
+		owner.actionsController->onHexHovered(getHoveredHex());
+}
+
+void BattleFieldController::mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance)
+{
+	hoveredHex = getHexAtPosition(cursorPosition);
+	currentAttackOriginPoint = cursorPosition;
+
+	if (pos.isInside(cursorPosition))
+		owner.actionsController->onHexHovered(getHoveredHex());
+	else
+		owner.actionsController->onHoverEnded();
+}
+
+void BattleFieldController::clickPressed(const Point & cursorPosition)
+{
+	BattleHex selectedHex = getHoveredHex();
+
+	if (selectedHex != BattleHex::INVALID)
+		owner.actionsController->onHexLeftClicked(selectedHex);
+}
+
+void BattleFieldController::showPopupWindow(const Point & cursorPosition)
+{
+	BattleHex selectedHex = getHoveredHex();
+
+	if (selectedHex != BattleHex::INVALID)
+		owner.actionsController->onHexRightClicked(selectedHex);
+}
+
+void BattleFieldController::renderBattlefield(Canvas & canvas)
+{
+	Canvas clippedCanvas(canvas, pos);
+
+	showBackground(clippedCanvas);
+
+	BattleRenderer renderer(owner);
+
+	renderer.execute(clippedCanvas);
+
+	owner.projectilesController->render(clippedCanvas);
+}
+
+void BattleFieldController::showBackground(Canvas & canvas)
+{
+	if (owner.stacksController->getActiveStack() != nullptr )
+		showBackgroundImageWithHexes(canvas);
+	else
+		showBackgroundImage(canvas);
+
+	showHighlightedHexes(canvas);
+}
+
+void BattleFieldController::showBackgroundImage(Canvas & canvas)
+{
+	canvas.draw(background, Point(0, 0));
+
+	owner.obstacleController->showAbsoluteObstacles(canvas);
+	if ( owner.siegeController )
+		owner.siegeController->showAbsoluteObstacles(canvas);
+
+	if (settings["battle"]["cellBorders"].Bool())
+	{
+		for (int i=0; i<GameConstants::BFIELD_SIZE; ++i)
+		{
+			if ( i % GameConstants::BFIELD_WIDTH == 0)
+				continue;
+			if ( i % GameConstants::BFIELD_WIDTH == GameConstants::BFIELD_WIDTH - 1)
+				continue;
+
+			canvas.draw(cellBorder, hexPositionLocal(i).topLeft());
+		}
+	}
+}
+
+void BattleFieldController::showBackgroundImageWithHexes(Canvas & canvas)
+{
+	canvas.draw(*backgroundWithHexes, Point(0, 0));
+}
+
+void BattleFieldController::redrawBackgroundWithHexes()
+{
+	const CStack *activeStack = owner.stacksController->getActiveStack();
+	std::vector<BattleHex> attackableHexes;
+	if(activeStack)
+		occupiableHexes = owner.getBattle()->battleGetAvailableHexes(activeStack, false, true, &attackableHexes);
+
+	// prepare background graphic with hexes and shaded hexes
+	backgroundWithHexes->draw(background, Point(0,0));
+	owner.obstacleController->showAbsoluteObstacles(*backgroundWithHexes);
+	if(owner.siegeController)
+		owner.siegeController->showAbsoluteObstacles(*backgroundWithHexes);
+
+	// show shaded hexes for active's stack valid movement and the hexes that it can attack
+	if(settings["battle"]["stackRange"].Bool())
+	{
+		std::vector<BattleHex> hexesToShade = occupiableHexes;
+		hexesToShade.insert(hexesToShade.end(), attackableHexes.begin(), attackableHexes.end());
+		for(BattleHex hex : hexesToShade)
+		{
+			showHighlightedHex(*backgroundWithHexes, cellShade, hex, false);
+		}
+	}
+
+	// draw cell borders
+	if(settings["battle"]["cellBorders"].Bool())
+	{
+		for(int i=0; i<GameConstants::BFIELD_SIZE; ++i)
+		{
+			if(i % GameConstants::BFIELD_WIDTH == 0)
+				continue;
+			if(i % GameConstants::BFIELD_WIDTH == GameConstants::BFIELD_WIDTH - 1)
+				continue;
+
+			backgroundWithHexes->draw(cellBorder, hexPositionLocal(i).topLeft());
+		}
+	}
+}
+
+void BattleFieldController::showHighlightedHex(Canvas & canvas, std::shared_ptr<IImage> highlight, BattleHex hex, bool darkBorder)
+{
+	Point hexPos = hexPositionLocal(hex).topLeft();
+
+	canvas.draw(highlight, hexPos);
+	if(!darkBorder && settings["battle"]["cellBorders"].Bool())
+		canvas.draw(cellBorder, hexPos);
+}
+
+std::set<BattleHex> BattleFieldController::getHighlightedHexesForActiveStack()
+{
+	std::set<BattleHex> result;
+
+	if(!owner.stacksController->getActiveStack())
+		return result;
+
+	if(!settings["battle"]["stackRange"].Bool())
+		return result;
+
+	auto hoveredHex = getHoveredHex();
+
+	std::set<BattleHex> set = owner.getBattle()->battleGetAttackedHexes(owner.stacksController->getActiveStack(), hoveredHex);
+	for(BattleHex hex : set)
+		result.insert(hex);
+
+	return result;
+}
+
+std::set<BattleHex> BattleFieldController::getMovementRangeForHoveredStack()
+{
+	std::set<BattleHex> result;
+
+	if (!owner.stacksController->getActiveStack())
+		return result;
+
+	if (!settings["battle"]["movementHighlightOnHover"].Bool() && !GH.isKeyboardShiftDown())
+		return result;
+
+	auto hoveredHex = getHoveredHex();
+
+	// add possible movement hexes for stack under mouse
+	const CStack * const hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true);
+	if(hoveredStack)
+	{
+		std::vector<BattleHex> v = owner.getBattle()->battleGetAvailableHexes(hoveredStack, true, true, nullptr);
+		for(BattleHex hex : v)
+			result.insert(hex);
+	}
+	return result;
+}
+
+std::set<BattleHex> BattleFieldController::getHighlightedHexesForSpellRange()
+{
+	std::set<BattleHex> result;
+	auto hoveredHex = getHoveredHex();
+
+	if(!settings["battle"]["mouseShadow"].Bool())
+		return result;
+
+	const spells::Caster *caster = nullptr;
+	const CSpell *spell = nullptr;
+
+	spells::Mode mode = owner.actionsController->getCurrentCastMode();
+	spell = owner.actionsController->getCurrentSpell(hoveredHex);
+	caster = owner.actionsController->getCurrentSpellcaster();
+
+	if(caster && spell) //when casting spell
+	{
+		// printing shaded hex(es)
+		spells::BattleCast event(owner.getBattle().get(), caster, mode, spell);
+		auto shadedHexes = spell->battleMechanics(&event)->rangeInHexes(hoveredHex);
+
+		for(BattleHex shadedHex : shadedHexes)
+		{
+			if((shadedHex.getX() != 0) && (shadedHex.getX() != GameConstants::BFIELD_WIDTH - 1))
+				result.insert(shadedHex);
+		}
+	}
+	return result;
+}
+
+std::set<BattleHex> BattleFieldController::getHighlightedHexesForMovementTarget()
+{
+	const CStack * stack = owner.stacksController->getActiveStack();
+	auto hoveredHex = getHoveredHex();
+
+	if(!stack)
+		return {};
+
+	std::vector<BattleHex> availableHexes = owner.getBattle()->battleGetAvailableHexes(stack, false, false, nullptr);
+
+	auto hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true);
+	if(owner.getBattle()->battleCanAttack(stack, hoveredStack, hoveredHex))
+	{
+		if(isTileAttackable(hoveredHex))
+		{
+			BattleHex attackFromHex = fromWhichHexAttack(hoveredHex);
+
+			if(stack->doubleWide())
+				return {attackFromHex, stack->occupiedHex(attackFromHex)};
+			else
+				return {attackFromHex};
+		}
+	}
+
+	if(vstd::contains(availableHexes, hoveredHex))
+	{
+		if(stack->doubleWide())
+			return {hoveredHex, stack->occupiedHex(hoveredHex)};
+		else
+			return {hoveredHex};
+	}
+
+	if(stack->doubleWide())
+	{
+		for(auto const & hex : availableHexes)
+		{
+			if(stack->occupiedHex(hex) == hoveredHex)
+				return {hoveredHex, hex};
+		}
+	}
+
+	return {};
+}
+
+// Range limit highlight helpers
+
+std::vector<BattleHex> BattleFieldController::getRangeHexes(BattleHex sourceHex, uint8_t distance)
+{
+	std::vector<BattleHex> rangeHexes;
+
+	if (!settings["battle"]["rangeLimitHighlightOnHover"].Bool() && !GH.isKeyboardShiftDown())
+		return rangeHexes;
+
+	// get only battlefield hexes that are within the given distance
+	for(auto i = 0; i < GameConstants::BFIELD_SIZE; i++)
+	{
+		BattleHex hex(i);
+		if(hex.isAvailable() && BattleHex::getDistance(sourceHex, hex) <= distance)
+			rangeHexes.push_back(hex);
+	}
+
+	return rangeHexes;
+}
+
+std::vector<BattleHex> BattleFieldController::getRangeLimitHexes(BattleHex hoveredHex, std::vector<BattleHex> rangeHexes, uint8_t distanceToLimit)
+{
+	std::vector<BattleHex> rangeLimitHexes;
+
+	// from range hexes get only the ones at the limit
+	for(auto & hex : rangeHexes)
+	{
+		if(BattleHex::getDistance(hoveredHex, hex) == distanceToLimit)
+			rangeLimitHexes.push_back(hex);
+	}
+
+	return rangeLimitHexes;
+}
+
+bool BattleFieldController::IsHexInRangeLimit(BattleHex hex, std::vector<BattleHex> & rangeLimitHexes, int * hexIndexInRangeLimit)
+{
+	bool  hexInRangeLimit = false;
+
+	if(!rangeLimitHexes.empty())
+	{
+		auto pos = std::find(rangeLimitHexes.begin(), rangeLimitHexes.end(), hex);
+		*hexIndexInRangeLimit = std::distance(rangeLimitHexes.begin(), pos);
+		hexInRangeLimit = pos != rangeLimitHexes.end();
+	}
+
+	return hexInRangeLimit;
+}
+
+std::vector<std::vector<BattleHex::EDir>> BattleFieldController::getOutsideNeighbourDirectionsForLimitHexes(std::vector<BattleHex> wholeRangeHexes, std::vector<BattleHex> rangeLimitHexes)
+{
+	std::vector<std::vector<BattleHex::EDir>> output;
+
+	if(wholeRangeHexes.empty())
+		return output;
+
+	for(auto & hex : rangeLimitHexes)
+	{
+		// get all neighbours and their directions
+		
+		auto neighbouringTiles = hex.allNeighbouringTiles();
+
+		std::vector<BattleHex::EDir> outsideNeighbourDirections;
+
+		// for each neighbour add to output only the valid ones and only that are not found in range Hexes
+		for(auto direction = 0; direction < 6; direction++)
+		{
+			if(!neighbouringTiles[direction].isAvailable())
+				continue;
+
+			auto it = std::find(wholeRangeHexes.begin(), wholeRangeHexes.end(), neighbouringTiles[direction]);
+
+			if(it == wholeRangeHexes.end())
+				outsideNeighbourDirections.push_back(BattleHex::EDir(direction)); // push direction
+		}
+
+		output.push_back(outsideNeighbourDirections);
+	}
+
+	return output;
+}
+
+std::vector<std::shared_ptr<IImage>> BattleFieldController::calculateRangeLimitHighlightImages(std::vector<std::vector<BattleHex::EDir>> hexesNeighbourDirections, std::shared_ptr<CAnimation> limitImages)
+{
+	std::vector<std::shared_ptr<IImage>> output; // if no image is to be shown an empty image is still added to help with traverssing the range
+
+	if(hexesNeighbourDirections.empty())
+		return output;
+
+	for(auto & directions : hexesNeighbourDirections)
+	{
+		std::bitset<6> mask;
+		
+		// convert directions to mask
+		for(auto direction : directions)
+			mask.set(direction);
+
+		uint8_t imageKey = static_cast<uint8_t>(mask.to_ulong());
+		output.push_back(limitImages->getImage(hexEdgeMaskToFrameIndex[imageKey]));
+	}
+
+	return output;
+}
+
+void BattleFieldController::calculateRangeLimitAndHighlightImages(uint8_t distance, std::shared_ptr<CAnimation> rangeLimitImages, std::vector<BattleHex> & rangeLimitHexes, std::vector<std::shared_ptr<IImage>> & rangeLimitHexesHighligts)
+{
+		std::vector<BattleHex> rangeHexes = getRangeHexes(hoveredHex, distance);
+		rangeLimitHexes = getRangeLimitHexes(hoveredHex, rangeHexes, distance);
+		std::vector<std::vector<BattleHex::EDir>> rangeLimitNeighbourDirections = getOutsideNeighbourDirectionsForLimitHexes(rangeHexes, rangeLimitHexes);
+		rangeLimitHexesHighligts = calculateRangeLimitHighlightImages(rangeLimitNeighbourDirections, rangeLimitImages);
+}
+
+void BattleFieldController::flipRangeLimitImagesIntoPositions(std::shared_ptr<CAnimation> images)
+{
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRight])->verticalFlip();
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::right])->verticalFlip();
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRight])->doubleFlip();
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeft])->horizontalFlip();
+
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottom])->horizontalFlip();
+
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRightHalfCorner])->verticalFlip();
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRightHalfCorner])->doubleFlip();
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeftHalfCorner])->horizontalFlip();
+
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::rightHalf])->verticalFlip();
+
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRightCorner])->verticalFlip();
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRightCorner])->doubleFlip();
+	images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeftCorner])->horizontalFlip();
+}
+
+void BattleFieldController::showHighlightedHexes(Canvas & canvas)
+{
+	std::vector<BattleHex> rangedFullDamageLimitHexes;
+	std::vector<BattleHex> shootingRangeLimitHexes;
+
+	std::vector<std::shared_ptr<IImage>> rangedFullDamageLimitHexesHighligts;
+	std::vector<std::shared_ptr<IImage>> shootingRangeLimitHexesHighligts;
+
+	std::set<BattleHex> hoveredStackMovementRangeHexes = getMovementRangeForHoveredStack();
+	std::set<BattleHex> hoveredSpellHexes = getHighlightedHexesForSpellRange();
+	std::set<BattleHex> hoveredMoveHexes  = getHighlightedHexesForMovementTarget();
+
+	BattleHex hoveredHex = getHoveredHex();
+	if(hoveredHex == BattleHex::INVALID)
+		return;
+
+	const CStack * hoveredStack = getHoveredStack();
+
+	// skip range limit calculations if unit hovered is not a shooter
+	if(hoveredStack && hoveredStack->isShooter())
+	{
+		// calculate array with highlight images for ranged full damage limit
+		auto rangedFullDamageDistance = hoveredStack->getRangedFullDamageDistance();
+		calculateRangeLimitAndHighlightImages(rangedFullDamageDistance, rangedFullDamageLimitImages, rangedFullDamageLimitHexes, rangedFullDamageLimitHexesHighligts);
+
+		// calculate array with highlight images for shooting range limit
+		auto shootingRangeDistance = hoveredStack->getShootingRangeDistance();
+		calculateRangeLimitAndHighlightImages(shootingRangeDistance, shootingRangeLimitImages, shootingRangeLimitHexes, shootingRangeLimitHexesHighligts);
+	}
+
+	auto const & hoveredMouseHexes = owner.actionsController->currentActionSpellcasting(getHoveredHex()) ? hoveredSpellHexes : hoveredMoveHexes;
+
+	for(int hex = 0; hex < GameConstants::BFIELD_SIZE; ++hex)
+	{
+		bool stackMovement = hoveredStackMovementRangeHexes.count(hex);
+		bool mouse = hoveredMouseHexes.count(hex);
+
+		// calculate if hex is Ranged Full Damage Limit and its position in highlight array
+		int hexIndexInRangedFullDamageLimit = 0;
+		bool hexInRangedFullDamageLimit = IsHexInRangeLimit(hex, rangedFullDamageLimitHexes, &hexIndexInRangedFullDamageLimit);
+
+		// calculate if hex is Shooting Range Limit and its position in highlight array
+		int hexIndexInShootingRangeLimit = 0;
+		bool hexInShootingRangeLimit = IsHexInRangeLimit(hex, shootingRangeLimitHexes, &hexIndexInShootingRangeLimit);
+
+		if(stackMovement && mouse) // area where hovered stackMovement can move shown with highlight. Because also affected by mouse cursor, shade as well
+		{
+			showHighlightedHex(canvas, cellUnitMovementHighlight, hex, false);
+			showHighlightedHex(canvas, cellShade, hex, true);
+		}
+		if(!stackMovement && mouse) // hexes affected only at mouse cursor shown as shaded
+		{
+			showHighlightedHex(canvas, cellShade, hex, true);
+		}
+		if(stackMovement && !mouse) // hexes where hovered stackMovement can move shown with highlight
+		{
+			showHighlightedHex(canvas, cellUnitMovementHighlight, hex, false);
+		}
+		if(hexInRangedFullDamageLimit)
+		{
+			showHighlightedHex(canvas, rangedFullDamageLimitHexesHighligts[hexIndexInRangedFullDamageLimit], hex, false);
+		}
+		if(hexInShootingRangeLimit)
+		{
+			showHighlightedHex(canvas, shootingRangeLimitHexesHighligts[hexIndexInShootingRangeLimit], hex, false);
+		}
+	}
+}
+
+Rect BattleFieldController::hexPositionLocal(BattleHex hex) const
+{
+	int x = 14 + ((hex.getY())%2==0 ? 22 : 0) + 44*hex.getX();
+	int y = 86 + 42 *hex.getY();
+	int w = cellShade->width();
+	int h = cellShade->height();
+	return Rect(x, y, w, h);
+}
+
+Rect BattleFieldController::hexPositionAbsolute(BattleHex hex) const
+{
+	return hexPositionLocal(hex) + pos.topLeft();
+}
+
+bool BattleFieldController::isPixelInHex(Point const & position)
+{
+	return !cellShade->isTransparent(position);
+}
+
+BattleHex BattleFieldController::getHoveredHex()
+{
+	return hoveredHex;
+}
+
+const CStack* BattleFieldController::getHoveredStack()
+{
+	auto hoveredHex = getHoveredHex();
+	const CStack* hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true);
+
+	return hoveredStack;
+}
+
+BattleHex BattleFieldController::getHexAtPosition(Point hoverPos)
+{
+	if (owner.attackingHero)
+	{
+		if (owner.attackingHero->pos.isInside(hoverPos))
+			return BattleHex::HERO_ATTACKER;
+	}
+
+	if (owner.defendingHero)
+	{
+		if (owner.attackingHero->pos.isInside(hoverPos))
+			return BattleHex::HERO_DEFENDER;
+	}
+
+	for (int h = 0; h < GameConstants::BFIELD_SIZE; ++h)
+	{
+		Rect hexPosition = hexPositionAbsolute(h);
+
+		if (!hexPosition.isInside(hoverPos))
+			continue;
+
+		if (isPixelInHex(hoverPos - hexPosition.topLeft()))
+			return h;
+	}
+
+	return BattleHex::INVALID;
+}
+
+BattleHex::EDir BattleFieldController::selectAttackDirection(BattleHex myNumber)
+{
+	const bool doubleWide = owner.stacksController->getActiveStack()->doubleWide();
+	auto neighbours = myNumber.allNeighbouringTiles();
+	//   0 1
+	//  5 x 2
+	//   4 3
+
+	// if true - our current stack can move into this hex (and attack)
+	std::array<bool, 8> attackAvailability;
+
+	if (doubleWide)
+	{
+		// For double-hexes we need to ensure that both hexes needed for this direction are occupyable:
+		// |    -0-   |   -1-    |    -2-   |   -3-    |    -4-   |   -5-    |    -6-   |   -7-
+		// |  o o -   |   - o o  |    - -   |   - -    |    - -   |   - -    |    o o   |   - -
+		// |   - x -  |  - x -   |   - x o o|  - x -   |   - x -  |o o x -   |   - x -  |  - x -
+		// |    - -   |   - -    |    - -   |   - o o  |  o o -   |   - -    |    - -   |   o o
+
+		for (size_t i : { 1, 2, 3})
+			attackAvailability[i] = vstd::contains(occupiableHexes, neighbours[i]) && vstd::contains(occupiableHexes, neighbours[i].cloneInDirection(BattleHex::RIGHT, false));
+
+		for (size_t i : { 4, 5, 0})
+			attackAvailability[i] = vstd::contains(occupiableHexes, neighbours[i]) && vstd::contains(occupiableHexes, neighbours[i].cloneInDirection(BattleHex::LEFT, false));
+
+		attackAvailability[6] = vstd::contains(occupiableHexes, neighbours[0]) && vstd::contains(occupiableHexes, neighbours[1]);
+		attackAvailability[7] = vstd::contains(occupiableHexes, neighbours[3]) && vstd::contains(occupiableHexes, neighbours[4]);
+	}
+	else
+	{
+		for (size_t i = 0; i < 6; ++i)
+			attackAvailability[i] = vstd::contains(occupiableHexes, neighbours[i]);
+
+		attackAvailability[6] = false;
+		attackAvailability[7] = false;
+	}
+
+	// Zero available tiles to attack from
+	if ( vstd::find(attackAvailability, true) == attackAvailability.end())
+	{
+		logGlobal->error("Error: cannot find a hex to attack hex %d from!", myNumber);
+		return BattleHex::NONE;
+	}
+
+	// For each valid direction, select position to test against
+	std::array<Point, 8> testPoint;
+
+	for (size_t i = 0; i < 6; ++i)
+		if (attackAvailability[i])
+			testPoint[i] = hexPositionAbsolute(neighbours[i]).center();
+
+	// For bottom/top directions select central point, but move it a bit away from true center to reduce zones allocated to them
+	if (attackAvailability[6])
+		testPoint[6] = (hexPositionAbsolute(neighbours[0]).center() + hexPositionAbsolute(neighbours[1]).center()) / 2 + Point(0, -5);
+
+	if (attackAvailability[7])
+		testPoint[7] = (hexPositionAbsolute(neighbours[3]).center() + hexPositionAbsolute(neighbours[4]).center()) / 2 + Point(0,  5);
+
+	// Compute distance between tested position & cursor position and pick nearest
+	std::array<int, 8> distance2;
+
+	for (size_t i = 0; i < 8; ++i)
+		if (attackAvailability[i])
+			distance2[i] = (testPoint[i].y - currentAttackOriginPoint.y)*(testPoint[i].y - currentAttackOriginPoint.y) + (testPoint[i].x - currentAttackOriginPoint.x)*(testPoint[i].x - currentAttackOriginPoint.x);
+
+	size_t nearest = -1;
+	for (size_t i = 0; i < 8; ++i)
+		if (attackAvailability[i] && (nearest == -1 || distance2[i] < distance2[nearest]) )
+			nearest = i;
+
+	assert(nearest != -1);
+	return BattleHex::EDir(nearest);
+}
+
+BattleHex BattleFieldController::fromWhichHexAttack(BattleHex attackTarget)
+{
+	BattleHex::EDir direction = selectAttackDirection(getHoveredHex());
+
+	const CStack * attacker = owner.stacksController->getActiveStack();
+
+	assert(direction != BattleHex::NONE);
+	assert(attacker);
+
+	if (!attacker->doubleWide())
+	{
+		assert(direction != BattleHex::BOTTOM);
+		assert(direction != BattleHex::TOP);
+		return attackTarget.cloneInDirection(direction);
+	}
+	else
+	{
+		// We need to find position of right hex of double-hex creature (or left for defending side)
+		// | TOP_LEFT |TOP_RIGHT |   RIGHT  |BOTTOM_RIGHT|BOTTOM_LEFT|  LEFT    |    TOP   |BOTTOM
+		// |  o o -   |   - o o  |    - -   |   - -      |    - -    |   - -    |    o o   |   - -
+		// |   - x -  |  - x -   |   - x o o|  - x -     |   - x -   |o o x -   |   - x -  |  - x -
+		// |    - -   |   - -    |    - -   |   - o o    |  o o -    |   - -    |    - -   |   o o
+
+		switch (direction)
+		{
+		case BattleHex::TOP_LEFT:
+		case BattleHex::LEFT:
+		case BattleHex::BOTTOM_LEFT:
+		{
+			if ( attacker->unitSide() == BattleSide::ATTACKER )
+				return attackTarget.cloneInDirection(direction);
+			else
+				return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::LEFT);
+		}
+
+		case BattleHex::TOP_RIGHT:
+		case BattleHex::RIGHT:
+		case BattleHex::BOTTOM_RIGHT:
+		{
+			if ( attacker->unitSide() == BattleSide::ATTACKER )
+				return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::RIGHT);
+			else
+				return attackTarget.cloneInDirection(direction);
+		}
+
+		case BattleHex::TOP:
+		{
+			if ( attacker->unitSide() == BattleSide::ATTACKER )
+				return attackTarget.cloneInDirection(BattleHex::TOP_RIGHT);
+			else
+				return attackTarget.cloneInDirection(BattleHex::TOP_LEFT);
+		}
+
+		case BattleHex::BOTTOM:
+		{
+			if ( attacker->unitSide() == BattleSide::ATTACKER )
+				return attackTarget.cloneInDirection(BattleHex::BOTTOM_RIGHT);
+			else
+				return attackTarget.cloneInDirection(BattleHex::BOTTOM_LEFT);
+		}
+		default:
+			assert(0);
+			return BattleHex::INVALID;
+		}
+	}
+}
+
+bool BattleFieldController::isTileAttackable(const BattleHex & number) const
+{
+	for (auto & elem : occupiableHexes)
+	{
+		if (BattleHex::mutualPosition(elem, number) != -1 || elem == number)
+			return true;
+	}
+	return false;
+}
+
+void BattleFieldController::updateAccessibleHexes()
+{
+	auto accessibility = owner.getBattle()->getAccesibility();
+
+	for(int i = 0; i < accessibility.size(); i++)
+		stackCountOutsideHexes[i] = (accessibility[i] == EAccessibility::ACCESSIBLE || (accessibility[i] == EAccessibility::SIDE_COLUMN));
+}
+
+bool BattleFieldController::stackCountOutsideHex(const BattleHex & number) const
+{
+	return stackCountOutsideHexes[number];
+}
+
+void BattleFieldController::showAll(Canvas & to)
+{
+	show(to);
+}
+
+void BattleFieldController::tick(uint32_t msPassed)
+{
+	updateAccessibleHexes();
+	owner.stacksController->tick(msPassed);
+	owner.obstacleController->tick(msPassed);
+	owner.projectilesController->tick(msPassed);
+}
+
+void BattleFieldController::show(Canvas & to)
+{
+	CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos);
+
+	renderBattlefield(to);
+
+	if (isActive() && isGesturing() && getHoveredHex() != BattleHex::INVALID)
+	{
+		auto combatCursorIndex = CCS->curh->get<Cursor::Combat>();
+		if (combatCursorIndex)
+		{
+			auto combatImageIndex = static_cast<size_t>(*combatCursorIndex);
+			to.draw(attackCursors->getImage(combatImageIndex), hexPositionAbsolute(getHoveredHex()).center() - CCS->curh->getPivotOffsetCombat(combatImageIndex));
+			return;
+		}
+
+		auto spellCursorIndex = CCS->curh->get<Cursor::Spellcast>();
+		if (spellCursorIndex)
+		{
+			auto spellImageIndex = static_cast<size_t>(*spellCursorIndex);
+			to.draw(spellCursors->getImage(spellImageIndex), hexPositionAbsolute(getHoveredHex()).center() - CCS->curh->getPivotOffsetSpellcast());
+			return;
+		}
+
+	}
+}
+
+bool BattleFieldController::receiveEvent(const Point & position, int eventType) const
+{
+	if (eventType == HOVER)
+		return true;
+	return CIntObject::receiveEvent(position, eventType);
+}

+ 144 - 144
client/battle/BattleFieldController.h

@@ -1,144 +1,144 @@
-/*
- * BattleFieldController.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/battle/BattleHex.h"
-#include "../../lib/Point.h"
-#include "../gui/CIntObject.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-class CStack;
-class Rect;
-VCMI_LIB_NAMESPACE_END
-
-class BattleHero;
-class CAnimation;
-class Canvas;
-class IImage;
-class BattleInterface;
-
-/// Handles battlefield grid as well as rendering of background layer of battle interface
-class BattleFieldController : public CIntObject
-{
-	BattleInterface & owner;
-
-	std::shared_ptr<IImage> background;
-	std::shared_ptr<IImage> cellBorder;
-	std::shared_ptr<IImage> cellUnitMovementHighlight;
-	std::shared_ptr<IImage> cellUnitMaxMovementHighlight;
-	std::shared_ptr<IImage> cellShade;
-	std::shared_ptr<CAnimation> rangedFullDamageLimitImages;
-	std::shared_ptr<CAnimation> shootingRangeLimitImages;
-
-	std::shared_ptr<CAnimation> attackCursors;
-	std::shared_ptr<CAnimation> spellCursors;
-
-	/// Canvas that contains background, hex grid (if enabled), absolute obstacles and movement range of active stack
-	std::unique_ptr<Canvas> backgroundWithHexes;
-
-	/// direction which will be used to perform attack with current cursor position
-	Point currentAttackOriginPoint;
-
-	/// hex currently under mouse hover
-	BattleHex hoveredHex;
-
-	/// hexes to which currently active stack can move
-	std::vector<BattleHex> occupiableHexes;
-
-	/// hexes that when in front of a unit cause it's amount box to move back
-	std::array<bool, GameConstants::BFIELD_SIZE> stackCountOutsideHexes;
-
-	void showHighlightedHex(Canvas & to, std::shared_ptr<IImage> highlight, BattleHex hex, bool darkBorder);
-
-	std::set<BattleHex> getHighlightedHexesForActiveStack();
-	std::set<BattleHex> getMovementRangeForHoveredStack();
-	std::set<BattleHex> getHighlightedHexesForSpellRange();
-	std::set<BattleHex> getHighlightedHexesForMovementTarget();
-
-	// Range limit highlight helpers
-
-	/// get all hexes within a certain distance of given hex
-	std::vector<BattleHex> getRangeHexes(BattleHex sourceHex, uint8_t distance);
-
-	/// get only hexes at the limit of a range
-	std::vector<BattleHex> getRangeLimitHexes(BattleHex hoveredHex, std::vector<BattleHex> hexRange, uint8_t distanceToLimit);
-
-	/// calculate if a hex is in range limit and return its index in range
-	bool IsHexInRangeLimit(BattleHex hex, std::vector<BattleHex> & rangeLimitHexes, int * hexIndexInRangeLimit);
-
-	/// get an array that has for each hex in range, an aray with all directions where an ouside neighbour hex exists
-	std::vector<std::vector<BattleHex::EDir>> getOutsideNeighbourDirectionsForLimitHexes(std::vector<BattleHex> rangeHexes, std::vector<BattleHex> rangeLimitHexes);
-
-	/// calculates what image to use as range limit, depending on the direction of neighbors
-	/// a mask is used internally to mark the directions of all neighbours
-	/// based on this mask the corresponding image is selected
-	std::vector<std::shared_ptr<IImage>> calculateRangeLimitHighlightImages(std::vector<std::vector<BattleHex::EDir>> hexesNeighbourDirections, std::shared_ptr<CAnimation> limitImages);
-
-	/// calculates all hexes for a range limit and what images to be shown as highlight for each of the hexes
-	void calculateRangeLimitAndHighlightImages(uint8_t distance, std::shared_ptr<CAnimation> rangeLimitImages, std::vector<BattleHex> & rangeLimitHexes, std::vector<std::shared_ptr<IImage>> & rangeLimitHexesHighligts);
-
-	/// to reduce the number of source images used, some images will be used as flipped versions of preloaded ones
-	void flipRangeLimitImagesIntoPositions(std::shared_ptr<CAnimation> images);
-
-	void showBackground(Canvas & canvas);
-	void showBackgroundImage(Canvas & canvas);
-	void showBackgroundImageWithHexes(Canvas & canvas);
-	void showHighlightedHexes(Canvas & canvas);
-	void updateAccessibleHexes();
-
-	BattleHex getHexAtPosition(Point hoverPosition);
-
-	/// Checks whether selected pixel is transparent, uses local coordinates of a hex
-	bool isPixelInHex(Point const & position);
-	size_t selectBattleCursor(BattleHex myNumber);
-
-	void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override;
-	void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override;
-	void mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) override;
-	void clickPressed(const Point & cursorPosition) override;
-	void showPopupWindow(const Point & cursorPosition) override;
-	void activate() override;
-
-	void showAll(Canvas & to) override;
-	void show(Canvas & to) override;
-	void tick(uint32_t msPassed) override;
-
-	bool receiveEvent(const Point & position, int eventType) const override;
-
-public:
-	BattleFieldController(BattleInterface & owner);
-
-	void createHeroes();
-
-	void redrawBackgroundWithHexes();
-	void renderBattlefield(Canvas & canvas);
-
-	/// Returns position of hex relative to owner (BattleInterface)
-	Rect hexPositionLocal(BattleHex hex) const;
-
-	/// Returns position of hex relative to game window
-	Rect hexPositionAbsolute(BattleHex hex) const;
-
-	/// Returns ID of currently hovered hex or BattleHex::INVALID if none
-	BattleHex getHoveredHex();
-
-	/// Returns the currently hovered stack
-	const CStack* getHoveredStack();
-
-	/// returns true if selected tile can be attacked in melee by current stack
-	bool isTileAttackable(const BattleHex & number) const;
-
-	/// returns true if stack should render its stack count image in default position - outside own hex
-	bool stackCountOutsideHex(const BattleHex & number) const;
-
-	BattleHex::EDir selectAttackDirection(BattleHex myNumber);
-
-	BattleHex fromWhichHexAttack(BattleHex myNumber);
-};
+/*
+ * BattleFieldController.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/battle/BattleHex.h"
+#include "../../lib/Point.h"
+#include "../gui/CIntObject.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+class CStack;
+class Rect;
+VCMI_LIB_NAMESPACE_END
+
+class BattleHero;
+class CAnimation;
+class Canvas;
+class IImage;
+class BattleInterface;
+
+/// Handles battlefield grid as well as rendering of background layer of battle interface
+class BattleFieldController : public CIntObject
+{
+	BattleInterface & owner;
+
+	std::shared_ptr<IImage> background;
+	std::shared_ptr<IImage> cellBorder;
+	std::shared_ptr<IImage> cellUnitMovementHighlight;
+	std::shared_ptr<IImage> cellUnitMaxMovementHighlight;
+	std::shared_ptr<IImage> cellShade;
+	std::shared_ptr<CAnimation> rangedFullDamageLimitImages;
+	std::shared_ptr<CAnimation> shootingRangeLimitImages;
+
+	std::shared_ptr<CAnimation> attackCursors;
+	std::shared_ptr<CAnimation> spellCursors;
+
+	/// Canvas that contains background, hex grid (if enabled), absolute obstacles and movement range of active stack
+	std::unique_ptr<Canvas> backgroundWithHexes;
+
+	/// direction which will be used to perform attack with current cursor position
+	Point currentAttackOriginPoint;
+
+	/// hex currently under mouse hover
+	BattleHex hoveredHex;
+
+	/// hexes to which currently active stack can move
+	std::vector<BattleHex> occupiableHexes;
+
+	/// hexes that when in front of a unit cause it's amount box to move back
+	std::array<bool, GameConstants::BFIELD_SIZE> stackCountOutsideHexes;
+
+	void showHighlightedHex(Canvas & to, std::shared_ptr<IImage> highlight, BattleHex hex, bool darkBorder);
+
+	std::set<BattleHex> getHighlightedHexesForActiveStack();
+	std::set<BattleHex> getMovementRangeForHoveredStack();
+	std::set<BattleHex> getHighlightedHexesForSpellRange();
+	std::set<BattleHex> getHighlightedHexesForMovementTarget();
+
+	// Range limit highlight helpers
+
+	/// get all hexes within a certain distance of given hex
+	std::vector<BattleHex> getRangeHexes(BattleHex sourceHex, uint8_t distance);
+
+	/// get only hexes at the limit of a range
+	std::vector<BattleHex> getRangeLimitHexes(BattleHex hoveredHex, std::vector<BattleHex> hexRange, uint8_t distanceToLimit);
+
+	/// calculate if a hex is in range limit and return its index in range
+	bool IsHexInRangeLimit(BattleHex hex, std::vector<BattleHex> & rangeLimitHexes, int * hexIndexInRangeLimit);
+
+	/// get an array that has for each hex in range, an aray with all directions where an ouside neighbour hex exists
+	std::vector<std::vector<BattleHex::EDir>> getOutsideNeighbourDirectionsForLimitHexes(std::vector<BattleHex> rangeHexes, std::vector<BattleHex> rangeLimitHexes);
+
+	/// calculates what image to use as range limit, depending on the direction of neighbors
+	/// a mask is used internally to mark the directions of all neighbours
+	/// based on this mask the corresponding image is selected
+	std::vector<std::shared_ptr<IImage>> calculateRangeLimitHighlightImages(std::vector<std::vector<BattleHex::EDir>> hexesNeighbourDirections, std::shared_ptr<CAnimation> limitImages);
+
+	/// calculates all hexes for a range limit and what images to be shown as highlight for each of the hexes
+	void calculateRangeLimitAndHighlightImages(uint8_t distance, std::shared_ptr<CAnimation> rangeLimitImages, std::vector<BattleHex> & rangeLimitHexes, std::vector<std::shared_ptr<IImage>> & rangeLimitHexesHighligts);
+
+	/// to reduce the number of source images used, some images will be used as flipped versions of preloaded ones
+	void flipRangeLimitImagesIntoPositions(std::shared_ptr<CAnimation> images);
+
+	void showBackground(Canvas & canvas);
+	void showBackgroundImage(Canvas & canvas);
+	void showBackgroundImageWithHexes(Canvas & canvas);
+	void showHighlightedHexes(Canvas & canvas);
+	void updateAccessibleHexes();
+
+	BattleHex getHexAtPosition(Point hoverPosition);
+
+	/// Checks whether selected pixel is transparent, uses local coordinates of a hex
+	bool isPixelInHex(Point const & position);
+	size_t selectBattleCursor(BattleHex myNumber);
+
+	void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override;
+	void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override;
+	void mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) override;
+	void clickPressed(const Point & cursorPosition) override;
+	void showPopupWindow(const Point & cursorPosition) override;
+	void activate() override;
+
+	void showAll(Canvas & to) override;
+	void show(Canvas & to) override;
+	void tick(uint32_t msPassed) override;
+
+	bool receiveEvent(const Point & position, int eventType) const override;
+
+public:
+	BattleFieldController(BattleInterface & owner);
+
+	void createHeroes();
+
+	void redrawBackgroundWithHexes();
+	void renderBattlefield(Canvas & canvas);
+
+	/// Returns position of hex relative to owner (BattleInterface)
+	Rect hexPositionLocal(BattleHex hex) const;
+
+	/// Returns position of hex relative to game window
+	Rect hexPositionAbsolute(BattleHex hex) const;
+
+	/// Returns ID of currently hovered hex or BattleHex::INVALID if none
+	BattleHex getHoveredHex();
+
+	/// Returns the currently hovered stack
+	const CStack* getHoveredStack();
+
+	/// returns true if selected tile can be attacked in melee by current stack
+	bool isTileAttackable(const BattleHex & number) const;
+
+	/// returns true if stack should render its stack count image in default position - outside own hex
+	bool stackCountOutsideHex(const BattleHex & number) const;
+
+	BattleHex::EDir selectAttackDirection(BattleHex myNumber);
+
+	BattleHex fromWhichHexAttack(BattleHex myNumber);
+};

+ 828 - 828
client/battle/BattleInterface.cpp

@@ -1,828 +1,828 @@
-/*
- * BattleInterface.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 "BattleInterface.h"
-
-#include "BattleAnimationClasses.h"
-#include "BattleActionsController.h"
-#include "BattleInterfaceClasses.h"
-#include "CreatureAnimation.h"
-#include "BattleProjectileController.h"
-#include "BattleEffectsController.h"
-#include "BattleObstacleController.h"
-#include "BattleSiegeController.h"
-#include "BattleFieldController.h"
-#include "BattleWindow.h"
-#include "BattleStacksController.h"
-#include "BattleRenderer.h"
-
-#include "../CGameInfo.h"
-#include "../CMusicHandler.h"
-#include "../CPlayerInterface.h"
-#include "../gui/CursorHandler.h"
-#include "../gui/CGuiHandler.h"
-#include "../gui/WindowHandler.h"
-#include "../render/Canvas.h"
-#include "../adventureMap/AdventureMapInterface.h"
-
-#include "../../CCallback.h"
-#include "../../lib/CStack.h"
-#include "../../lib/CConfigHandler.h"
-#include "../../lib/CGeneralTextHandler.h"
-#include "../../lib/CHeroHandler.h"
-#include "../../lib/CondSh.h"
-#include "../../lib/gameState/InfoAboutArmy.h"
-#include "../../lib/mapObjects/CGTownInstance.h"
-#include "../../lib/NetPacks.h"
-#include "../../lib/UnlockGuard.h"
-#include "../../lib/TerrainHandler.h"
-#include "../../lib/CThreadHelper.h"
-
-BattleInterface::BattleInterface(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2,
-		const CGHeroInstance *hero1, const CGHeroInstance *hero2,
-		std::shared_ptr<CPlayerInterface> att,
-		std::shared_ptr<CPlayerInterface> defen,
-		std::shared_ptr<CPlayerInterface> spectatorInt)
-	: attackingHeroInstance(hero1)
-	, defendingHeroInstance(hero2)
-	, attackerInt(att)
-	, defenderInt(defen)
-	, curInt(att)
-	, battleID(battleID)
-	, battleOpeningDelayActive(true)
-{
-	if(spectatorInt)
-	{
-		curInt = spectatorInt;
-	}
-	else if(!curInt)
-	{
-		//May happen when we are defending during network MP game -> attacker interface is just not present
-		curInt = defenderInt;
-	}
-
-	//hot-seat -> check tactics for both players (defender may be local human)
-	if(attackerInt && attackerInt->cb->getBattle(getBattleID())->battleGetTacticDist())
-		tacticianInterface = attackerInt;
-	else if(defenderInt && defenderInt->cb->getBattle(getBattleID())->battleGetTacticDist())
-		tacticianInterface = defenderInt;
-
-	//if we found interface of player with tactics, then enter tactics mode
-	tacticsMode = static_cast<bool>(tacticianInterface);
-
-	//initializing armies
-	this->army1 = army1;
-	this->army2 = army2;
-
-	const CGTownInstance *town = getBattle()->battleGetDefendedTown();
-	if(town && town->hasFort())
-		siegeController.reset(new BattleSiegeController(*this, town));
-
-	windowObject = std::make_shared<BattleWindow>(*this);
-	projectilesController.reset(new BattleProjectileController(*this));
-	stacksController.reset( new BattleStacksController(*this));
-	actionsController.reset( new BattleActionsController(*this));
-	effectsController.reset(new BattleEffectsController(*this));
-	obstacleController.reset(new BattleObstacleController(*this));
-
-	adventureInt->onAudioPaused();
-	ongoingAnimationsState.set(true);
-
-	GH.windows().pushWindow(windowObject);
-	windowObject->blockUI(true);
-	windowObject->updateQueue();
-
-	playIntroSoundAndUnlockInterface();
-}
-
-void BattleInterface::playIntroSoundAndUnlockInterface()
-{
-	auto onIntroPlayed = [this]()
-	{
-		if(LOCPLINT->battleInt)
-			onIntroSoundPlayed();
-	};
-
-	int battleIntroSoundChannel = CCS->soundh->playSoundFromSet(CCS->soundh->battleIntroSounds);
-	if (battleIntroSoundChannel != -1)
-	{
-		CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed);
-
-		if (settings["gameTweaks"]["skipBattleIntroMusic"].Bool())
-			openingEnd();
-	}
-	else // failed to play sound
-	{
-		onIntroSoundPlayed();
-	}
-}
-
-bool BattleInterface::openingPlaying()
-{
-	return battleOpeningDelayActive;
-}
-
-void BattleInterface::onIntroSoundPlayed()
-{
-	if (openingPlaying())
-		openingEnd();
-
-	CCS->musich->playMusicFromSet("battle", true, true);
-}
-
-void BattleInterface::openingEnd()
-{
-	assert(openingPlaying());
-	if (!openingPlaying())
-		return;
-
-	onAnimationsFinished();
-	if(tacticsMode)
-		tacticNextStack(nullptr);
-	activateStack();
-	battleOpeningDelayActive = false;
-}
-
-BattleInterface::~BattleInterface()
-{
-	CPlayerInterface::battleInt = nullptr;
-
-	if (adventureInt)
-		adventureInt->onAudioResumed();
-
-	awaitingEvents.clear();
-	onAnimationsFinished();
-}
-
-void BattleInterface::redrawBattlefield()
-{
-	fieldController->redrawBackgroundWithHexes();
-	GH.windows().totalRedraw();
-}
-
-void BattleInterface::stackReset(const CStack * stack)
-{
-	stacksController->stackReset(stack);
-}
-
-void BattleInterface::stackAdded(const CStack * stack)
-{
-	stacksController->stackAdded(stack, false);
-}
-
-void BattleInterface::stackRemoved(uint32_t stackID)
-{
-	stacksController->stackRemoved(stackID);
-	fieldController->redrawBackgroundWithHexes();
-	windowObject->updateQueue();
-}
-
-void BattleInterface::stackActivated(const CStack *stack)
-{
-	stacksController->stackActivated(stack);
-}
-
-void BattleInterface::stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance, bool teleport)
-{
-	if (teleport)
-		stacksController->stackTeleported(stack, destHex, distance);
-	else
-		stacksController->stackMoved(stack, destHex, distance);
-}
-
-void BattleInterface::stacksAreAttacked(std::vector<StackAttackedInfo> attackedInfos)
-{
-	stacksController->stacksAreAttacked(attackedInfos);
-
-	std::array<int, 2> killedBySide = {0, 0};
-
-	for(const StackAttackedInfo & attackedInfo : attackedInfos)
-	{
-		ui8 side = attackedInfo.defender->unitSide();
-		killedBySide.at(side) += attackedInfo.amountKilled;
-	}
-
-	for(ui8 side = 0; side < 2; side++)
-	{
-		if(killedBySide.at(side) > killedBySide.at(1-side))
-			setHeroAnimation(side, EHeroAnimType::DEFEAT);
-		else if(killedBySide.at(side) < killedBySide.at(1-side))
-			setHeroAnimation(side, EHeroAnimType::VICTORY);
-	}
-}
-
-void BattleInterface::stackAttacking( const StackAttackInfo & attackInfo )
-{
-	stacksController->stackAttacking(attackInfo);
-}
-
-void BattleInterface::newRoundFirst()
-{
-	waitForAnimations();
-}
-
-void BattleInterface::newRound()
-{
-	console->addText(CGI->generaltexth->allTexts[412]);
-}
-
-void BattleInterface::giveCommand(EActionType action, BattleHex tile, SpellID spell)
-{
-	const CStack * actor = nullptr;
-	if(action != EActionType::HERO_SPELL && action != EActionType::RETREAT && action != EActionType::SURRENDER)
-	{
-		actor = stacksController->getActiveStack();
-	}
-
-	auto side = getBattle()->playerToSide(curInt->playerID);
-	if(!side)
-	{
-		logGlobal->error("Player %s is not in battle", curInt->playerID.toString());
-		return;
-	}
-
-	BattleAction ba;
-	ba.side = side.value();
-	ba.actionType = action;
-	ba.aimToHex(tile);
-	ba.spell = spell;
-
-	sendCommand(ba, actor);
-}
-
-void BattleInterface::sendCommand(BattleAction command, const CStack * actor)
-{
-	command.stackNumber = actor ? actor->unitId() : ((command.side == BattleSide::ATTACKER) ? -1 : -2);
-
-	if(!tacticsMode)
-	{
-		logGlobal->trace("Setting command for %s", (actor ? actor->nodeName() : "hero"));
-		stacksController->setActiveStack(nullptr);
-		curInt->cb->battleMakeUnitAction(battleID, command);
-	}
-	else
-	{
-		curInt->cb->battleMakeTacticAction(battleID, command);
-		stacksController->setActiveStack(nullptr);
-		//next stack will be activated when action ends
-	}
-	CCS->curh->set(Cursor::Combat::POINTER);
-}
-
-const CGHeroInstance * BattleInterface::getActiveHero()
-{
-	const CStack *attacker = stacksController->getActiveStack();
-	if(!attacker)
-	{
-		return nullptr;
-	}
-
-	if(attacker->unitSide() == BattleSide::ATTACKER)
-	{
-		return attackingHeroInstance;
-	}
-
-	return defendingHeroInstance;
-}
-
-void BattleInterface::stackIsCatapulting(const CatapultAttack & ca)
-{
-	if (siegeController)
-		siegeController->stackIsCatapulting(ca);
-}
-
-void BattleInterface::gateStateChanged(const EGateState state)
-{
-	if (siegeController)
-		siegeController->gateStateChanged(state);
-}
-
-void BattleInterface::battleFinished(const BattleResult& br, QueryID queryID)
-{
-	checkForAnimations();
-	stacksController->setActiveStack(nullptr);
-
-	CCS->curh->set(Cursor::Map::POINTER);
-	curInt->waitWhileDialog();
-
-	if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-skip-battle-result"].Bool())
-	{
-		curInt->cb->selectionMade(0, queryID);
-		windowObject->close();
-		return;
-	}
-
-	auto wnd = std::make_shared<BattleResultWindow>(br, *(this->curInt));
-	wnd->resultCallback = [=](ui32 selection)
-	{
-		curInt->cb->selectionMade(selection, queryID);
-	};
-	GH.windows().pushWindow(wnd);
-
-	curInt->waitWhileDialog(); // Avoid freeze when AI end turn after battle. Check bug #1897
-	CPlayerInterface::battleInt = nullptr;
-}
-
-void BattleInterface::spellCast(const BattleSpellCast * sc)
-{
-	// Do not deactivate anything in tactics mode
-	// This is battlefield setup spells
-	if(!tacticsMode)
-	{
-		windowObject->blockUI(true);
-
-		// Disable current active stack duing the cast
-		// Store the current activeStack to stackToActivate
-		stacksController->deactivateStack();
-	}
-
-	CCS->curh->set(Cursor::Combat::BLOCKED);
-
-	const SpellID spellID = sc->spellID;
-	const CSpell * spell = spellID.toSpell();
-	auto targetedTile = sc->tile;
-
-	assert(spell);
-	if(!spell)
-		return;
-
-	const AudioPath & castSoundPath = spell->getCastSound();
-
-	if (!castSoundPath.empty())
-	{
-		auto group = spell->animationInfo.projectile.empty() ?
-					EAnimationEvents::HIT:
-					EAnimationEvents::BEFORE_HIT;//FIXME: recheck whether this should be on projectile spawning
-
-		addToAnimationStage(group, [=]() {
-			CCS->soundh->playSound(castSoundPath);
-		});
-	}
-
-	if ( sc->activeCast )
-	{
-		const CStack * casterStack = getBattle()->battleGetStackByID(sc->casterStack);
-
-		if(casterStack != nullptr )
-		{
-			addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]()
-			{
-				stacksController->addNewAnim(new CastAnimation(*this, casterStack, targetedTile, getBattle()->battleGetStackByPos(targetedTile), spell));
-				displaySpellCast(spell, casterStack->getPosition());
-			});
-		}
-		else
-		{
-			auto hero = sc->side ? defendingHero : attackingHero;
-			assert(hero);
-
-			addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]()
-			{
-				stacksController->addNewAnim(new HeroCastAnimation(*this, hero, targetedTile, getBattle()->battleGetStackByPos(targetedTile), spell));
-			});
-		}
-	}
-
-	addToAnimationStage(EAnimationEvents::HIT, [=](){
-		displaySpellHit(spell, targetedTile);
-	});
-
-	//queuing affect animation
-	for(auto & elem : sc->affectedCres)
-	{
-		auto stack = getBattle()->battleGetStackByID(elem, false);
-		assert(stack);
-		if(stack)
-		{
-			addToAnimationStage(EAnimationEvents::HIT, [=](){
-				displaySpellEffect(spell, stack->getPosition());
-			});
-		}
-	}
-
-	for(auto & elem : sc->reflectedCres)
-	{
-		auto stack = getBattle()->battleGetStackByID(elem, false);
-		assert(stack);
-		addToAnimationStage(EAnimationEvents::HIT, [=](){
-			effectsController->displayEffect(EBattleEffect::MAGIC_MIRROR, stack->getPosition());
-		});
-	}
-
-	if (!sc->resistedCres.empty())
-	{
-		addToAnimationStage(EAnimationEvents::HIT, [=](){
-			CCS->soundh->playSound(AudioPath::builtin("MAGICRES"));
-		});
-	}
-
-	for(auto & elem : sc->resistedCres)
-	{
-		auto stack = getBattle()->battleGetStackByID(elem, false);
-		assert(stack);
-		addToAnimationStage(EAnimationEvents::HIT, [=](){
-			effectsController->displayEffect(EBattleEffect::RESISTANCE, stack->getPosition());
-		});
-	}
-
-	//mana absorption
-	if (sc->manaGained > 0)
-	{
-		Point leftHero = Point(15, 30);
-		Point rightHero = Point(755, 30);
-		bool side = sc->side;
-
-		addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){
-			stacksController->addNewAnim(new EffectAnimation(*this, AnimationPath::builtin(side ? "SP07_A.DEF" : "SP07_B.DEF"), leftHero));
-			stacksController->addNewAnim(new EffectAnimation(*this, AnimationPath::builtin(side ? "SP07_B.DEF" : "SP07_A.DEF"), rightHero));
-		});
-	}
-
-	// animations will be executed by spell effects
-}
-
-void BattleInterface::battleStacksEffectsSet(const SetStackEffect & sse)
-{
-	if(stacksController->getActiveStack() != nullptr)
-		fieldController->redrawBackgroundWithHexes();
-}
-
-void BattleInterface::setHeroAnimation(ui8 side, EHeroAnimType phase)
-{
-	if(side == BattleSide::ATTACKER)
-	{
-		if(attackingHero)
-			attackingHero->setPhase(phase);
-	}
-	else
-	{
-		if(defendingHero)
-			defendingHero->setPhase(phase);
-	}
-}
-
-void BattleInterface::displayBattleLog(const std::vector<MetaString> & battleLog)
-{
-	for(const auto & line : battleLog)
-	{
-		std::string formatted = line.toString();
-		boost::algorithm::trim(formatted);
-		appendBattleLog(formatted);
-	}
-}
-
-void BattleInterface::displaySpellAnimationQueue(const CSpell * spell, const CSpell::TAnimationQueue & q, BattleHex destinationTile, bool isHit)
-{
-	for(const CSpell::TAnimation & animation : q)
-	{
-		if(animation.pause > 0)
-			stacksController->addNewAnim(new DummyAnimation(*this, animation.pause));
-
-		if (!animation.effectName.empty())
-		{
-			const CStack * destStack = getBattle()->battleGetStackByPos(destinationTile, false);
-
-			if (destStack)
-				stacksController->addNewAnim(new ColorTransformAnimation(*this, destStack, animation.effectName, spell ));
-		}
-
-		if(!animation.resourceName.empty())
-		{
-			int flags = 0;
-
-			if (isHit)
-				flags |= EffectAnimation::FORCE_ON_TOP;
-
-			if (animation.verticalPosition == VerticalPosition::BOTTOM)
-				flags |= EffectAnimation::ALIGN_TO_BOTTOM;
-
-			if (!destinationTile.isValid())
-				flags |= EffectAnimation::SCREEN_FILL;
-
-			if (!destinationTile.isValid())
-				stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, flags));
-			else
-				stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, destinationTile, flags));
-		}
-	}
-}
-
-void BattleInterface::displaySpellCast(const CSpell * spell, BattleHex destinationTile)
-{
-	if(spell)
-		displaySpellAnimationQueue(spell, spell->animationInfo.cast, destinationTile, false);
-}
-
-void BattleInterface::displaySpellEffect(const CSpell * spell, BattleHex destinationTile)
-{
-	if(spell)
-		displaySpellAnimationQueue(spell, spell->animationInfo.affect, destinationTile, false);
-}
-
-void BattleInterface::displaySpellHit(const CSpell * spell, BattleHex destinationTile)
-{
-	if(spell)
-		displaySpellAnimationQueue(spell, spell->animationInfo.hit, destinationTile, true);
-}
-
-CPlayerInterface *BattleInterface::getCurrentPlayerInterface() const
-{
-	return curInt.get();
-}
-
-void BattleInterface::trySetActivePlayer( PlayerColor player )
-{
-	if ( attackerInt && attackerInt->playerID == player )
-		curInt = attackerInt;
-
-	if ( defenderInt && defenderInt->playerID == player )
-		curInt = defenderInt;
-}
-
-void BattleInterface::activateStack()
-{
-	stacksController->activateStack();
-
-	const CStack * s = stacksController->getActiveStack();
-	if(!s)
-		return;
-
-	windowObject->updateQueue();
-	windowObject->blockUI(false);
-	fieldController->redrawBackgroundWithHexes();
-	actionsController->activateStack();
-	GH.fakeMouseMove();
-}
-
-bool BattleInterface::makingTurn() const
-{
-	return stacksController->getActiveStack() != nullptr;
-}
-
-BattleID BattleInterface::getBattleID() const
-{
-	return battleID;
-}
-
-std::shared_ptr<CPlayerBattleCallback> BattleInterface::getBattle() const
-{
-	return curInt->cb->getBattle(battleID);
-}
-
-void BattleInterface::endAction(const BattleAction &action)
-{
-	// it is possible that tactics mode ended while opening music is still playing
-	waitForAnimations();
-
-	const CStack *stack = getBattle()->battleGetStackByID(action.stackNumber);
-
-	// Activate stack from stackToActivate because this might have been temporary disabled, e.g., during spell cast
-	activateStack();
-
-	stacksController->endAction(action);
-	windowObject->updateQueue();
-
-	//stack ended movement in tactics phase -> select the next one
-	if (tacticsMode)
-		tacticNextStack(stack);
-
-	//we have activated next stack after sending request that has been just realized -> blockmap due to movement has changed
-	if(action.actionType == EActionType::HERO_SPELL)
-		fieldController->redrawBackgroundWithHexes();
-}
-
-void BattleInterface::appendBattleLog(const std::string & newEntry)
-{
-	console->addText(newEntry);
-}
-
-void BattleInterface::startAction(const BattleAction & action)
-{
-	if(action.actionType == EActionType::END_TACTIC_PHASE)
-	{
-		windowObject->tacticPhaseEnded();
-		return;
-	}
-
-	stacksController->startAction(action);
-
-	if (!action.isUnitAction())
-		return;
-
-	assert(getBattle()->battleGetStackByID(action.stackNumber));
-	windowObject->updateQueue();
-	effectsController->startAction(action);
-}
-
-void BattleInterface::tacticPhaseEnd()
-{
-	stacksController->setActiveStack(nullptr);
-	tacticsMode = false;
-
-	auto side = tacticianInterface->cb->getBattle(battleID)->playerToSide(tacticianInterface->playerID);
-	auto action = BattleAction::makeEndOFTacticPhase(*side);
-
-	tacticianInterface->cb->battleMakeTacticAction(battleID, action);
-}
-
-static bool immobile(const CStack *s)
-{
-	return !s->speed(0, true); //should bound stacks be immobile?
-}
-
-void BattleInterface::tacticNextStack(const CStack * current)
-{
-	if (!current)
-		current = stacksController->getActiveStack();
-
-	//no switching stacks when the current one is moving
-	checkForAnimations();
-
-	TStacks stacksOfMine = tacticianInterface->cb->getBattle(battleID)->battleGetStacks(CPlayerBattleCallback::ONLY_MINE);
-	vstd::erase_if (stacksOfMine, &immobile);
-	if (stacksOfMine.empty())
-	{
-		tacticPhaseEnd();
-		return;
-	}
-
-	auto it = vstd::find(stacksOfMine, current);
-	if (it != stacksOfMine.end() && ++it != stacksOfMine.end())
-		stackActivated(*it);
-	else
-		stackActivated(stacksOfMine.front());
-
-}
-
-void BattleInterface::obstaclePlaced(const std::vector<std::shared_ptr<const CObstacleInstance>> oi)
-{
-	obstacleController->obstaclePlaced(oi);
-}
-
-void BattleInterface::obstacleRemoved(const std::vector<ObstacleChanges> & obstacles)
-{
-	obstacleController->obstacleRemoved(obstacles);
-}
-
-const CGHeroInstance *BattleInterface::currentHero() const
-{
-	if (attackingHeroInstance && attackingHeroInstance->tempOwner == curInt->playerID)
-		return attackingHeroInstance;
-
-	if (defendingHeroInstance && defendingHeroInstance->tempOwner == curInt->playerID)
-		return defendingHeroInstance;
-
-	return nullptr;
-}
-
-InfoAboutHero BattleInterface::enemyHero() const
-{
-	InfoAboutHero ret;
-	if (attackingHeroInstance->tempOwner == curInt->playerID)
-		curInt->cb->getHeroInfo(defendingHeroInstance, ret);
-	else
-		curInt->cb->getHeroInfo(attackingHeroInstance, ret);
-
-	return ret;
-}
-
-void BattleInterface::requestAutofightingAIToTakeAction()
-{
-	assert(curInt->isAutoFightOn);
-
-	if(getBattle()->battleIsFinished())
-	{
-		return; // battle finished with spellcast
-	}
-
-	if (tacticsMode)
-	{
-		// Always end tactics mode. Player interface is blocked currently, so it's not possible that
-		// the AI can take any action except end tactics phase (AI actions won't be triggered)
-		//TODO implement the possibility that the AI will be triggered for further actions
-		//TODO any solution to merge tactics phase & normal phase in the way it is handled by the player and battle interface?
-		tacticPhaseEnd();
-		stacksController->setActiveStack(nullptr);
-	}
-	else
-	{
-		const CStack* activeStack = stacksController->getActiveStack();
-
-		// If enemy is moving, activeStack can be null
-		if (activeStack)
-		{
-			stacksController->setActiveStack(nullptr);
-
-			// FIXME: unsafe
-			// Run task in separate thread to avoid UI lock while AI is making turn (which might take some time)
-			// HOWEVER this thread won't atttempt to lock game state, potentially leading to races
-			boost::thread aiThread([this, activeStack]()
-			{
-				setThreadName("autofightingAI");
-				curInt->autofightingAI->activeStack(battleID, activeStack);
-			});
-			aiThread.detach();
-		}
-	}
-}
-
-void BattleInterface::castThisSpell(SpellID spellID)
-{
-	actionsController->castThisSpell(spellID);
-}
-
-void BattleInterface::executeStagedAnimations()
-{
-	EAnimationEvents earliestStage = EAnimationEvents::COUNT;
-
-	for(const auto & event : awaitingEvents)
-		earliestStage = std::min(earliestStage, event.event);
-
-	if(earliestStage != EAnimationEvents::COUNT)
-		executeAnimationStage(earliestStage);
-}
-
-void BattleInterface::executeAnimationStage(EAnimationEvents event)
-{
-	decltype(awaitingEvents) executingEvents;
-
-	for(auto it = awaitingEvents.begin(); it != awaitingEvents.end();)
-	{
-		if(it->event == event)
-		{
-			executingEvents.push_back(*it);
-			it = awaitingEvents.erase(it);
-		}
-		else
-			++it;
-	}
-	for(const auto & event : executingEvents)
-		event.action();
-}
-
-void BattleInterface::onAnimationsStarted()
-{
-	ongoingAnimationsState.setn(true);
-}
-
-void BattleInterface::onAnimationsFinished()
-{
-	ongoingAnimationsState.setn(false);
-}
-
-void BattleInterface::waitForAnimations()
-{
-	{
-		auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex);
-		ongoingAnimationsState.waitUntil(false);
-	}
-
-	assert(!hasAnimations());
-	assert(awaitingEvents.empty());
-
-	if (!awaitingEvents.empty())
-	{
-		logGlobal->error("Wait for animations finished but we still have awaiting events!");
-		awaitingEvents.clear();
-	}
-}
-
-bool BattleInterface::hasAnimations()
-{
-	return ongoingAnimationsState.get();
-}
-
-void BattleInterface::checkForAnimations()
-{
-	assert(!hasAnimations());
-	if(hasAnimations())
-		logGlobal->error("Unexpected animations state: expected all animations to be over, but some are still ongoing!");
-
-	waitForAnimations();
-}
-
-void BattleInterface::addToAnimationStage(EAnimationEvents event, const AwaitingAnimationAction & action)
-{
-	awaitingEvents.push_back({action, event});
-}
-
-void BattleInterface::setBattleQueueVisibility(bool visible)
-{
-	windowObject->hideQueue();
-	if(visible)
-		windowObject->showQueue();
-}
-
-void BattleInterface::setStickyHeroWindowsVisibility(bool visible)
-{
-	windowObject->hideStickyHeroWindows();
-	if(visible)
-		windowObject->showStickyHeroWindows();
-}
+/*
+ * BattleInterface.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 "BattleInterface.h"
+
+#include "BattleAnimationClasses.h"
+#include "BattleActionsController.h"
+#include "BattleInterfaceClasses.h"
+#include "CreatureAnimation.h"
+#include "BattleProjectileController.h"
+#include "BattleEffectsController.h"
+#include "BattleObstacleController.h"
+#include "BattleSiegeController.h"
+#include "BattleFieldController.h"
+#include "BattleWindow.h"
+#include "BattleStacksController.h"
+#include "BattleRenderer.h"
+
+#include "../CGameInfo.h"
+#include "../CMusicHandler.h"
+#include "../CPlayerInterface.h"
+#include "../gui/CursorHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/WindowHandler.h"
+#include "../render/Canvas.h"
+#include "../adventureMap/AdventureMapInterface.h"
+
+#include "../../CCallback.h"
+#include "../../lib/CStack.h"
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/CHeroHandler.h"
+#include "../../lib/CondSh.h"
+#include "../../lib/gameState/InfoAboutArmy.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
+#include "../../lib/NetPacks.h"
+#include "../../lib/UnlockGuard.h"
+#include "../../lib/TerrainHandler.h"
+#include "../../lib/CThreadHelper.h"
+
+BattleInterface::BattleInterface(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2,
+		const CGHeroInstance *hero1, const CGHeroInstance *hero2,
+		std::shared_ptr<CPlayerInterface> att,
+		std::shared_ptr<CPlayerInterface> defen,
+		std::shared_ptr<CPlayerInterface> spectatorInt)
+	: attackingHeroInstance(hero1)
+	, defendingHeroInstance(hero2)
+	, attackerInt(att)
+	, defenderInt(defen)
+	, curInt(att)
+	, battleID(battleID)
+	, battleOpeningDelayActive(true)
+{
+	if(spectatorInt)
+	{
+		curInt = spectatorInt;
+	}
+	else if(!curInt)
+	{
+		//May happen when we are defending during network MP game -> attacker interface is just not present
+		curInt = defenderInt;
+	}
+
+	//hot-seat -> check tactics for both players (defender may be local human)
+	if(attackerInt && attackerInt->cb->getBattle(getBattleID())->battleGetTacticDist())
+		tacticianInterface = attackerInt;
+	else if(defenderInt && defenderInt->cb->getBattle(getBattleID())->battleGetTacticDist())
+		tacticianInterface = defenderInt;
+
+	//if we found interface of player with tactics, then enter tactics mode
+	tacticsMode = static_cast<bool>(tacticianInterface);
+
+	//initializing armies
+	this->army1 = army1;
+	this->army2 = army2;
+
+	const CGTownInstance *town = getBattle()->battleGetDefendedTown();
+	if(town && town->hasFort())
+		siegeController.reset(new BattleSiegeController(*this, town));
+
+	windowObject = std::make_shared<BattleWindow>(*this);
+	projectilesController.reset(new BattleProjectileController(*this));
+	stacksController.reset( new BattleStacksController(*this));
+	actionsController.reset( new BattleActionsController(*this));
+	effectsController.reset(new BattleEffectsController(*this));
+	obstacleController.reset(new BattleObstacleController(*this));
+
+	adventureInt->onAudioPaused();
+	ongoingAnimationsState.set(true);
+
+	GH.windows().pushWindow(windowObject);
+	windowObject->blockUI(true);
+	windowObject->updateQueue();
+
+	playIntroSoundAndUnlockInterface();
+}
+
+void BattleInterface::playIntroSoundAndUnlockInterface()
+{
+	auto onIntroPlayed = [this]()
+	{
+		if(LOCPLINT->battleInt)
+			onIntroSoundPlayed();
+	};
+
+	int battleIntroSoundChannel = CCS->soundh->playSoundFromSet(CCS->soundh->battleIntroSounds);
+	if (battleIntroSoundChannel != -1)
+	{
+		CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed);
+
+		if (settings["gameTweaks"]["skipBattleIntroMusic"].Bool())
+			openingEnd();
+	}
+	else // failed to play sound
+	{
+		onIntroSoundPlayed();
+	}
+}
+
+bool BattleInterface::openingPlaying()
+{
+	return battleOpeningDelayActive;
+}
+
+void BattleInterface::onIntroSoundPlayed()
+{
+	if (openingPlaying())
+		openingEnd();
+
+	CCS->musich->playMusicFromSet("battle", true, true);
+}
+
+void BattleInterface::openingEnd()
+{
+	assert(openingPlaying());
+	if (!openingPlaying())
+		return;
+
+	onAnimationsFinished();
+	if(tacticsMode)
+		tacticNextStack(nullptr);
+	activateStack();
+	battleOpeningDelayActive = false;
+}
+
+BattleInterface::~BattleInterface()
+{
+	CPlayerInterface::battleInt = nullptr;
+
+	if (adventureInt)
+		adventureInt->onAudioResumed();
+
+	awaitingEvents.clear();
+	onAnimationsFinished();
+}
+
+void BattleInterface::redrawBattlefield()
+{
+	fieldController->redrawBackgroundWithHexes();
+	GH.windows().totalRedraw();
+}
+
+void BattleInterface::stackReset(const CStack * stack)
+{
+	stacksController->stackReset(stack);
+}
+
+void BattleInterface::stackAdded(const CStack * stack)
+{
+	stacksController->stackAdded(stack, false);
+}
+
+void BattleInterface::stackRemoved(uint32_t stackID)
+{
+	stacksController->stackRemoved(stackID);
+	fieldController->redrawBackgroundWithHexes();
+	windowObject->updateQueue();
+}
+
+void BattleInterface::stackActivated(const CStack *stack)
+{
+	stacksController->stackActivated(stack);
+}
+
+void BattleInterface::stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance, bool teleport)
+{
+	if (teleport)
+		stacksController->stackTeleported(stack, destHex, distance);
+	else
+		stacksController->stackMoved(stack, destHex, distance);
+}
+
+void BattleInterface::stacksAreAttacked(std::vector<StackAttackedInfo> attackedInfos)
+{
+	stacksController->stacksAreAttacked(attackedInfos);
+
+	std::array<int, 2> killedBySide = {0, 0};
+
+	for(const StackAttackedInfo & attackedInfo : attackedInfos)
+	{
+		ui8 side = attackedInfo.defender->unitSide();
+		killedBySide.at(side) += attackedInfo.amountKilled;
+	}
+
+	for(ui8 side = 0; side < 2; side++)
+	{
+		if(killedBySide.at(side) > killedBySide.at(1-side))
+			setHeroAnimation(side, EHeroAnimType::DEFEAT);
+		else if(killedBySide.at(side) < killedBySide.at(1-side))
+			setHeroAnimation(side, EHeroAnimType::VICTORY);
+	}
+}
+
+void BattleInterface::stackAttacking( const StackAttackInfo & attackInfo )
+{
+	stacksController->stackAttacking(attackInfo);
+}
+
+void BattleInterface::newRoundFirst()
+{
+	waitForAnimations();
+}
+
+void BattleInterface::newRound()
+{
+	console->addText(CGI->generaltexth->allTexts[412]);
+}
+
+void BattleInterface::giveCommand(EActionType action, BattleHex tile, SpellID spell)
+{
+	const CStack * actor = nullptr;
+	if(action != EActionType::HERO_SPELL && action != EActionType::RETREAT && action != EActionType::SURRENDER)
+	{
+		actor = stacksController->getActiveStack();
+	}
+
+	auto side = getBattle()->playerToSide(curInt->playerID);
+	if(!side)
+	{
+		logGlobal->error("Player %s is not in battle", curInt->playerID.toString());
+		return;
+	}
+
+	BattleAction ba;
+	ba.side = side.value();
+	ba.actionType = action;
+	ba.aimToHex(tile);
+	ba.spell = spell;
+
+	sendCommand(ba, actor);
+}
+
+void BattleInterface::sendCommand(BattleAction command, const CStack * actor)
+{
+	command.stackNumber = actor ? actor->unitId() : ((command.side == BattleSide::ATTACKER) ? -1 : -2);
+
+	if(!tacticsMode)
+	{
+		logGlobal->trace("Setting command for %s", (actor ? actor->nodeName() : "hero"));
+		stacksController->setActiveStack(nullptr);
+		curInt->cb->battleMakeUnitAction(battleID, command);
+	}
+	else
+	{
+		curInt->cb->battleMakeTacticAction(battleID, command);
+		stacksController->setActiveStack(nullptr);
+		//next stack will be activated when action ends
+	}
+	CCS->curh->set(Cursor::Combat::POINTER);
+}
+
+const CGHeroInstance * BattleInterface::getActiveHero()
+{
+	const CStack *attacker = stacksController->getActiveStack();
+	if(!attacker)
+	{
+		return nullptr;
+	}
+
+	if(attacker->unitSide() == BattleSide::ATTACKER)
+	{
+		return attackingHeroInstance;
+	}
+
+	return defendingHeroInstance;
+}
+
+void BattleInterface::stackIsCatapulting(const CatapultAttack & ca)
+{
+	if (siegeController)
+		siegeController->stackIsCatapulting(ca);
+}
+
+void BattleInterface::gateStateChanged(const EGateState state)
+{
+	if (siegeController)
+		siegeController->gateStateChanged(state);
+}
+
+void BattleInterface::battleFinished(const BattleResult& br, QueryID queryID)
+{
+	checkForAnimations();
+	stacksController->setActiveStack(nullptr);
+
+	CCS->curh->set(Cursor::Map::POINTER);
+	curInt->waitWhileDialog();
+
+	if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-skip-battle-result"].Bool())
+	{
+		curInt->cb->selectionMade(0, queryID);
+		windowObject->close();
+		return;
+	}
+
+	auto wnd = std::make_shared<BattleResultWindow>(br, *(this->curInt));
+	wnd->resultCallback = [=](ui32 selection)
+	{
+		curInt->cb->selectionMade(selection, queryID);
+	};
+	GH.windows().pushWindow(wnd);
+
+	curInt->waitWhileDialog(); // Avoid freeze when AI end turn after battle. Check bug #1897
+	CPlayerInterface::battleInt = nullptr;
+}
+
+void BattleInterface::spellCast(const BattleSpellCast * sc)
+{
+	// Do not deactivate anything in tactics mode
+	// This is battlefield setup spells
+	if(!tacticsMode)
+	{
+		windowObject->blockUI(true);
+
+		// Disable current active stack duing the cast
+		// Store the current activeStack to stackToActivate
+		stacksController->deactivateStack();
+	}
+
+	CCS->curh->set(Cursor::Combat::BLOCKED);
+
+	const SpellID spellID = sc->spellID;
+	const CSpell * spell = spellID.toSpell();
+	auto targetedTile = sc->tile;
+
+	assert(spell);
+	if(!spell)
+		return;
+
+	const AudioPath & castSoundPath = spell->getCastSound();
+
+	if (!castSoundPath.empty())
+	{
+		auto group = spell->animationInfo.projectile.empty() ?
+					EAnimationEvents::HIT:
+					EAnimationEvents::BEFORE_HIT;//FIXME: recheck whether this should be on projectile spawning
+
+		addToAnimationStage(group, [=]() {
+			CCS->soundh->playSound(castSoundPath);
+		});
+	}
+
+	if ( sc->activeCast )
+	{
+		const CStack * casterStack = getBattle()->battleGetStackByID(sc->casterStack);
+
+		if(casterStack != nullptr )
+		{
+			addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]()
+			{
+				stacksController->addNewAnim(new CastAnimation(*this, casterStack, targetedTile, getBattle()->battleGetStackByPos(targetedTile), spell));
+				displaySpellCast(spell, casterStack->getPosition());
+			});
+		}
+		else
+		{
+			auto hero = sc->side ? defendingHero : attackingHero;
+			assert(hero);
+
+			addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]()
+			{
+				stacksController->addNewAnim(new HeroCastAnimation(*this, hero, targetedTile, getBattle()->battleGetStackByPos(targetedTile), spell));
+			});
+		}
+	}
+
+	addToAnimationStage(EAnimationEvents::HIT, [=](){
+		displaySpellHit(spell, targetedTile);
+	});
+
+	//queuing affect animation
+	for(auto & elem : sc->affectedCres)
+	{
+		auto stack = getBattle()->battleGetStackByID(elem, false);
+		assert(stack);
+		if(stack)
+		{
+			addToAnimationStage(EAnimationEvents::HIT, [=](){
+				displaySpellEffect(spell, stack->getPosition());
+			});
+		}
+	}
+
+	for(auto & elem : sc->reflectedCres)
+	{
+		auto stack = getBattle()->battleGetStackByID(elem, false);
+		assert(stack);
+		addToAnimationStage(EAnimationEvents::HIT, [=](){
+			effectsController->displayEffect(EBattleEffect::MAGIC_MIRROR, stack->getPosition());
+		});
+	}
+
+	if (!sc->resistedCres.empty())
+	{
+		addToAnimationStage(EAnimationEvents::HIT, [=](){
+			CCS->soundh->playSound(AudioPath::builtin("MAGICRES"));
+		});
+	}
+
+	for(auto & elem : sc->resistedCres)
+	{
+		auto stack = getBattle()->battleGetStackByID(elem, false);
+		assert(stack);
+		addToAnimationStage(EAnimationEvents::HIT, [=](){
+			effectsController->displayEffect(EBattleEffect::RESISTANCE, stack->getPosition());
+		});
+	}
+
+	//mana absorption
+	if (sc->manaGained > 0)
+	{
+		Point leftHero = Point(15, 30);
+		Point rightHero = Point(755, 30);
+		bool side = sc->side;
+
+		addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){
+			stacksController->addNewAnim(new EffectAnimation(*this, AnimationPath::builtin(side ? "SP07_A.DEF" : "SP07_B.DEF"), leftHero));
+			stacksController->addNewAnim(new EffectAnimation(*this, AnimationPath::builtin(side ? "SP07_B.DEF" : "SP07_A.DEF"), rightHero));
+		});
+	}
+
+	// animations will be executed by spell effects
+}
+
+void BattleInterface::battleStacksEffectsSet(const SetStackEffect & sse)
+{
+	if(stacksController->getActiveStack() != nullptr)
+		fieldController->redrawBackgroundWithHexes();
+}
+
+void BattleInterface::setHeroAnimation(ui8 side, EHeroAnimType phase)
+{
+	if(side == BattleSide::ATTACKER)
+	{
+		if(attackingHero)
+			attackingHero->setPhase(phase);
+	}
+	else
+	{
+		if(defendingHero)
+			defendingHero->setPhase(phase);
+	}
+}
+
+void BattleInterface::displayBattleLog(const std::vector<MetaString> & battleLog)
+{
+	for(const auto & line : battleLog)
+	{
+		std::string formatted = line.toString();
+		boost::algorithm::trim(formatted);
+		appendBattleLog(formatted);
+	}
+}
+
+void BattleInterface::displaySpellAnimationQueue(const CSpell * spell, const CSpell::TAnimationQueue & q, BattleHex destinationTile, bool isHit)
+{
+	for(const CSpell::TAnimation & animation : q)
+	{
+		if(animation.pause > 0)
+			stacksController->addNewAnim(new DummyAnimation(*this, animation.pause));
+
+		if (!animation.effectName.empty())
+		{
+			const CStack * destStack = getBattle()->battleGetStackByPos(destinationTile, false);
+
+			if (destStack)
+				stacksController->addNewAnim(new ColorTransformAnimation(*this, destStack, animation.effectName, spell ));
+		}
+
+		if(!animation.resourceName.empty())
+		{
+			int flags = 0;
+
+			if (isHit)
+				flags |= EffectAnimation::FORCE_ON_TOP;
+
+			if (animation.verticalPosition == VerticalPosition::BOTTOM)
+				flags |= EffectAnimation::ALIGN_TO_BOTTOM;
+
+			if (!destinationTile.isValid())
+				flags |= EffectAnimation::SCREEN_FILL;
+
+			if (!destinationTile.isValid())
+				stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, flags));
+			else
+				stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, destinationTile, flags));
+		}
+	}
+}
+
+void BattleInterface::displaySpellCast(const CSpell * spell, BattleHex destinationTile)
+{
+	if(spell)
+		displaySpellAnimationQueue(spell, spell->animationInfo.cast, destinationTile, false);
+}
+
+void BattleInterface::displaySpellEffect(const CSpell * spell, BattleHex destinationTile)
+{
+	if(spell)
+		displaySpellAnimationQueue(spell, spell->animationInfo.affect, destinationTile, false);
+}
+
+void BattleInterface::displaySpellHit(const CSpell * spell, BattleHex destinationTile)
+{
+	if(spell)
+		displaySpellAnimationQueue(spell, spell->animationInfo.hit, destinationTile, true);
+}
+
+CPlayerInterface *BattleInterface::getCurrentPlayerInterface() const
+{
+	return curInt.get();
+}
+
+void BattleInterface::trySetActivePlayer( PlayerColor player )
+{
+	if ( attackerInt && attackerInt->playerID == player )
+		curInt = attackerInt;
+
+	if ( defenderInt && defenderInt->playerID == player )
+		curInt = defenderInt;
+}
+
+void BattleInterface::activateStack()
+{
+	stacksController->activateStack();
+
+	const CStack * s = stacksController->getActiveStack();
+	if(!s)
+		return;
+
+	windowObject->updateQueue();
+	windowObject->blockUI(false);
+	fieldController->redrawBackgroundWithHexes();
+	actionsController->activateStack();
+	GH.fakeMouseMove();
+}
+
+bool BattleInterface::makingTurn() const
+{
+	return stacksController->getActiveStack() != nullptr;
+}
+
+BattleID BattleInterface::getBattleID() const
+{
+	return battleID;
+}
+
+std::shared_ptr<CPlayerBattleCallback> BattleInterface::getBattle() const
+{
+	return curInt->cb->getBattle(battleID);
+}
+
+void BattleInterface::endAction(const BattleAction &action)
+{
+	// it is possible that tactics mode ended while opening music is still playing
+	waitForAnimations();
+
+	const CStack *stack = getBattle()->battleGetStackByID(action.stackNumber);
+
+	// Activate stack from stackToActivate because this might have been temporary disabled, e.g., during spell cast
+	activateStack();
+
+	stacksController->endAction(action);
+	windowObject->updateQueue();
+
+	//stack ended movement in tactics phase -> select the next one
+	if (tacticsMode)
+		tacticNextStack(stack);
+
+	//we have activated next stack after sending request that has been just realized -> blockmap due to movement has changed
+	if(action.actionType == EActionType::HERO_SPELL)
+		fieldController->redrawBackgroundWithHexes();
+}
+
+void BattleInterface::appendBattleLog(const std::string & newEntry)
+{
+	console->addText(newEntry);
+}
+
+void BattleInterface::startAction(const BattleAction & action)
+{
+	if(action.actionType == EActionType::END_TACTIC_PHASE)
+	{
+		windowObject->tacticPhaseEnded();
+		return;
+	}
+
+	stacksController->startAction(action);
+
+	if (!action.isUnitAction())
+		return;
+
+	assert(getBattle()->battleGetStackByID(action.stackNumber));
+	windowObject->updateQueue();
+	effectsController->startAction(action);
+}
+
+void BattleInterface::tacticPhaseEnd()
+{
+	stacksController->setActiveStack(nullptr);
+	tacticsMode = false;
+
+	auto side = tacticianInterface->cb->getBattle(battleID)->playerToSide(tacticianInterface->playerID);
+	auto action = BattleAction::makeEndOFTacticPhase(*side);
+
+	tacticianInterface->cb->battleMakeTacticAction(battleID, action);
+}
+
+static bool immobile(const CStack *s)
+{
+	return !s->speed(0, true); //should bound stacks be immobile?
+}
+
+void BattleInterface::tacticNextStack(const CStack * current)
+{
+	if (!current)
+		current = stacksController->getActiveStack();
+
+	//no switching stacks when the current one is moving
+	checkForAnimations();
+
+	TStacks stacksOfMine = tacticianInterface->cb->getBattle(battleID)->battleGetStacks(CPlayerBattleCallback::ONLY_MINE);
+	vstd::erase_if (stacksOfMine, &immobile);
+	if (stacksOfMine.empty())
+	{
+		tacticPhaseEnd();
+		return;
+	}
+
+	auto it = vstd::find(stacksOfMine, current);
+	if (it != stacksOfMine.end() && ++it != stacksOfMine.end())
+		stackActivated(*it);
+	else
+		stackActivated(stacksOfMine.front());
+
+}
+
+void BattleInterface::obstaclePlaced(const std::vector<std::shared_ptr<const CObstacleInstance>> oi)
+{
+	obstacleController->obstaclePlaced(oi);
+}
+
+void BattleInterface::obstacleRemoved(const std::vector<ObstacleChanges> & obstacles)
+{
+	obstacleController->obstacleRemoved(obstacles);
+}
+
+const CGHeroInstance *BattleInterface::currentHero() const
+{
+	if (attackingHeroInstance && attackingHeroInstance->tempOwner == curInt->playerID)
+		return attackingHeroInstance;
+
+	if (defendingHeroInstance && defendingHeroInstance->tempOwner == curInt->playerID)
+		return defendingHeroInstance;
+
+	return nullptr;
+}
+
+InfoAboutHero BattleInterface::enemyHero() const
+{
+	InfoAboutHero ret;
+	if (attackingHeroInstance->tempOwner == curInt->playerID)
+		curInt->cb->getHeroInfo(defendingHeroInstance, ret);
+	else
+		curInt->cb->getHeroInfo(attackingHeroInstance, ret);
+
+	return ret;
+}
+
+void BattleInterface::requestAutofightingAIToTakeAction()
+{
+	assert(curInt->isAutoFightOn);
+
+	if(getBattle()->battleIsFinished())
+	{
+		return; // battle finished with spellcast
+	}
+
+	if (tacticsMode)
+	{
+		// Always end tactics mode. Player interface is blocked currently, so it's not possible that
+		// the AI can take any action except end tactics phase (AI actions won't be triggered)
+		//TODO implement the possibility that the AI will be triggered for further actions
+		//TODO any solution to merge tactics phase & normal phase in the way it is handled by the player and battle interface?
+		tacticPhaseEnd();
+		stacksController->setActiveStack(nullptr);
+	}
+	else
+	{
+		const CStack* activeStack = stacksController->getActiveStack();
+
+		// If enemy is moving, activeStack can be null
+		if (activeStack)
+		{
+			stacksController->setActiveStack(nullptr);
+
+			// FIXME: unsafe
+			// Run task in separate thread to avoid UI lock while AI is making turn (which might take some time)
+			// HOWEVER this thread won't atttempt to lock game state, potentially leading to races
+			boost::thread aiThread([this, activeStack]()
+			{
+				setThreadName("autofightingAI");
+				curInt->autofightingAI->activeStack(battleID, activeStack);
+			});
+			aiThread.detach();
+		}
+	}
+}
+
+void BattleInterface::castThisSpell(SpellID spellID)
+{
+	actionsController->castThisSpell(spellID);
+}
+
+void BattleInterface::executeStagedAnimations()
+{
+	EAnimationEvents earliestStage = EAnimationEvents::COUNT;
+
+	for(const auto & event : awaitingEvents)
+		earliestStage = std::min(earliestStage, event.event);
+
+	if(earliestStage != EAnimationEvents::COUNT)
+		executeAnimationStage(earliestStage);
+}
+
+void BattleInterface::executeAnimationStage(EAnimationEvents event)
+{
+	decltype(awaitingEvents) executingEvents;
+
+	for(auto it = awaitingEvents.begin(); it != awaitingEvents.end();)
+	{
+		if(it->event == event)
+		{
+			executingEvents.push_back(*it);
+			it = awaitingEvents.erase(it);
+		}
+		else
+			++it;
+	}
+	for(const auto & event : executingEvents)
+		event.action();
+}
+
+void BattleInterface::onAnimationsStarted()
+{
+	ongoingAnimationsState.setn(true);
+}
+
+void BattleInterface::onAnimationsFinished()
+{
+	ongoingAnimationsState.setn(false);
+}
+
+void BattleInterface::waitForAnimations()
+{
+	{
+		auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex);
+		ongoingAnimationsState.waitUntil(false);
+	}
+
+	assert(!hasAnimations());
+	assert(awaitingEvents.empty());
+
+	if (!awaitingEvents.empty())
+	{
+		logGlobal->error("Wait for animations finished but we still have awaiting events!");
+		awaitingEvents.clear();
+	}
+}
+
+bool BattleInterface::hasAnimations()
+{
+	return ongoingAnimationsState.get();
+}
+
+void BattleInterface::checkForAnimations()
+{
+	assert(!hasAnimations());
+	if(hasAnimations())
+		logGlobal->error("Unexpected animations state: expected all animations to be over, but some are still ongoing!");
+
+	waitForAnimations();
+}
+
+void BattleInterface::addToAnimationStage(EAnimationEvents event, const AwaitingAnimationAction & action)
+{
+	awaitingEvents.push_back({action, event});
+}
+
+void BattleInterface::setBattleQueueVisibility(bool visible)
+{
+	windowObject->hideQueue();
+	if(visible)
+		windowObject->showQueue();
+}
+
+void BattleInterface::setStickyHeroWindowsVisibility(bool visible)
+{
+	windowObject->hideStickyHeroWindows();
+	if(visible)
+		windowObject->showStickyHeroWindows();
+}

+ 230 - 230
client/battle/BattleInterface.h

@@ -1,230 +1,230 @@
-/*
- * BattleInterface.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 "BattleConstants.h"
-#include "../gui/CIntObject.h"
-#include "../../lib/spells/CSpellHandler.h" //CSpell::TAnimation
-#include "../../lib/CondSh.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-class CCreatureSet;
-class CGHeroInstance;
-class CStack;
-struct BattleResult;
-struct BattleSpellCast;
-struct CObstacleInstance;
-struct SetStackEffect;
-class BattleAction;
-class CGTownInstance;
-struct CatapultAttack;
-struct BattleTriggerEffect;
-struct BattleHex;
-struct InfoAboutHero;
-class ObstacleChanges;
-class CPlayerBattleCallback;
-
-VCMI_LIB_NAMESPACE_END
-
-class BattleHero;
-class Canvas;
-class BattleResultWindow;
-class StackQueue;
-class CPlayerInterface;
-class CAnimation;
-struct BattleEffect;
-class IImage;
-class StackQueue;
-
-class BattleProjectileController;
-class BattleSiegeController;
-class BattleObstacleController;
-class BattleFieldController;
-class BattleRenderer;
-class BattleWindow;
-class BattleStacksController;
-class BattleActionsController;
-class BattleEffectsController;
-class BattleConsole;
-
-/// Small struct which contains information about the id of the attacked stack, the damage dealt,...
-struct StackAttackedInfo
-{
-	const CStack *defender;
-	const CStack *attacker;
-
-	int64_t  damageDealt;
-	uint32_t amountKilled;
-	SpellID spellEffect;
-
-	bool indirectAttack; //if true, stack was attacked indirectly - spell or ranged attack
-	bool killed; //if true, stack has been killed
-	bool rebirth; //if true, play rebirth animation after all
-	bool cloneKilled;
-	bool fireShield;
-};
-
-struct StackAttackInfo
-{
-	const CStack *attacker;
-	const CStack *defender;
-	std::vector< const CStack *> secondaryDefender;
-
-	SpellID spellEffect;
-	BattleHex tile;
-
-	bool indirectAttack;
-	bool lucky;
-	bool unlucky;
-	bool deathBlow;
-	bool lifeDrain;
-};
-
-/// Main class for battles, responsible for relaying information from server to various battle entities
-class BattleInterface
-{
-	using AwaitingAnimationAction = std::function<void()>;
-
-	struct AwaitingAnimationEvents {
-		AwaitingAnimationAction action;
-		EAnimationEvents event;
-	};
-
-	/// Conditional variables that are set depending on ongoing animations on the battlefield
-	CondSh<bool> ongoingAnimationsState;
-
-	/// List of events that are waiting to be triggered
-	std::vector<AwaitingAnimationEvents> awaitingEvents;
-
-	/// used during tactics mode, points to the interface of player with higher tactics (can be either attacker or defender in hot-seat), valid onloy for human players
-	std::shared_ptr<CPlayerInterface> tacticianInterface;
-
-	/// attacker interface, not null if attacker is human in our vcmiclient
-	std::shared_ptr<CPlayerInterface> attackerInt;
-
-	/// defender interface, not null if attacker is human in our vcmiclient
-	std::shared_ptr<CPlayerInterface> defenderInt;
-
-	/// if set to true, battle is still starting and waiting for intro sound to end / key press from player
-	bool battleOpeningDelayActive;
-
-	/// ID of ongoing battle
-	BattleID battleID;
-
-	void playIntroSoundAndUnlockInterface();
-	void onIntroSoundPlayed();
-public:
-	/// copy of initial armies (for result window)
-	const CCreatureSet *army1;
-	const CCreatureSet *army2;
-
-	std::shared_ptr<BattleWindow> windowObject;
-	std::shared_ptr<BattleConsole> console;
-
-	/// currently active player interface
-	std::shared_ptr<CPlayerInterface> curInt;
-
-	const CGHeroInstance *attackingHeroInstance;
-	const CGHeroInstance *defendingHeroInstance;
-
-	bool tacticsMode;
-
-	std::unique_ptr<BattleProjectileController> projectilesController;
-	std::unique_ptr<BattleSiegeController> siegeController;
-	std::unique_ptr<BattleObstacleController> obstacleController;
-	std::unique_ptr<BattleFieldController> fieldController;
-	std::unique_ptr<BattleStacksController> stacksController;
-	std::unique_ptr<BattleActionsController> actionsController;
-	std::unique_ptr<BattleEffectsController> effectsController;
-
-	std::shared_ptr<BattleHero> attackingHero;
-	std::shared_ptr<BattleHero> defendingHero;
-
-	bool openingPlaying();
-	void openingEnd();
-
-	bool makingTurn() const;
-
-	BattleID getBattleID() const;
-	std::shared_ptr<CPlayerBattleCallback> getBattle() const;
-
-	BattleInterface(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, std::shared_ptr<CPlayerInterface> att, std::shared_ptr<CPlayerInterface> defen, std::shared_ptr<CPlayerInterface> spectatorInt = nullptr);
-	~BattleInterface();
-
-	void trySetActivePlayer( PlayerColor player ); // if in hotseat, will activate interface of chosen player
-	void activateStack(); //sets activeStack to stackToActivate etc. //FIXME: No, it's not clear at all
-	void requestAutofightingAIToTakeAction();
-
-	void giveCommand(EActionType action, BattleHex tile = BattleHex(), SpellID spell = SpellID::NONE);
-	void sendCommand(BattleAction command, const CStack * actor = nullptr);
-
-	const CGHeroInstance *getActiveHero(); //returns hero that can currently cast a spell
-
-	void showInterface(Canvas & to);
-
-	void setHeroAnimation(ui8 side, EHeroAnimType phase);
-
-	void executeSpellCast(); //called when a hero casts a spell
-
-	void appendBattleLog(const std::string & newEntry);
-
-	void redrawBattlefield(); //refresh GUI after changing stack range / grid settings
-	CPlayerInterface *getCurrentPlayerInterface() const;
-
-	void tacticNextStack(const CStack *current);
-	void tacticPhaseEnd();
-
-	void setBattleQueueVisibility(bool visible);
-	void setStickyHeroWindowsVisibility(bool visible);
-
-	void executeStagedAnimations();
-	void executeAnimationStage( EAnimationEvents event);
-	void onAnimationsStarted();
-	void onAnimationsFinished();
-	void waitForAnimations();
-	bool hasAnimations();
-	void checkForAnimations();
-	void addToAnimationStage( EAnimationEvents event, const AwaitingAnimationAction & action);
-
-	//call-ins
-	void startAction(const BattleAction & action);
-	void stackReset(const CStack * stack);
-	void stackAdded(const CStack * stack); //new stack appeared on battlefield
-	void stackRemoved(uint32_t stackID); //stack disappeared from batlefiled
-	void stackActivated(const CStack *stack); //active stack has been changed
-	void stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance, bool teleport); //stack with id number moved to destHex
-	void stacksAreAttacked(std::vector<StackAttackedInfo> attackedInfos); //called when a certain amount of stacks has been attacked
-	void stackAttacking(const StackAttackInfo & attackInfo); //called when stack with id ID is attacking something on hex dest
-	void newRoundFirst();
-	void newRound(); //caled when round is ended;
-	void stackIsCatapulting(const CatapultAttack & ca); //called when a stack is attacking walls
-	void battleFinished(const BattleResult& br, QueryID queryID); //called when battle is finished - battleresult window should be printed
-	void spellCast(const BattleSpellCast *sc); //called when a hero casts a spell
-	void battleStacksEffectsSet(const SetStackEffect & sse); //called when a specific effect is set to stacks
-	void castThisSpell(SpellID spellID); //called when player has chosen a spell from spellbook
-
-	void displayBattleLog(const std::vector<MetaString> & battleLog);
-
-	void displaySpellAnimationQueue(const CSpell * spell, const CSpell::TAnimationQueue & q, BattleHex destinationTile, bool isHit);
-	void displaySpellCast(const CSpell * spell, BattleHex destinationTile); //displays spell`s cast animation
-	void displaySpellEffect(const CSpell * spell, BattleHex destinationTile); //displays spell`s affected animation
-	void displaySpellHit(const CSpell * spell, BattleHex destinationTile); //displays spell`s affected animation
-
-	void endAction(const BattleAction & action);
-
-	void obstaclePlaced(const std::vector<std::shared_ptr<const CObstacleInstance>> oi);
-	void obstacleRemoved(const std::vector<ObstacleChanges> & obstacles);
-
-	void gateStateChanged(const EGateState state);
-
-	const CGHeroInstance *currentHero() const;
-	InfoAboutHero enemyHero() const;
-};
+/*
+ * BattleInterface.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 "BattleConstants.h"
+#include "../gui/CIntObject.h"
+#include "../../lib/spells/CSpellHandler.h" //CSpell::TAnimation
+#include "../../lib/CondSh.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class CCreatureSet;
+class CGHeroInstance;
+class CStack;
+struct BattleResult;
+struct BattleSpellCast;
+struct CObstacleInstance;
+struct SetStackEffect;
+class BattleAction;
+class CGTownInstance;
+struct CatapultAttack;
+struct BattleTriggerEffect;
+struct BattleHex;
+struct InfoAboutHero;
+class ObstacleChanges;
+class CPlayerBattleCallback;
+
+VCMI_LIB_NAMESPACE_END
+
+class BattleHero;
+class Canvas;
+class BattleResultWindow;
+class StackQueue;
+class CPlayerInterface;
+class CAnimation;
+struct BattleEffect;
+class IImage;
+class StackQueue;
+
+class BattleProjectileController;
+class BattleSiegeController;
+class BattleObstacleController;
+class BattleFieldController;
+class BattleRenderer;
+class BattleWindow;
+class BattleStacksController;
+class BattleActionsController;
+class BattleEffectsController;
+class BattleConsole;
+
+/// Small struct which contains information about the id of the attacked stack, the damage dealt,...
+struct StackAttackedInfo
+{
+	const CStack *defender;
+	const CStack *attacker;
+
+	int64_t  damageDealt;
+	uint32_t amountKilled;
+	SpellID spellEffect;
+
+	bool indirectAttack; //if true, stack was attacked indirectly - spell or ranged attack
+	bool killed; //if true, stack has been killed
+	bool rebirth; //if true, play rebirth animation after all
+	bool cloneKilled;
+	bool fireShield;
+};
+
+struct StackAttackInfo
+{
+	const CStack *attacker;
+	const CStack *defender;
+	std::vector< const CStack *> secondaryDefender;
+
+	SpellID spellEffect;
+	BattleHex tile;
+
+	bool indirectAttack;
+	bool lucky;
+	bool unlucky;
+	bool deathBlow;
+	bool lifeDrain;
+};
+
+/// Main class for battles, responsible for relaying information from server to various battle entities
+class BattleInterface
+{
+	using AwaitingAnimationAction = std::function<void()>;
+
+	struct AwaitingAnimationEvents {
+		AwaitingAnimationAction action;
+		EAnimationEvents event;
+	};
+
+	/// Conditional variables that are set depending on ongoing animations on the battlefield
+	CondSh<bool> ongoingAnimationsState;
+
+	/// List of events that are waiting to be triggered
+	std::vector<AwaitingAnimationEvents> awaitingEvents;
+
+	/// used during tactics mode, points to the interface of player with higher tactics (can be either attacker or defender in hot-seat), valid onloy for human players
+	std::shared_ptr<CPlayerInterface> tacticianInterface;
+
+	/// attacker interface, not null if attacker is human in our vcmiclient
+	std::shared_ptr<CPlayerInterface> attackerInt;
+
+	/// defender interface, not null if attacker is human in our vcmiclient
+	std::shared_ptr<CPlayerInterface> defenderInt;
+
+	/// if set to true, battle is still starting and waiting for intro sound to end / key press from player
+	bool battleOpeningDelayActive;
+
+	/// ID of ongoing battle
+	BattleID battleID;
+
+	void playIntroSoundAndUnlockInterface();
+	void onIntroSoundPlayed();
+public:
+	/// copy of initial armies (for result window)
+	const CCreatureSet *army1;
+	const CCreatureSet *army2;
+
+	std::shared_ptr<BattleWindow> windowObject;
+	std::shared_ptr<BattleConsole> console;
+
+	/// currently active player interface
+	std::shared_ptr<CPlayerInterface> curInt;
+
+	const CGHeroInstance *attackingHeroInstance;
+	const CGHeroInstance *defendingHeroInstance;
+
+	bool tacticsMode;
+
+	std::unique_ptr<BattleProjectileController> projectilesController;
+	std::unique_ptr<BattleSiegeController> siegeController;
+	std::unique_ptr<BattleObstacleController> obstacleController;
+	std::unique_ptr<BattleFieldController> fieldController;
+	std::unique_ptr<BattleStacksController> stacksController;
+	std::unique_ptr<BattleActionsController> actionsController;
+	std::unique_ptr<BattleEffectsController> effectsController;
+
+	std::shared_ptr<BattleHero> attackingHero;
+	std::shared_ptr<BattleHero> defendingHero;
+
+	bool openingPlaying();
+	void openingEnd();
+
+	bool makingTurn() const;
+
+	BattleID getBattleID() const;
+	std::shared_ptr<CPlayerBattleCallback> getBattle() const;
+
+	BattleInterface(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, std::shared_ptr<CPlayerInterface> att, std::shared_ptr<CPlayerInterface> defen, std::shared_ptr<CPlayerInterface> spectatorInt = nullptr);
+	~BattleInterface();
+
+	void trySetActivePlayer( PlayerColor player ); // if in hotseat, will activate interface of chosen player
+	void activateStack(); //sets activeStack to stackToActivate etc. //FIXME: No, it's not clear at all
+	void requestAutofightingAIToTakeAction();
+
+	void giveCommand(EActionType action, BattleHex tile = BattleHex(), SpellID spell = SpellID::NONE);
+	void sendCommand(BattleAction command, const CStack * actor = nullptr);
+
+	const CGHeroInstance *getActiveHero(); //returns hero that can currently cast a spell
+
+	void showInterface(Canvas & to);
+
+	void setHeroAnimation(ui8 side, EHeroAnimType phase);
+
+	void executeSpellCast(); //called when a hero casts a spell
+
+	void appendBattleLog(const std::string & newEntry);
+
+	void redrawBattlefield(); //refresh GUI after changing stack range / grid settings
+	CPlayerInterface *getCurrentPlayerInterface() const;
+
+	void tacticNextStack(const CStack *current);
+	void tacticPhaseEnd();
+
+	void setBattleQueueVisibility(bool visible);
+	void setStickyHeroWindowsVisibility(bool visible);
+
+	void executeStagedAnimations();
+	void executeAnimationStage( EAnimationEvents event);
+	void onAnimationsStarted();
+	void onAnimationsFinished();
+	void waitForAnimations();
+	bool hasAnimations();
+	void checkForAnimations();
+	void addToAnimationStage( EAnimationEvents event, const AwaitingAnimationAction & action);
+
+	//call-ins
+	void startAction(const BattleAction & action);
+	void stackReset(const CStack * stack);
+	void stackAdded(const CStack * stack); //new stack appeared on battlefield
+	void stackRemoved(uint32_t stackID); //stack disappeared from batlefiled
+	void stackActivated(const CStack *stack); //active stack has been changed
+	void stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance, bool teleport); //stack with id number moved to destHex
+	void stacksAreAttacked(std::vector<StackAttackedInfo> attackedInfos); //called when a certain amount of stacks has been attacked
+	void stackAttacking(const StackAttackInfo & attackInfo); //called when stack with id ID is attacking something on hex dest
+	void newRoundFirst();
+	void newRound(); //caled when round is ended;
+	void stackIsCatapulting(const CatapultAttack & ca); //called when a stack is attacking walls
+	void battleFinished(const BattleResult& br, QueryID queryID); //called when battle is finished - battleresult window should be printed
+	void spellCast(const BattleSpellCast *sc); //called when a hero casts a spell
+	void battleStacksEffectsSet(const SetStackEffect & sse); //called when a specific effect is set to stacks
+	void castThisSpell(SpellID spellID); //called when player has chosen a spell from spellbook
+
+	void displayBattleLog(const std::vector<MetaString> & battleLog);
+
+	void displaySpellAnimationQueue(const CSpell * spell, const CSpell::TAnimationQueue & q, BattleHex destinationTile, bool isHit);
+	void displaySpellCast(const CSpell * spell, BattleHex destinationTile); //displays spell`s cast animation
+	void displaySpellEffect(const CSpell * spell, BattleHex destinationTile); //displays spell`s affected animation
+	void displaySpellHit(const CSpell * spell, BattleHex destinationTile); //displays spell`s affected animation
+
+	void endAction(const BattleAction & action);
+
+	void obstaclePlaced(const std::vector<std::shared_ptr<const CObstacleInstance>> oi);
+	void obstacleRemoved(const std::vector<ObstacleChanges> & obstacles);
+
+	void gateStateChanged(const EGateState state);
+
+	const CGHeroInstance *currentHero() const;
+	InfoAboutHero enemyHero() const;
+};

+ 921 - 921
client/battle/BattleInterfaceClasses.cpp

@@ -1,921 +1,921 @@
-/*
- * BattleInterfaceClasses.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 "BattleInterfaceClasses.h"
-
-#include "BattleInterface.h"
-#include "BattleActionsController.h"
-#include "BattleRenderer.h"
-#include "BattleSiegeController.h"
-#include "BattleFieldController.h"
-#include "BattleStacksController.h"
-#include "BattleWindow.h"
-
-#include "../CGameInfo.h"
-#include "../CMusicHandler.h"
-#include "../CPlayerInterface.h"
-#include "../CVideoHandler.h"
-#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 "../render/IFont.h"
-#include "../render/Graphics.h"
-#include "../widgets/Buttons.h"
-#include "../widgets/Images.h"
-#include "../widgets/TextControls.h"
-#include "../windows/CMessage.h"
-#include "../windows/CSpellWindow.h"
-#include "../render/CAnimation.h"
-#include "../render/IRenderHandler.h"
-#include "../adventureMap/CInGameConsole.h"
-
-#include "../../CCallback.h"
-#include "../../lib/CStack.h"
-#include "../../lib/CConfigHandler.h"
-#include "../../lib/CCreatureHandler.h"
-#include "../../lib/gameState/InfoAboutArmy.h"
-#include "../../lib/CGeneralTextHandler.h"
-#include "../../lib/CTownHandler.h"
-#include "../../lib/CHeroHandler.h"
-#include "../../lib/NetPacks.h"
-#include "../../lib/StartInfo.h"
-#include "../../lib/CondSh.h"
-#include "../../lib/mapObjects/CGTownInstance.h"
-#include "../../lib/TextOperations.h"
-
-void BattleConsole::showAll(Canvas & to)
-{
-	CIntObject::showAll(to);
-
-	Point line1 (pos.x + pos.w/2, pos.y +  8);
-	Point line2 (pos.x + pos.w/2, pos.y + 24);
-
-	auto visibleText = getVisibleText();
-
-	if(visibleText.size() > 0)
-		to.drawText(line1, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, visibleText[0]);
-
-	if(visibleText.size() > 1)
-		to.drawText(line2, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, visibleText[1]);
-}
-
-std::vector<std::string> BattleConsole::getVisibleText()
-{
-	// high priority texts that hide battle log entries
-	for(const auto & text : {consoleText, hoverText})
-	{
-		if (text.empty())
-			continue;
-
-		auto result = CMessage::breakText(text, pos.w, FONT_SMALL);
-
-		if(result.size() > 2)
-			result.resize(2);
-		return result;
-	}
-
-	// log is small enough to fit entirely - display it as such
-	if (logEntries.size() < 3)
-		return logEntries;
-
-	return { logEntries[scrollPosition - 1], logEntries[scrollPosition] };
-}
-
-std::vector<std::string> BattleConsole::splitText(const std::string &text)
-{
-	std::vector<std::string> lines;
-	std::vector<std::string> output;
-
-	boost::split(lines, text, boost::is_any_of("\n"));
-
-	for(const auto & line : lines)
-	{
-		if (graphics->fonts[FONT_SMALL]->getStringWidth(text) < pos.w)
-		{
-			output.push_back(line);
-		}
-		else
-		{
-			std::vector<std::string> substrings = CMessage::breakText(line, pos.w, FONT_SMALL);
-			output.insert(output.end(), substrings.begin(), substrings.end());
-		}
-	}
-	return output;
-}
-
-bool BattleConsole::addText(const std::string & text)
-{
-	logGlobal->trace("CBattleConsole message: %s", text);
-
-	auto newLines = splitText(text);
-
-	logEntries.insert(logEntries.end(), newLines.begin(), newLines.end());
-	scrollPosition = (int)logEntries.size()-1;
-	redraw();
-	return true;
-}
-void BattleConsole::scrollUp(ui32 by)
-{
-	if(scrollPosition > static_cast<int>(by))
-		scrollPosition -= by;
-	redraw();
-}
-
-void BattleConsole::scrollDown(ui32 by)
-{
-	if(scrollPosition + by < logEntries.size())
-		scrollPosition += by;
-	redraw();
-}
-
-BattleConsole::BattleConsole(std::shared_ptr<CPicture> backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size)
-	: scrollPosition(-1)
-	, enteringText(false)
-{
-	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
-	pos += objectPos;
-	pos.w = size.x;
-	pos.h = size.y;
-
-	background = std::make_shared<CPicture>(backgroundSource->getSurface(), Rect(imagePos, size), 0, 0 );
-}
-
-void BattleConsole::deactivate()
-{
-	if (enteringText)
-		LOCPLINT->cingconsole->endEnteringText(false);
-
-	CIntObject::deactivate();
-}
-
-void BattleConsole::setEnteringMode(bool on)
-{
-	consoleText.clear();
-
-	if (on)
-	{
-		assert(enteringText == false);
-		GH.startTextInput(pos);
-	}
-	else
-	{
-		assert(enteringText == true);
-		GH.stopTextInput();
-	}
-	enteringText = on;
-	redraw();
-}
-
-void BattleConsole::setEnteredText(const std::string & text)
-{
-	assert(enteringText == true);
-	consoleText = text;
-	redraw();
-}
-
-void BattleConsole::write(const std::string & Text)
-{
-	hoverText = Text;
-	redraw();
-}
-
-void BattleConsole::clearIfMatching(const std::string & Text)
-{
-	if (hoverText == Text)
-		clear();
-}
-
-void BattleConsole::clear()
-{
-	write({});
-}
-
-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);
-
-	auto flagFrame = flagAnimation->getImage(flagCurrentFrame, 0, true);
-	auto heroFrame = animation->getImage(currentFrame, groupIndex, true);
-
-	Point heroPosition = pos.center() - parent->pos.topLeft() - heroFrame->dimensions() / 2;
-	Point flagPosition = pos.center() - parent->pos.topLeft() - flagFrame->dimensions() / 2;
-
-	if(defender)
-		flagPosition += Point(-4, -41);
-	else
-		flagPosition += Point(4, -41);
-
-	canvas.draw(flagFrame, flagPosition);
-	canvas.draw(heroFrame, heroPosition);
-}
-
-void BattleHero::pause()
-{
-	currentSpeed = 0.f;
-}
-
-void BattleHero::play()
-{
-	//H3 speed: 10 fps ( 100 ms per frame)
-	currentSpeed = 10.f;
-}
-
-float BattleHero::getFrame() const
-{
-	return currentFrame;
-}
-
-void BattleHero::collectRenderableObjects(BattleRenderer & renderer)
-{
-	auto hex = defender ? BattleHex(GameConstants::BFIELD_WIDTH-1) : BattleHex(0);
-
-	renderer.insert(EBattleFieldLayer::HEROES, hex, [this](BattleRenderer::RendererRef canvas)
-	{
-		render(canvas);
-	});
-}
-
-void BattleHero::onPhaseFinished(const std::function<void()> & callback)
-{
-	phaseFinishedCallback = callback;
-}
-
-void BattleHero::setPhase(EHeroAnimType newPhase)
-{
-	nextPhase = newPhase;
-	switchToNextPhase(); //immediately switch to next phase and then restore idling phase
-	nextPhase = EHeroAnimType::HOLDING;
-}
-
-void BattleHero::heroLeftClicked()
-{
-	if(owner.actionsController->spellcastingModeActive()) //we are casting a spell
-		return;
-
-	if(!hero || !owner.makingTurn())
-		return;
-
-	if(owner.getBattle()->battleCanCastSpell(hero, spells::Mode::HERO) == ESpellCastProblem::OK) //check conditions
-	{
-		CCS->curh->set(Cursor::Map::POINTER);
-		GH.windows().createAndPushWindow<CSpellWindow>(hero, owner.getCurrentPlayerInterface());
-	}
-}
-
-void BattleHero::heroRightClicked()
-{
-	if(settings["battle"]["stickyHeroInfoWindows"].Bool())
-		return;
-
-	Point windowPosition;
-	if(GH.screenDimensions().x < 1000)
-	{
-		windowPosition.x = (!defender) ? owner.fieldController->pos.left() + 1 : owner.fieldController->pos.right() - 79;
-		windowPosition.y = owner.fieldController->pos.y + 135;
-	}
-	else
-	{
-		windowPosition.x = (!defender) ? owner.fieldController->pos.left() - 93 : owner.fieldController->pos.right() + 15;
-		windowPosition.y = owner.fieldController->pos.y;
-	}
-
-	InfoAboutHero targetHero;
-	if(owner.makingTurn() || settings["session"]["spectate"].Bool())
-	{
-		auto h = defender ? owner.defendingHeroInstance : owner.attackingHeroInstance;
-		targetHero.initFromHero(h, InfoAboutHero::EInfoLevel::INBATTLE);
-		GH.windows().createAndPushWindow<HeroInfoWindow>(targetHero, &windowPosition);
-	}
-}
-
-void BattleHero::switchToNextPhase()
-{
-	phase = nextPhase;
-	currentFrame = 0.f;
-
-	auto copy = phaseFinishedCallback;
-	phaseFinishedCallback.clear();
-	copy();
-}
-
-BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender):
-	defender(defender),
-	hero(hero),
-	owner(owner),
-	phase(EHeroAnimType::HOLDING),
-	nextPhase(EHeroAnimType::HOLDING),
-	currentSpeed(0.f),
-	currentFrame(0.f),
-	flagCurrentFrame(0.f)
-{
-	AnimationPath animationPath;
-
-	if(!hero->type->battleImage.empty())
-		animationPath = hero->type->battleImage;
-	else
-	if(hero->gender == EHeroGender::FEMALE)
-		animationPath = hero->type->heroClass->imageBattleFemale;
-	else
-		animationPath = hero->type->heroClass->imageBattleMale;
-
-	animation = GH.renderHandler().loadAnimation(animationPath);
-	animation->preload();
-
-	pos.w = 64;
-	pos.h = 136;
-	pos.x = owner.fieldController->pos.x + (defender ? (owner.fieldController->pos.w - pos.w) : 0);
-	pos.y = owner.fieldController->pos.y;
-
-	if(defender)
-		animation->verticalFlip();
-
-	if(defender)
-		flagAnimation = GH.renderHandler().loadAnimation(AnimationPath::builtin("CMFLAGR"));
-	else
-		flagAnimation = GH.renderHandler().loadAnimation(AnimationPath::builtin("CMFLAGL"));
-
-	flagAnimation->preload();
-	flagAnimation->playerColored(hero->tempOwner);
-
-	switchToNextPhase();
-	play();
-
-	addUsedEvents(TIME);
-}
-
-HeroInfoBasicPanel::HeroInfoBasicPanel(const InfoAboutHero & hero, Point * position, bool initializeBackground)
-	: CIntObject(0)
-{
-	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
-	if (position != nullptr)
-		moveTo(*position);
-
-	if(initializeBackground)
-	{
-		background = std::make_shared<CPicture>(ImagePath::builtin("CHRPOP"));
-		background->getSurface()->setBlitMode(EImageBlitMode::OPAQUE);
-		background->colorize(hero.owner);
-	}
-
-	initializeData(hero);
-}
-
-void HeroInfoBasicPanel::initializeData(const InfoAboutHero & hero)
-{
-	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
-	auto attack = hero.details->primskills[0];
-	auto defense = hero.details->primskills[1];
-	auto power = hero.details->primskills[2];
-	auto knowledge = hero.details->primskills[3];
-	auto morale = hero.details->morale;
-	auto luck = hero.details->luck;
-	auto currentSpellPoints = hero.details->mana;
-	auto maxSpellPoints = hero.details->manaLimit;
-
-	icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("PortraitsLarge"), hero.getIconIndex(), 0, 10, 6));
-
-	//primary stats
-	labels.push_back(std::make_shared<CLabel>(9, 75, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[380] + ":"));
-	labels.push_back(std::make_shared<CLabel>(9, 87, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[381] + ":"));
-	labels.push_back(std::make_shared<CLabel>(9, 99, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[382] + ":"));
-	labels.push_back(std::make_shared<CLabel>(9, 111, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[383] + ":"));
-
-	labels.push_back(std::make_shared<CLabel>(69, 87, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(attack)));
-	labels.push_back(std::make_shared<CLabel>(69, 99, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(defense)));
-	labels.push_back(std::make_shared<CLabel>(69, 111, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(power)));
-	labels.push_back(std::make_shared<CLabel>(69, 123, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(knowledge)));
-
-	//morale+luck
-	labels.push_back(std::make_shared<CLabel>(9, 131, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[384] + ":"));
-	labels.push_back(std::make_shared<CLabel>(9, 143, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[385] + ":"));
-
-	icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("IMRL22"), morale + 3, 0, 47, 131));
-	icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("ILCK22"), luck + 3, 0, 47, 143));
-
-	//spell points
-	labels.push_back(std::make_shared<CLabel>(39, 174, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[387]));
-	labels.push_back(std::make_shared<CLabel>(39, 186, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, std::to_string(currentSpellPoints) + "/" + std::to_string(maxSpellPoints)));
-}
-
-void HeroInfoBasicPanel::update(const InfoAboutHero & updatedInfo)
-{
-	icons.clear();
-	labels.clear();
-
-	initializeData(updatedInfo);
-}
-
-void HeroInfoBasicPanel::show(Canvas & to)
-{
-	showAll(to);
-	CIntObject::show(to);
-}
-
-HeroInfoWindow::HeroInfoWindow(const InfoAboutHero & hero, Point * position)
-	: CWindowObject(RCLICK_POPUP | SHADOW_DISABLED, ImagePath::builtin("CHRPOP"))
-{
-	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
-	if (position != nullptr)
-		moveTo(*position);
-
-	background->colorize(hero.owner); //maybe add this functionality to base class?
-
-	content = std::make_shared<HeroInfoBasicPanel>(hero, nullptr, false);
-}
-
-BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay)
-	: owner(_owner), currentVideo(BattleResultVideo::NONE)
-{
-	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
-
-	background = std::make_shared<CPicture>(ImagePath::builtin("CPRESULT"));
-	background->colorize(owner.playerID);
-	pos = center(background->pos);
-
-	exit = std::make_shared<CButton>(Point(384, 505), AnimationPath::builtin("iok6432.def"), std::make_pair("", ""), [&](){ bExitf();}, EShortcut::GLOBAL_ACCEPT);
-	exit->setBorderColor(Colors::METALLIC_GOLD);
-	
-	if(allowReplay)
-	{
-		repeat = std::make_shared<CButton>(Point(24, 505), AnimationPath::builtin("icn6432.def"), std::make_pair("", ""), [&](){ bRepeatf();}, EShortcut::GLOBAL_CANCEL);
-		repeat->setBorderColor(Colors::METALLIC_GOLD);
-		labels.push_back(std::make_shared<CLabel>(232, 520, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("vcmi.battleResultsWindow.applyResultsLabel")));
-	}
-
-	if(br.winner == 0) //attacker won
-	{
-		labels.push_back(std::make_shared<CLabel>(59, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[410]));
-	}
-	else
-	{
-		labels.push_back(std::make_shared<CLabel>(59, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[411]));
-	}
-
-	if(br.winner == 1)
-	{
-		labels.push_back(std::make_shared<CLabel>(412, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[410]));
-	}
-	else
-	{
-		labels.push_back(std::make_shared<CLabel>(408, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[411]));
-	}
-
-	labels.push_back(std::make_shared<CLabel>(232, 302, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW,  CGI->generaltexth->allTexts[407]));
-	labels.push_back(std::make_shared<CLabel>(232, 332, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[408]));
-	labels.push_back(std::make_shared<CLabel>(232, 428, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[409]));
-
-	std::string sideNames[2] = {"N/A", "N/A"};
-
-	for(int i = 0; i < 2; i++)
-	{
-		auto heroInfo = owner.cb->getBattle(br.battleID)->battleGetHeroInfo(i);
-		const int xs[] = {21, 392};
-
-		if(heroInfo.portraitSource.isValid()) //attacking hero
-		{
-			icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("PortraitsLarge"), heroInfo.getIconIndex(), 0, xs[i], 38));
-			sideNames[i] = heroInfo.name;
-		}
-		else
-		{
-			auto stacks = owner.cb->getBattle(br.battleID)->battleGetAllStacks();
-			vstd::erase_if(stacks, [i](const CStack * stack) //erase stack of other side and not coming from garrison
-			{
-				return stack->unitSide() != i || !stack->base;
-			});
-
-			auto best = vstd::maxElementByFun(stacks, [](const CStack * stack)
-			{
-				return stack->unitType()->getAIValue();
-			});
-
-			if(best != stacks.end()) //should be always but to be safe...
-			{
-				icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("TWCRPORT"), (*best)->unitType()->getIconIndex(), 0, xs[i], 38));
-				sideNames[i] = (*best)->unitType()->getNamePluralTranslated();
-			}
-		}
-	}
-
-	//printing attacker and defender's names
-	labels.push_back(std::make_shared<CLabel>(89, 37, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, sideNames[0]));
-	labels.push_back(std::make_shared<CLabel>(381, 53, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, sideNames[1]));
-
-	//printing casualties
-	for(int step = 0; step < 2; ++step)
-	{
-		if(br.casualties[step].size()==0)
-		{
-			labels.push_back(std::make_shared<CLabel>(235, 360 + 97 * step, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[523]));
-		}
-		else
-		{
-			int xPos = 235 - ((int)br.casualties[step].size()*32 + ((int)br.casualties[step].size() - 1)*10)/2; //increment by 42 with each picture
-			int yPos = 344 + step * 97;
-			for(auto & elem : br.casualties[step])
-			{
-				auto creature = CGI->creatures()->getByIndex(elem.first);
-				if (creature->getId() == CreatureID::ARROW_TOWERS )
-					continue; // do not show destroyed towers in battle results
-
-				icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("CPRSMALL"), creature->getIconIndex(), 0, xPos, yPos));
-				std::ostringstream amount;
-				amount<<elem.second;
-				labels.push_back(std::make_shared<CLabel>(xPos + 16, yPos + 42, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, amount.str()));
-				xPos += 42;
-			}
-		}
-	}
-	//printing result description
-	bool weAreAttacker = !(owner.cb->getBattle(br.battleID)->battleGetMySide());
-	if((br.winner == 0 && weAreAttacker) || (br.winner == 1 && !weAreAttacker)) //we've won
-	{
-		int text = 304;
-		currentVideo = BattleResultVideo::WIN;
-		switch(br.result)
-		{
-		case EBattleResult::NORMAL:
-			if(owner.cb->getBattle(br.battleID)->battleGetDefendedTown() && !weAreAttacker)
-				currentVideo = BattleResultVideo::WIN_SIEGE;
-			break;
-		case EBattleResult::ESCAPE:
-			text = 303;
-			break;
-		case EBattleResult::SURRENDER:
-			text = 302;
-			break;
-		default:
-			logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast<int>(br.result));
-			break;
-		}
-		playVideo();
-
-		std::string str = CGI->generaltexth->allTexts[text];
-
-		const CGHeroInstance * ourHero = owner.cb->getBattle(br.battleID)->battleGetMyHero();
-		if (ourHero)
-		{
-			str += CGI->generaltexth->allTexts[305];
-			boost::algorithm::replace_first(str, "%s", ourHero->getNameTranslated());
-			boost::algorithm::replace_first(str, "%d", std::to_string(br.exp[weAreAttacker ? 0 : 1]));
-		}
-
-		description = std::make_shared<CTextBox>(str, Rect(69, 203, 330, 68), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
-	}
-	else // we lose
-	{
-		int text = 311;
-		currentVideo = BattleResultVideo::DEFEAT;
-		switch(br.result)
-		{
-		case EBattleResult::NORMAL:
-			if(owner.cb->getBattle(br.battleID)->battleGetDefendedTown() && !weAreAttacker)
-				currentVideo = BattleResultVideo::DEFEAT_SIEGE;
-			break;
-		case EBattleResult::ESCAPE:
-			currentVideo = BattleResultVideo::RETREAT;
-			text = 310;
-			break;
-		case EBattleResult::SURRENDER:
-			currentVideo = BattleResultVideo::SURRENDER;
-			text = 309;
-			break;
-		default:
-			logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast<int>(br.result));
-			break;
-		}
-		playVideo();
-
-		labels.push_back(std::make_shared<CLabel>(235, 235, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[text]));
-	}
-}
-
-void BattleResultWindow::activate()
-{
-	owner.showingDialog->set(true);
-	CIntObject::activate();
-}
-
-void BattleResultWindow::show(Canvas & to)
-{
-	CIntObject::show(to);
-	CCS->videoh->update(pos.x + 107, pos.y + 70, to.getInternalSurface(), true, false,
-	[&]()
-	{
-		playVideo(true);
-	});
-}
-
-void BattleResultWindow::playVideo(bool startLoop)
-{
-	AudioPath musicName = AudioPath();
-	VideoPath videoName = VideoPath();
-
-	if(!startLoop)
-	{
-		switch(currentVideo)
-		{
-			case BattleResultVideo::WIN:
-				musicName = AudioPath::builtin("Music/Win Battle");
-				videoName = VideoPath::builtin("WIN3.BIK");
-				break;
-			case BattleResultVideo::SURRENDER:
-				musicName = AudioPath::builtin("Music/Surrender Battle");
-				videoName = VideoPath::builtin("SURRENDER.BIK");
-				break;
-			case BattleResultVideo::RETREAT:
-				musicName = AudioPath::builtin("Music/Retreat Battle");
-				videoName = VideoPath::builtin("RTSTART.BIK");
-				break;
-			case BattleResultVideo::DEFEAT:
-				musicName = AudioPath::builtin("Music/LoseCombat");
-				videoName = VideoPath::builtin("LBSTART.BIK");
-				break;
-			case BattleResultVideo::DEFEAT_SIEGE:
-				musicName = AudioPath::builtin("Music/LoseCastle");
-				videoName = VideoPath::builtin("LOSECSTL.BIK");	
-				break;
-			case BattleResultVideo::WIN_SIEGE:
-				musicName = AudioPath::builtin("Music/Defend Castle");
-				videoName = VideoPath::builtin("DEFENDALL.BIK");	
-				break;
-		}
-	}
-	else
-	{
-		switch(currentVideo)
-		{
-			case BattleResultVideo::RETREAT:
-				currentVideo = BattleResultVideo::RETREAT_LOOP;
-				videoName = VideoPath::builtin("RTLOOP.BIK");
-				break;
-			case BattleResultVideo::DEFEAT:
-				currentVideo = BattleResultVideo::DEFEAT_LOOP;
-				videoName = VideoPath::builtin("LBLOOP.BIK");
-				break;
-			case BattleResultVideo::DEFEAT_SIEGE:
-				currentVideo = BattleResultVideo::DEFEAT_SIEGE_LOOP;
-				videoName = VideoPath::builtin("LOSECSLP.BIK");	
-				break;
-			case BattleResultVideo::WIN_SIEGE:
-				currentVideo = BattleResultVideo::WIN_SIEGE_LOOP;
-				videoName = VideoPath::builtin("DEFENDLOOP.BIK");	
-				break;
-		}	
-	}
-
-	if(musicName != AudioPath())
-		CCS->musich->playMusic(musicName, false, true);
-	
-	if(videoName != VideoPath())
-		CCS->videoh->open(videoName);
-}
-
-void BattleResultWindow::buttonPressed(int button)
-{
-	if (resultCallback)
-		resultCallback(button);
-
-	CPlayerInterface &intTmp = owner; //copy reference because "this" will be destructed soon
-
-	close();
-
-	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.
-	intTmp.showingDialog->setn(false);
-	CCS->videoh->close();
-}
-
-void BattleResultWindow::bExitf()
-{
-	buttonPressed(0);
-}
-
-void BattleResultWindow::bRepeatf()
-{
-	buttonPressed(1);
-}
-
-StackQueue::StackQueue(bool Embedded, BattleInterface & owner)
-	: embedded(Embedded),
-	owner(owner)
-{
-	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
-	if(embedded)
-	{
-		pos.w = QUEUE_SIZE * 41;
-		pos.h = 49;
-		pos.x += parent->pos.w/2 - pos.w/2;
-		pos.y += 10;
-
-		icons = GH.renderHandler().loadAnimation(AnimationPath::builtin("CPRSMALL"));
-		stateIcons = GH.renderHandler().loadAnimation(AnimationPath::builtin("VCMI/BATTLEQUEUE/STATESSMALL"));
-	}
-	else
-	{
-		pos.w = 800;
-		pos.h = 85;
-		pos.x += 0;
-		pos.y -= pos.h;
-
-		background = std::make_shared<CFilledTexture>(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, pos.w, pos.h));
-
-		icons = GH.renderHandler().loadAnimation(AnimationPath::builtin("TWCRPORT"));
-		stateIcons = GH.renderHandler().loadAnimation(AnimationPath::builtin("VCMI/BATTLEQUEUE/STATESSMALL"));
-		//TODO: where use big icons?
-		//stateIcons = GH.renderHandler().loadAnimation("VCMI/BATTLEQUEUE/STATESBIG");
-	}
-	stateIcons->preload();
-
-	stackBoxes.resize(QUEUE_SIZE);
-	for (int i = 0; i < stackBoxes.size(); i++)
-	{
-		stackBoxes[i] = std::make_shared<StackBox>(this);
-		stackBoxes[i]->moveBy(Point(1 + (embedded ? 41 : 80) * i, 0));
-	}
-}
-
-void StackQueue::show(Canvas & to)
-{
-	if (embedded)
-		showAll(to);
-	CIntObject::show(to);
-}
-
-void StackQueue::update()
-{
-	std::vector<battle::Units> queueData;
-
-	owner.getBattle()->battleGetTurnOrder(queueData, stackBoxes.size(), 0);
-
-	size_t boxIndex = 0;
-
-	for(size_t turn = 0; turn < queueData.size() && boxIndex < stackBoxes.size(); turn++)
-	{
-		for(size_t unitIndex = 0; unitIndex < queueData[turn].size() && boxIndex < stackBoxes.size(); boxIndex++, unitIndex++)
-			stackBoxes[boxIndex]->setUnit(queueData[turn][unitIndex], turn);
-	}
-
-	for(; boxIndex < stackBoxes.size(); boxIndex++)
-		stackBoxes[boxIndex]->setUnit(nullptr);
-}
-
-int32_t StackQueue::getSiegeShooterIconID()
-{
-	return owner.siegeController->getSiegedTown()->town->faction->getIndex();
-}
-
-std::optional<uint32_t> StackQueue::getHoveredUnitIdIfAny() const
-{
-	for(const auto & stackBox : stackBoxes)
-	{
-		if(stackBox->isHovered())
-		{
-			return stackBox->getBoundUnitID();
-		}
-	}
-
-	return std::nullopt;
-}
-
-StackQueue::StackBox::StackBox(StackQueue * owner):
-	CIntObject(SHOW_POPUP | HOVER), owner(owner)
-{
-	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
-	background = std::make_shared<CPicture>(ImagePath::builtin(owner->embedded ? "StackQueueSmall" : "StackQueueLarge"));
-
-	pos.w = background->pos.w;
-	pos.h = background->pos.h;
-
-	if(owner->embedded)
-	{
-		icon = std::make_shared<CAnimImage>(owner->icons, 0, 0, 5, 2);
-		amount = std::make_shared<CLabel>(pos.w/2, pos.h - 7, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
-	}
-	else
-	{
-		icon = std::make_shared<CAnimImage>(owner->icons, 0, 0, 9, 1);
-		amount = std::make_shared<CLabel>(pos.w/2, pos.h - 8, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE);
-
-		int icon_x = pos.w - 17;
-		int icon_y = pos.h - 18;
-
-		stateIcon = std::make_shared<CAnimImage>(owner->stateIcons, 0, 0, icon_x, icon_y);
-		stateIcon->visible = false;
-	}
-}
-
-void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn)
-{
-	if(unit)
-	{
-		boundUnitID = unit->unitId();
-		background->colorize(unit->unitOwner());
-		icon->visible = true;
-
-		// temporary code for mod compatibility:
-		// first, set icon that should definitely exist (arrow tower icon in base vcmi mod)
-		// second, try to switch to icon that should be provided by mod
-		// if mod is not up to date and does have arrow tower icon yet - second setFrame call will fail and retain previously set image
-		// for 1.2 release & later next line should be moved into 'else' block
-		icon->setFrame(unit->creatureIconIndex(), 0);
-		if (unit->unitType()->getId() == CreatureID::ARROW_TOWERS)
-			icon->setFrame(owner->getSiegeShooterIconID(), 1);
-
-		amount->setText(TextOperations::formatMetric(unit->getCount(), 4));
-
-		if(stateIcon)
-		{
-			if(unit->defended((int)turn) || (turn > 0 && unit->defended((int)turn - 1)))
-			{
-				stateIcon->setFrame(0, 0);
-				stateIcon->visible = true;
-			}
-			else if(unit->waited((int)turn))
-			{
-				stateIcon->setFrame(1, 0);
-				stateIcon->visible = true;
-			}
-			else
-			{
-				stateIcon->visible = false;
-			}
-		}
-	}
-	else
-	{
-		boundUnitID = std::nullopt;
-		background->colorize(PlayerColor::NEUTRAL);
-		icon->visible = false;
-		icon->setFrame(0);
-		amount->setText("");
-
-		if(stateIcon)
-			stateIcon->visible = false;
-	}
-}
-
-std::optional<uint32_t> StackQueue::StackBox::getBoundUnitID() const
-{
-	return boundUnitID;
-}
-
-bool StackQueue::StackBox::isBoundUnitHighlighted() const
-{
-	auto unitIdsToHighlight = owner->owner.stacksController->getHoveredStacksUnitIds();
-	return vstd::contains(unitIdsToHighlight, getBoundUnitID());
-}
-
-void StackQueue::StackBox::showAll(Canvas & to)
-{
-	CIntObject::showAll(to);
-
-	if(isBoundUnitHighlighted())
-		to.drawBorder(background->pos, Colors::CYAN, 2);
-}
-
-void StackQueue::StackBox::show(Canvas & to)
-{
-	CIntObject::show(to);
-
-	if(isBoundUnitHighlighted())
-		to.drawBorder(background->pos, Colors::CYAN, 2);
-}
+/*
+ * BattleInterfaceClasses.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 "BattleInterfaceClasses.h"
+
+#include "BattleInterface.h"
+#include "BattleActionsController.h"
+#include "BattleRenderer.h"
+#include "BattleSiegeController.h"
+#include "BattleFieldController.h"
+#include "BattleStacksController.h"
+#include "BattleWindow.h"
+
+#include "../CGameInfo.h"
+#include "../CMusicHandler.h"
+#include "../CPlayerInterface.h"
+#include "../CVideoHandler.h"
+#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 "../render/IFont.h"
+#include "../render/Graphics.h"
+#include "../widgets/Buttons.h"
+#include "../widgets/Images.h"
+#include "../widgets/TextControls.h"
+#include "../windows/CMessage.h"
+#include "../windows/CSpellWindow.h"
+#include "../render/CAnimation.h"
+#include "../render/IRenderHandler.h"
+#include "../adventureMap/CInGameConsole.h"
+
+#include "../../CCallback.h"
+#include "../../lib/CStack.h"
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/CCreatureHandler.h"
+#include "../../lib/gameState/InfoAboutArmy.h"
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/CTownHandler.h"
+#include "../../lib/CHeroHandler.h"
+#include "../../lib/NetPacks.h"
+#include "../../lib/StartInfo.h"
+#include "../../lib/CondSh.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
+#include "../../lib/TextOperations.h"
+
+void BattleConsole::showAll(Canvas & to)
+{
+	CIntObject::showAll(to);
+
+	Point line1 (pos.x + pos.w/2, pos.y +  8);
+	Point line2 (pos.x + pos.w/2, pos.y + 24);
+
+	auto visibleText = getVisibleText();
+
+	if(visibleText.size() > 0)
+		to.drawText(line1, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, visibleText[0]);
+
+	if(visibleText.size() > 1)
+		to.drawText(line2, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, visibleText[1]);
+}
+
+std::vector<std::string> BattleConsole::getVisibleText()
+{
+	// high priority texts that hide battle log entries
+	for(const auto & text : {consoleText, hoverText})
+	{
+		if (text.empty())
+			continue;
+
+		auto result = CMessage::breakText(text, pos.w, FONT_SMALL);
+
+		if(result.size() > 2)
+			result.resize(2);
+		return result;
+	}
+
+	// log is small enough to fit entirely - display it as such
+	if (logEntries.size() < 3)
+		return logEntries;
+
+	return { logEntries[scrollPosition - 1], logEntries[scrollPosition] };
+}
+
+std::vector<std::string> BattleConsole::splitText(const std::string &text)
+{
+	std::vector<std::string> lines;
+	std::vector<std::string> output;
+
+	boost::split(lines, text, boost::is_any_of("\n"));
+
+	for(const auto & line : lines)
+	{
+		if (graphics->fonts[FONT_SMALL]->getStringWidth(text) < pos.w)
+		{
+			output.push_back(line);
+		}
+		else
+		{
+			std::vector<std::string> substrings = CMessage::breakText(line, pos.w, FONT_SMALL);
+			output.insert(output.end(), substrings.begin(), substrings.end());
+		}
+	}
+	return output;
+}
+
+bool BattleConsole::addText(const std::string & text)
+{
+	logGlobal->trace("CBattleConsole message: %s", text);
+
+	auto newLines = splitText(text);
+
+	logEntries.insert(logEntries.end(), newLines.begin(), newLines.end());
+	scrollPosition = (int)logEntries.size()-1;
+	redraw();
+	return true;
+}
+void BattleConsole::scrollUp(ui32 by)
+{
+	if(scrollPosition > static_cast<int>(by))
+		scrollPosition -= by;
+	redraw();
+}
+
+void BattleConsole::scrollDown(ui32 by)
+{
+	if(scrollPosition + by < logEntries.size())
+		scrollPosition += by;
+	redraw();
+}
+
+BattleConsole::BattleConsole(std::shared_ptr<CPicture> backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size)
+	: scrollPosition(-1)
+	, enteringText(false)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	pos += objectPos;
+	pos.w = size.x;
+	pos.h = size.y;
+
+	background = std::make_shared<CPicture>(backgroundSource->getSurface(), Rect(imagePos, size), 0, 0 );
+}
+
+void BattleConsole::deactivate()
+{
+	if (enteringText)
+		LOCPLINT->cingconsole->endEnteringText(false);
+
+	CIntObject::deactivate();
+}
+
+void BattleConsole::setEnteringMode(bool on)
+{
+	consoleText.clear();
+
+	if (on)
+	{
+		assert(enteringText == false);
+		GH.startTextInput(pos);
+	}
+	else
+	{
+		assert(enteringText == true);
+		GH.stopTextInput();
+	}
+	enteringText = on;
+	redraw();
+}
+
+void BattleConsole::setEnteredText(const std::string & text)
+{
+	assert(enteringText == true);
+	consoleText = text;
+	redraw();
+}
+
+void BattleConsole::write(const std::string & Text)
+{
+	hoverText = Text;
+	redraw();
+}
+
+void BattleConsole::clearIfMatching(const std::string & Text)
+{
+	if (hoverText == Text)
+		clear();
+}
+
+void BattleConsole::clear()
+{
+	write({});
+}
+
+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);
+
+	auto flagFrame = flagAnimation->getImage(flagCurrentFrame, 0, true);
+	auto heroFrame = animation->getImage(currentFrame, groupIndex, true);
+
+	Point heroPosition = pos.center() - parent->pos.topLeft() - heroFrame->dimensions() / 2;
+	Point flagPosition = pos.center() - parent->pos.topLeft() - flagFrame->dimensions() / 2;
+
+	if(defender)
+		flagPosition += Point(-4, -41);
+	else
+		flagPosition += Point(4, -41);
+
+	canvas.draw(flagFrame, flagPosition);
+	canvas.draw(heroFrame, heroPosition);
+}
+
+void BattleHero::pause()
+{
+	currentSpeed = 0.f;
+}
+
+void BattleHero::play()
+{
+	//H3 speed: 10 fps ( 100 ms per frame)
+	currentSpeed = 10.f;
+}
+
+float BattleHero::getFrame() const
+{
+	return currentFrame;
+}
+
+void BattleHero::collectRenderableObjects(BattleRenderer & renderer)
+{
+	auto hex = defender ? BattleHex(GameConstants::BFIELD_WIDTH-1) : BattleHex(0);
+
+	renderer.insert(EBattleFieldLayer::HEROES, hex, [this](BattleRenderer::RendererRef canvas)
+	{
+		render(canvas);
+	});
+}
+
+void BattleHero::onPhaseFinished(const std::function<void()> & callback)
+{
+	phaseFinishedCallback = callback;
+}
+
+void BattleHero::setPhase(EHeroAnimType newPhase)
+{
+	nextPhase = newPhase;
+	switchToNextPhase(); //immediately switch to next phase and then restore idling phase
+	nextPhase = EHeroAnimType::HOLDING;
+}
+
+void BattleHero::heroLeftClicked()
+{
+	if(owner.actionsController->spellcastingModeActive()) //we are casting a spell
+		return;
+
+	if(!hero || !owner.makingTurn())
+		return;
+
+	if(owner.getBattle()->battleCanCastSpell(hero, spells::Mode::HERO) == ESpellCastProblem::OK) //check conditions
+	{
+		CCS->curh->set(Cursor::Map::POINTER);
+		GH.windows().createAndPushWindow<CSpellWindow>(hero, owner.getCurrentPlayerInterface());
+	}
+}
+
+void BattleHero::heroRightClicked()
+{
+	if(settings["battle"]["stickyHeroInfoWindows"].Bool())
+		return;
+
+	Point windowPosition;
+	if(GH.screenDimensions().x < 1000)
+	{
+		windowPosition.x = (!defender) ? owner.fieldController->pos.left() + 1 : owner.fieldController->pos.right() - 79;
+		windowPosition.y = owner.fieldController->pos.y + 135;
+	}
+	else
+	{
+		windowPosition.x = (!defender) ? owner.fieldController->pos.left() - 93 : owner.fieldController->pos.right() + 15;
+		windowPosition.y = owner.fieldController->pos.y;
+	}
+
+	InfoAboutHero targetHero;
+	if(owner.makingTurn() || settings["session"]["spectate"].Bool())
+	{
+		auto h = defender ? owner.defendingHeroInstance : owner.attackingHeroInstance;
+		targetHero.initFromHero(h, InfoAboutHero::EInfoLevel::INBATTLE);
+		GH.windows().createAndPushWindow<HeroInfoWindow>(targetHero, &windowPosition);
+	}
+}
+
+void BattleHero::switchToNextPhase()
+{
+	phase = nextPhase;
+	currentFrame = 0.f;
+
+	auto copy = phaseFinishedCallback;
+	phaseFinishedCallback.clear();
+	copy();
+}
+
+BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender):
+	defender(defender),
+	hero(hero),
+	owner(owner),
+	phase(EHeroAnimType::HOLDING),
+	nextPhase(EHeroAnimType::HOLDING),
+	currentSpeed(0.f),
+	currentFrame(0.f),
+	flagCurrentFrame(0.f)
+{
+	AnimationPath animationPath;
+
+	if(!hero->type->battleImage.empty())
+		animationPath = hero->type->battleImage;
+	else
+	if(hero->gender == EHeroGender::FEMALE)
+		animationPath = hero->type->heroClass->imageBattleFemale;
+	else
+		animationPath = hero->type->heroClass->imageBattleMale;
+
+	animation = GH.renderHandler().loadAnimation(animationPath);
+	animation->preload();
+
+	pos.w = 64;
+	pos.h = 136;
+	pos.x = owner.fieldController->pos.x + (defender ? (owner.fieldController->pos.w - pos.w) : 0);
+	pos.y = owner.fieldController->pos.y;
+
+	if(defender)
+		animation->verticalFlip();
+
+	if(defender)
+		flagAnimation = GH.renderHandler().loadAnimation(AnimationPath::builtin("CMFLAGR"));
+	else
+		flagAnimation = GH.renderHandler().loadAnimation(AnimationPath::builtin("CMFLAGL"));
+
+	flagAnimation->preload();
+	flagAnimation->playerColored(hero->tempOwner);
+
+	switchToNextPhase();
+	play();
+
+	addUsedEvents(TIME);
+}
+
+HeroInfoBasicPanel::HeroInfoBasicPanel(const InfoAboutHero & hero, Point * position, bool initializeBackground)
+	: CIntObject(0)
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
+	if (position != nullptr)
+		moveTo(*position);
+
+	if(initializeBackground)
+	{
+		background = std::make_shared<CPicture>(ImagePath::builtin("CHRPOP"));
+		background->getSurface()->setBlitMode(EImageBlitMode::OPAQUE);
+		background->colorize(hero.owner);
+	}
+
+	initializeData(hero);
+}
+
+void HeroInfoBasicPanel::initializeData(const InfoAboutHero & hero)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	auto attack = hero.details->primskills[0];
+	auto defense = hero.details->primskills[1];
+	auto power = hero.details->primskills[2];
+	auto knowledge = hero.details->primskills[3];
+	auto morale = hero.details->morale;
+	auto luck = hero.details->luck;
+	auto currentSpellPoints = hero.details->mana;
+	auto maxSpellPoints = hero.details->manaLimit;
+
+	icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("PortraitsLarge"), hero.getIconIndex(), 0, 10, 6));
+
+	//primary stats
+	labels.push_back(std::make_shared<CLabel>(9, 75, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[380] + ":"));
+	labels.push_back(std::make_shared<CLabel>(9, 87, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[381] + ":"));
+	labels.push_back(std::make_shared<CLabel>(9, 99, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[382] + ":"));
+	labels.push_back(std::make_shared<CLabel>(9, 111, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[383] + ":"));
+
+	labels.push_back(std::make_shared<CLabel>(69, 87, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(attack)));
+	labels.push_back(std::make_shared<CLabel>(69, 99, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(defense)));
+	labels.push_back(std::make_shared<CLabel>(69, 111, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(power)));
+	labels.push_back(std::make_shared<CLabel>(69, 123, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(knowledge)));
+
+	//morale+luck
+	labels.push_back(std::make_shared<CLabel>(9, 131, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[384] + ":"));
+	labels.push_back(std::make_shared<CLabel>(9, 143, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[385] + ":"));
+
+	icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("IMRL22"), morale + 3, 0, 47, 131));
+	icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("ILCK22"), luck + 3, 0, 47, 143));
+
+	//spell points
+	labels.push_back(std::make_shared<CLabel>(39, 174, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[387]));
+	labels.push_back(std::make_shared<CLabel>(39, 186, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, std::to_string(currentSpellPoints) + "/" + std::to_string(maxSpellPoints)));
+}
+
+void HeroInfoBasicPanel::update(const InfoAboutHero & updatedInfo)
+{
+	icons.clear();
+	labels.clear();
+
+	initializeData(updatedInfo);
+}
+
+void HeroInfoBasicPanel::show(Canvas & to)
+{
+	showAll(to);
+	CIntObject::show(to);
+}
+
+HeroInfoWindow::HeroInfoWindow(const InfoAboutHero & hero, Point * position)
+	: CWindowObject(RCLICK_POPUP | SHADOW_DISABLED, ImagePath::builtin("CHRPOP"))
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
+	if (position != nullptr)
+		moveTo(*position);
+
+	background->colorize(hero.owner); //maybe add this functionality to base class?
+
+	content = std::make_shared<HeroInfoBasicPanel>(hero, nullptr, false);
+}
+
+BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay)
+	: owner(_owner), currentVideo(BattleResultVideo::NONE)
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
+
+	background = std::make_shared<CPicture>(ImagePath::builtin("CPRESULT"));
+	background->colorize(owner.playerID);
+	pos = center(background->pos);
+
+	exit = std::make_shared<CButton>(Point(384, 505), AnimationPath::builtin("iok6432.def"), std::make_pair("", ""), [&](){ bExitf();}, EShortcut::GLOBAL_ACCEPT);
+	exit->setBorderColor(Colors::METALLIC_GOLD);
+	
+	if(allowReplay)
+	{
+		repeat = std::make_shared<CButton>(Point(24, 505), AnimationPath::builtin("icn6432.def"), std::make_pair("", ""), [&](){ bRepeatf();}, EShortcut::GLOBAL_CANCEL);
+		repeat->setBorderColor(Colors::METALLIC_GOLD);
+		labels.push_back(std::make_shared<CLabel>(232, 520, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("vcmi.battleResultsWindow.applyResultsLabel")));
+	}
+
+	if(br.winner == 0) //attacker won
+	{
+		labels.push_back(std::make_shared<CLabel>(59, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[410]));
+	}
+	else
+	{
+		labels.push_back(std::make_shared<CLabel>(59, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[411]));
+	}
+
+	if(br.winner == 1)
+	{
+		labels.push_back(std::make_shared<CLabel>(412, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[410]));
+	}
+	else
+	{
+		labels.push_back(std::make_shared<CLabel>(408, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[411]));
+	}
+
+	labels.push_back(std::make_shared<CLabel>(232, 302, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW,  CGI->generaltexth->allTexts[407]));
+	labels.push_back(std::make_shared<CLabel>(232, 332, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[408]));
+	labels.push_back(std::make_shared<CLabel>(232, 428, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[409]));
+
+	std::string sideNames[2] = {"N/A", "N/A"};
+
+	for(int i = 0; i < 2; i++)
+	{
+		auto heroInfo = owner.cb->getBattle(br.battleID)->battleGetHeroInfo(i);
+		const int xs[] = {21, 392};
+
+		if(heroInfo.portraitSource.isValid()) //attacking hero
+		{
+			icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("PortraitsLarge"), heroInfo.getIconIndex(), 0, xs[i], 38));
+			sideNames[i] = heroInfo.name;
+		}
+		else
+		{
+			auto stacks = owner.cb->getBattle(br.battleID)->battleGetAllStacks();
+			vstd::erase_if(stacks, [i](const CStack * stack) //erase stack of other side and not coming from garrison
+			{
+				return stack->unitSide() != i || !stack->base;
+			});
+
+			auto best = vstd::maxElementByFun(stacks, [](const CStack * stack)
+			{
+				return stack->unitType()->getAIValue();
+			});
+
+			if(best != stacks.end()) //should be always but to be safe...
+			{
+				icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("TWCRPORT"), (*best)->unitType()->getIconIndex(), 0, xs[i], 38));
+				sideNames[i] = (*best)->unitType()->getNamePluralTranslated();
+			}
+		}
+	}
+
+	//printing attacker and defender's names
+	labels.push_back(std::make_shared<CLabel>(89, 37, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, sideNames[0]));
+	labels.push_back(std::make_shared<CLabel>(381, 53, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, sideNames[1]));
+
+	//printing casualties
+	for(int step = 0; step < 2; ++step)
+	{
+		if(br.casualties[step].size()==0)
+		{
+			labels.push_back(std::make_shared<CLabel>(235, 360 + 97 * step, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[523]));
+		}
+		else
+		{
+			int xPos = 235 - ((int)br.casualties[step].size()*32 + ((int)br.casualties[step].size() - 1)*10)/2; //increment by 42 with each picture
+			int yPos = 344 + step * 97;
+			for(auto & elem : br.casualties[step])
+			{
+				auto creature = CGI->creatures()->getByIndex(elem.first);
+				if (creature->getId() == CreatureID::ARROW_TOWERS )
+					continue; // do not show destroyed towers in battle results
+
+				icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("CPRSMALL"), creature->getIconIndex(), 0, xPos, yPos));
+				std::ostringstream amount;
+				amount<<elem.second;
+				labels.push_back(std::make_shared<CLabel>(xPos + 16, yPos + 42, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, amount.str()));
+				xPos += 42;
+			}
+		}
+	}
+	//printing result description
+	bool weAreAttacker = !(owner.cb->getBattle(br.battleID)->battleGetMySide());
+	if((br.winner == 0 && weAreAttacker) || (br.winner == 1 && !weAreAttacker)) //we've won
+	{
+		int text = 304;
+		currentVideo = BattleResultVideo::WIN;
+		switch(br.result)
+		{
+		case EBattleResult::NORMAL:
+			if(owner.cb->getBattle(br.battleID)->battleGetDefendedTown() && !weAreAttacker)
+				currentVideo = BattleResultVideo::WIN_SIEGE;
+			break;
+		case EBattleResult::ESCAPE:
+			text = 303;
+			break;
+		case EBattleResult::SURRENDER:
+			text = 302;
+			break;
+		default:
+			logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast<int>(br.result));
+			break;
+		}
+		playVideo();
+
+		std::string str = CGI->generaltexth->allTexts[text];
+
+		const CGHeroInstance * ourHero = owner.cb->getBattle(br.battleID)->battleGetMyHero();
+		if (ourHero)
+		{
+			str += CGI->generaltexth->allTexts[305];
+			boost::algorithm::replace_first(str, "%s", ourHero->getNameTranslated());
+			boost::algorithm::replace_first(str, "%d", std::to_string(br.exp[weAreAttacker ? 0 : 1]));
+		}
+
+		description = std::make_shared<CTextBox>(str, Rect(69, 203, 330, 68), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
+	}
+	else // we lose
+	{
+		int text = 311;
+		currentVideo = BattleResultVideo::DEFEAT;
+		switch(br.result)
+		{
+		case EBattleResult::NORMAL:
+			if(owner.cb->getBattle(br.battleID)->battleGetDefendedTown() && !weAreAttacker)
+				currentVideo = BattleResultVideo::DEFEAT_SIEGE;
+			break;
+		case EBattleResult::ESCAPE:
+			currentVideo = BattleResultVideo::RETREAT;
+			text = 310;
+			break;
+		case EBattleResult::SURRENDER:
+			currentVideo = BattleResultVideo::SURRENDER;
+			text = 309;
+			break;
+		default:
+			logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast<int>(br.result));
+			break;
+		}
+		playVideo();
+
+		labels.push_back(std::make_shared<CLabel>(235, 235, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[text]));
+	}
+}
+
+void BattleResultWindow::activate()
+{
+	owner.showingDialog->set(true);
+	CIntObject::activate();
+}
+
+void BattleResultWindow::show(Canvas & to)
+{
+	CIntObject::show(to);
+	CCS->videoh->update(pos.x + 107, pos.y + 70, to.getInternalSurface(), true, false,
+	[&]()
+	{
+		playVideo(true);
+	});
+}
+
+void BattleResultWindow::playVideo(bool startLoop)
+{
+	AudioPath musicName = AudioPath();
+	VideoPath videoName = VideoPath();
+
+	if(!startLoop)
+	{
+		switch(currentVideo)
+		{
+			case BattleResultVideo::WIN:
+				musicName = AudioPath::builtin("Music/Win Battle");
+				videoName = VideoPath::builtin("WIN3.BIK");
+				break;
+			case BattleResultVideo::SURRENDER:
+				musicName = AudioPath::builtin("Music/Surrender Battle");
+				videoName = VideoPath::builtin("SURRENDER.BIK");
+				break;
+			case BattleResultVideo::RETREAT:
+				musicName = AudioPath::builtin("Music/Retreat Battle");
+				videoName = VideoPath::builtin("RTSTART.BIK");
+				break;
+			case BattleResultVideo::DEFEAT:
+				musicName = AudioPath::builtin("Music/LoseCombat");
+				videoName = VideoPath::builtin("LBSTART.BIK");
+				break;
+			case BattleResultVideo::DEFEAT_SIEGE:
+				musicName = AudioPath::builtin("Music/LoseCastle");
+				videoName = VideoPath::builtin("LOSECSTL.BIK");	
+				break;
+			case BattleResultVideo::WIN_SIEGE:
+				musicName = AudioPath::builtin("Music/Defend Castle");
+				videoName = VideoPath::builtin("DEFENDALL.BIK");	
+				break;
+		}
+	}
+	else
+	{
+		switch(currentVideo)
+		{
+			case BattleResultVideo::RETREAT:
+				currentVideo = BattleResultVideo::RETREAT_LOOP;
+				videoName = VideoPath::builtin("RTLOOP.BIK");
+				break;
+			case BattleResultVideo::DEFEAT:
+				currentVideo = BattleResultVideo::DEFEAT_LOOP;
+				videoName = VideoPath::builtin("LBLOOP.BIK");
+				break;
+			case BattleResultVideo::DEFEAT_SIEGE:
+				currentVideo = BattleResultVideo::DEFEAT_SIEGE_LOOP;
+				videoName = VideoPath::builtin("LOSECSLP.BIK");	
+				break;
+			case BattleResultVideo::WIN_SIEGE:
+				currentVideo = BattleResultVideo::WIN_SIEGE_LOOP;
+				videoName = VideoPath::builtin("DEFENDLOOP.BIK");	
+				break;
+		}	
+	}
+
+	if(musicName != AudioPath())
+		CCS->musich->playMusic(musicName, false, true);
+	
+	if(videoName != VideoPath())
+		CCS->videoh->open(videoName);
+}
+
+void BattleResultWindow::buttonPressed(int button)
+{
+	if (resultCallback)
+		resultCallback(button);
+
+	CPlayerInterface &intTmp = owner; //copy reference because "this" will be destructed soon
+
+	close();
+
+	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.
+	intTmp.showingDialog->setn(false);
+	CCS->videoh->close();
+}
+
+void BattleResultWindow::bExitf()
+{
+	buttonPressed(0);
+}
+
+void BattleResultWindow::bRepeatf()
+{
+	buttonPressed(1);
+}
+
+StackQueue::StackQueue(bool Embedded, BattleInterface & owner)
+	: embedded(Embedded),
+	owner(owner)
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
+	if(embedded)
+	{
+		pos.w = QUEUE_SIZE * 41;
+		pos.h = 49;
+		pos.x += parent->pos.w/2 - pos.w/2;
+		pos.y += 10;
+
+		icons = GH.renderHandler().loadAnimation(AnimationPath::builtin("CPRSMALL"));
+		stateIcons = GH.renderHandler().loadAnimation(AnimationPath::builtin("VCMI/BATTLEQUEUE/STATESSMALL"));
+	}
+	else
+	{
+		pos.w = 800;
+		pos.h = 85;
+		pos.x += 0;
+		pos.y -= pos.h;
+
+		background = std::make_shared<CFilledTexture>(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, pos.w, pos.h));
+
+		icons = GH.renderHandler().loadAnimation(AnimationPath::builtin("TWCRPORT"));
+		stateIcons = GH.renderHandler().loadAnimation(AnimationPath::builtin("VCMI/BATTLEQUEUE/STATESSMALL"));
+		//TODO: where use big icons?
+		//stateIcons = GH.renderHandler().loadAnimation("VCMI/BATTLEQUEUE/STATESBIG");
+	}
+	stateIcons->preload();
+
+	stackBoxes.resize(QUEUE_SIZE);
+	for (int i = 0; i < stackBoxes.size(); i++)
+	{
+		stackBoxes[i] = std::make_shared<StackBox>(this);
+		stackBoxes[i]->moveBy(Point(1 + (embedded ? 41 : 80) * i, 0));
+	}
+}
+
+void StackQueue::show(Canvas & to)
+{
+	if (embedded)
+		showAll(to);
+	CIntObject::show(to);
+}
+
+void StackQueue::update()
+{
+	std::vector<battle::Units> queueData;
+
+	owner.getBattle()->battleGetTurnOrder(queueData, stackBoxes.size(), 0);
+
+	size_t boxIndex = 0;
+
+	for(size_t turn = 0; turn < queueData.size() && boxIndex < stackBoxes.size(); turn++)
+	{
+		for(size_t unitIndex = 0; unitIndex < queueData[turn].size() && boxIndex < stackBoxes.size(); boxIndex++, unitIndex++)
+			stackBoxes[boxIndex]->setUnit(queueData[turn][unitIndex], turn);
+	}
+
+	for(; boxIndex < stackBoxes.size(); boxIndex++)
+		stackBoxes[boxIndex]->setUnit(nullptr);
+}
+
+int32_t StackQueue::getSiegeShooterIconID()
+{
+	return owner.siegeController->getSiegedTown()->town->faction->getIndex();
+}
+
+std::optional<uint32_t> StackQueue::getHoveredUnitIdIfAny() const
+{
+	for(const auto & stackBox : stackBoxes)
+	{
+		if(stackBox->isHovered())
+		{
+			return stackBox->getBoundUnitID();
+		}
+	}
+
+	return std::nullopt;
+}
+
+StackQueue::StackBox::StackBox(StackQueue * owner):
+	CIntObject(SHOW_POPUP | HOVER), owner(owner)
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
+	background = std::make_shared<CPicture>(ImagePath::builtin(owner->embedded ? "StackQueueSmall" : "StackQueueLarge"));
+
+	pos.w = background->pos.w;
+	pos.h = background->pos.h;
+
+	if(owner->embedded)
+	{
+		icon = std::make_shared<CAnimImage>(owner->icons, 0, 0, 5, 2);
+		amount = std::make_shared<CLabel>(pos.w/2, pos.h - 7, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
+	}
+	else
+	{
+		icon = std::make_shared<CAnimImage>(owner->icons, 0, 0, 9, 1);
+		amount = std::make_shared<CLabel>(pos.w/2, pos.h - 8, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE);
+
+		int icon_x = pos.w - 17;
+		int icon_y = pos.h - 18;
+
+		stateIcon = std::make_shared<CAnimImage>(owner->stateIcons, 0, 0, icon_x, icon_y);
+		stateIcon->visible = false;
+	}
+}
+
+void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn)
+{
+	if(unit)
+	{
+		boundUnitID = unit->unitId();
+		background->colorize(unit->unitOwner());
+		icon->visible = true;
+
+		// temporary code for mod compatibility:
+		// first, set icon that should definitely exist (arrow tower icon in base vcmi mod)
+		// second, try to switch to icon that should be provided by mod
+		// if mod is not up to date and does have arrow tower icon yet - second setFrame call will fail and retain previously set image
+		// for 1.2 release & later next line should be moved into 'else' block
+		icon->setFrame(unit->creatureIconIndex(), 0);
+		if (unit->unitType()->getId() == CreatureID::ARROW_TOWERS)
+			icon->setFrame(owner->getSiegeShooterIconID(), 1);
+
+		amount->setText(TextOperations::formatMetric(unit->getCount(), 4));
+
+		if(stateIcon)
+		{
+			if(unit->defended((int)turn) || (turn > 0 && unit->defended((int)turn - 1)))
+			{
+				stateIcon->setFrame(0, 0);
+				stateIcon->visible = true;
+			}
+			else if(unit->waited((int)turn))
+			{
+				stateIcon->setFrame(1, 0);
+				stateIcon->visible = true;
+			}
+			else
+			{
+				stateIcon->visible = false;
+			}
+		}
+	}
+	else
+	{
+		boundUnitID = std::nullopt;
+		background->colorize(PlayerColor::NEUTRAL);
+		icon->visible = false;
+		icon->setFrame(0);
+		amount->setText("");
+
+		if(stateIcon)
+			stateIcon->visible = false;
+	}
+}
+
+std::optional<uint32_t> StackQueue::StackBox::getBoundUnitID() const
+{
+	return boundUnitID;
+}
+
+bool StackQueue::StackBox::isBoundUnitHighlighted() const
+{
+	auto unitIdsToHighlight = owner->owner.stacksController->getHoveredStacksUnitIds();
+	return vstd::contains(unitIdsToHighlight, getBoundUnitID());
+}
+
+void StackQueue::StackBox::showAll(Canvas & to)
+{
+	CIntObject::showAll(to);
+
+	if(isBoundUnitHighlighted())
+		to.drawBorder(background->pos, Colors::CYAN, 2);
+}
+
+void StackQueue::StackBox::show(Canvas & to)
+{
+	CIntObject::show(to);
+
+	if(isBoundUnitHighlighted())
+		to.drawBorder(background->pos, Colors::CYAN, 2);
+}

+ 238 - 238
client/battle/BattleInterfaceClasses.h

@@ -1,238 +1,238 @@
-/*
- * BattleInterfaceClasses.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 "BattleConstants.h"
-#include "../gui/CIntObject.h"
-#include "../../lib/FunctionList.h"
-#include "../../lib/battle/BattleHex.h"
-#include "../windows/CWindowObject.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-class CGHeroInstance;
-struct BattleResult;
-struct InfoAboutHero;
-class CStack;
-
-namespace battle
-{
-class Unit;
-}
-
-VCMI_LIB_NAMESPACE_END
-
-class CAnimation;
-class Canvas;
-class BattleInterface;
-class CPicture;
-class CFilledTexture;
-class CButton;
-class CToggleButton;
-class CLabel;
-class CTextBox;
-class CAnimImage;
-class CPlayerInterface;
-class BattleRenderer;
-
-/// Class which shows the console at the bottom of the battle screen and manages the text of the console
-class BattleConsole : public CIntObject, public IStatusBar
-{
-private:
-	std::shared_ptr<CPicture> background;
-
-	/// List of all texts added during battle, essentially - log of entire battle
-	std::vector< std::string > logEntries;
-
-	/// Current scrolling position, to allow showing older entries via scroll buttons
-	int scrollPosition;
-
-	/// current hover text set on mouse move, takes priority over log entries
-	std::string hoverText;
-
-	/// current text entered via in-game console, takes priority over both log entries and hover text
-	std::string consoleText;
-
-	/// if true then we are currently entering console text
-	bool enteringText;
-
-	/// splits text into individual strings for battle log
-	std::vector<std::string> splitText(const std::string &text);
-
-	/// select line(s) that will be visible in UI
-	std::vector<std::string> getVisibleText();
-public:
-	BattleConsole(std::shared_ptr<CPicture> backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size);
-
-	void showAll(Canvas & to) override;
-	void deactivate() override;
-
-	bool addText(const std::string &text); //adds text at the last position; returns false if failed (e.g. text longer than 70 characters)
-	void scrollUp(ui32 by = 1); //scrolls console up by 'by' positions
-	void scrollDown(ui32 by = 1); //scrolls console up by 'by' positions
-
-	// IStatusBar interface
-	void write(const std::string & Text) override;
-	void clearIfMatching(const std::string & Text) override;
-	void clear() override;
-	void setEnteringMode(bool on) override;
-	void setEnteredText(const std::string & text) override;
-};
-
-/// Hero battle animation
-class BattleHero : public CIntObject
-{
-	bool defender;
-
-	CFunctionList<void()> phaseFinishedCallback;
-
-	std::shared_ptr<CAnimation> animation;
-	std::shared_ptr<CAnimation> flagAnimation;
-
-	const CGHeroInstance * hero; //this animation's hero instance
-	const BattleInterface & owner; //battle interface to which this animation is assigned
-
-	EHeroAnimType phase; //stage of animation
-	EHeroAnimType nextPhase; //stage of animation to be set after current phase is fully displayed
-
-	float currentSpeed;
-	float currentFrame; //frame of animation
-	float flagCurrentFrame;
-
-	void switchToNextPhase();
-
-	void render(Canvas & canvas); //prints next frame of animation to to
-public:
-	const CGHeroInstance * instance();
-
-	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()> &);
-
-	void pause();
-	void play();
-
-	void heroLeftClicked();
-	void heroRightClicked();
-
-	BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender);
-};
-
-class HeroInfoBasicPanel : public CIntObject //extracted from InfoWindow to fit better as non-popup embed element
-{
-private:
-	std::shared_ptr<CPicture> background;
-	std::vector<std::shared_ptr<CLabel>> labels;
-	std::vector<std::shared_ptr<CAnimImage>> icons;
-public:
-	HeroInfoBasicPanel(const InfoAboutHero & hero, Point * position, bool initializeBackground = true);
-
-	void show(Canvas & to) override;
-
-	void initializeData(const InfoAboutHero & hero);
-	void update(const InfoAboutHero & updatedInfo);
-};
-
-class HeroInfoWindow : public CWindowObject
-{
-private:
-	std::shared_ptr<HeroInfoBasicPanel> content;
-public:
-	HeroInfoWindow(const InfoAboutHero & hero, Point * position);
-};
-
-/// Class which is responsible for showing the battle result window
-class BattleResultWindow : public WindowBase
-{
-private:
-	std::shared_ptr<CPicture> background;
-	std::vector<std::shared_ptr<CLabel>> labels;
-	std::shared_ptr<CButton> exit;
-	std::shared_ptr<CButton> repeat;
-	std::vector<std::shared_ptr<CAnimImage>> icons;
-	std::shared_ptr<CTextBox> description;
-	CPlayerInterface & owner;
-
-	enum BattleResultVideo
-	{
-		NONE,
-		WIN,
-		SURRENDER,
-		RETREAT,
-		RETREAT_LOOP,
-		DEFEAT,
-		DEFEAT_LOOP,
-		DEFEAT_SIEGE,
-		DEFEAT_SIEGE_LOOP,
-		WIN_SIEGE,
-		WIN_SIEGE_LOOP,
-	};
-	BattleResultVideo currentVideo;
-
-	void playVideo(bool startLoop = false);
-	
-	void buttonPressed(int button); //internal function for button callbacks
-public:
-	BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay = false);
-
-	void bExitf(); //exit button callback
-	void bRepeatf(); //repeat button callback
-	std::function<void(int result)> resultCallback; //callback receiving which button was pressed
-
-	void activate() override;
-	void show(Canvas & to) override;
-};
-
-/// Shows the stack queue
-class StackQueue : public CIntObject
-{
-	class StackBox : public CIntObject
-	{
-		StackQueue * owner;
-		std::optional<uint32_t> boundUnitID;
-
-		std::shared_ptr<CPicture> background;
-		std::shared_ptr<CAnimImage> icon;
-		std::shared_ptr<CLabel> amount;
-		std::shared_ptr<CAnimImage> stateIcon;
-
-		void show(Canvas & to) override;
-		void showAll(Canvas & to) override;
-
-		bool isBoundUnitHighlighted() const;
-	public:
-		StackBox(StackQueue * owner);
-		void setUnit(const battle::Unit * unit, size_t turn = 0);
-		std::optional<uint32_t> getBoundUnitID() const;
-
-	};
-
-	static const int QUEUE_SIZE = 10;
-	std::shared_ptr<CFilledTexture> background;
-	std::vector<std::shared_ptr<StackBox>> stackBoxes;
-	BattleInterface & owner;
-
-	std::shared_ptr<CAnimation> icons;
-	std::shared_ptr<CAnimation> stateIcons;
-
-	int32_t getSiegeShooterIconID();
-public:
-	const bool embedded;
-
-	StackQueue(bool Embedded, BattleInterface & owner);
-	void update();
-	std::optional<uint32_t> getHoveredUnitIdIfAny() const;
-
-	void show(Canvas & to) override;
-};
+/*
+ * BattleInterfaceClasses.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 "BattleConstants.h"
+#include "../gui/CIntObject.h"
+#include "../../lib/FunctionList.h"
+#include "../../lib/battle/BattleHex.h"
+#include "../windows/CWindowObject.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class CGHeroInstance;
+struct BattleResult;
+struct InfoAboutHero;
+class CStack;
+
+namespace battle
+{
+class Unit;
+}
+
+VCMI_LIB_NAMESPACE_END
+
+class CAnimation;
+class Canvas;
+class BattleInterface;
+class CPicture;
+class CFilledTexture;
+class CButton;
+class CToggleButton;
+class CLabel;
+class CTextBox;
+class CAnimImage;
+class CPlayerInterface;
+class BattleRenderer;
+
+/// Class which shows the console at the bottom of the battle screen and manages the text of the console
+class BattleConsole : public CIntObject, public IStatusBar
+{
+private:
+	std::shared_ptr<CPicture> background;
+
+	/// List of all texts added during battle, essentially - log of entire battle
+	std::vector< std::string > logEntries;
+
+	/// Current scrolling position, to allow showing older entries via scroll buttons
+	int scrollPosition;
+
+	/// current hover text set on mouse move, takes priority over log entries
+	std::string hoverText;
+
+	/// current text entered via in-game console, takes priority over both log entries and hover text
+	std::string consoleText;
+
+	/// if true then we are currently entering console text
+	bool enteringText;
+
+	/// splits text into individual strings for battle log
+	std::vector<std::string> splitText(const std::string &text);
+
+	/// select line(s) that will be visible in UI
+	std::vector<std::string> getVisibleText();
+public:
+	BattleConsole(std::shared_ptr<CPicture> backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size);
+
+	void showAll(Canvas & to) override;
+	void deactivate() override;
+
+	bool addText(const std::string &text); //adds text at the last position; returns false if failed (e.g. text longer than 70 characters)
+	void scrollUp(ui32 by = 1); //scrolls console up by 'by' positions
+	void scrollDown(ui32 by = 1); //scrolls console up by 'by' positions
+
+	// IStatusBar interface
+	void write(const std::string & Text) override;
+	void clearIfMatching(const std::string & Text) override;
+	void clear() override;
+	void setEnteringMode(bool on) override;
+	void setEnteredText(const std::string & text) override;
+};
+
+/// Hero battle animation
+class BattleHero : public CIntObject
+{
+	bool defender;
+
+	CFunctionList<void()> phaseFinishedCallback;
+
+	std::shared_ptr<CAnimation> animation;
+	std::shared_ptr<CAnimation> flagAnimation;
+
+	const CGHeroInstance * hero; //this animation's hero instance
+	const BattleInterface & owner; //battle interface to which this animation is assigned
+
+	EHeroAnimType phase; //stage of animation
+	EHeroAnimType nextPhase; //stage of animation to be set after current phase is fully displayed
+
+	float currentSpeed;
+	float currentFrame; //frame of animation
+	float flagCurrentFrame;
+
+	void switchToNextPhase();
+
+	void render(Canvas & canvas); //prints next frame of animation to to
+public:
+	const CGHeroInstance * instance();
+
+	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()> &);
+
+	void pause();
+	void play();
+
+	void heroLeftClicked();
+	void heroRightClicked();
+
+	BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender);
+};
+
+class HeroInfoBasicPanel : public CIntObject //extracted from InfoWindow to fit better as non-popup embed element
+{
+private:
+	std::shared_ptr<CPicture> background;
+	std::vector<std::shared_ptr<CLabel>> labels;
+	std::vector<std::shared_ptr<CAnimImage>> icons;
+public:
+	HeroInfoBasicPanel(const InfoAboutHero & hero, Point * position, bool initializeBackground = true);
+
+	void show(Canvas & to) override;
+
+	void initializeData(const InfoAboutHero & hero);
+	void update(const InfoAboutHero & updatedInfo);
+};
+
+class HeroInfoWindow : public CWindowObject
+{
+private:
+	std::shared_ptr<HeroInfoBasicPanel> content;
+public:
+	HeroInfoWindow(const InfoAboutHero & hero, Point * position);
+};
+
+/// Class which is responsible for showing the battle result window
+class BattleResultWindow : public WindowBase
+{
+private:
+	std::shared_ptr<CPicture> background;
+	std::vector<std::shared_ptr<CLabel>> labels;
+	std::shared_ptr<CButton> exit;
+	std::shared_ptr<CButton> repeat;
+	std::vector<std::shared_ptr<CAnimImage>> icons;
+	std::shared_ptr<CTextBox> description;
+	CPlayerInterface & owner;
+
+	enum BattleResultVideo
+	{
+		NONE,
+		WIN,
+		SURRENDER,
+		RETREAT,
+		RETREAT_LOOP,
+		DEFEAT,
+		DEFEAT_LOOP,
+		DEFEAT_SIEGE,
+		DEFEAT_SIEGE_LOOP,
+		WIN_SIEGE,
+		WIN_SIEGE_LOOP,
+	};
+	BattleResultVideo currentVideo;
+
+	void playVideo(bool startLoop = false);
+	
+	void buttonPressed(int button); //internal function for button callbacks
+public:
+	BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay = false);
+
+	void bExitf(); //exit button callback
+	void bRepeatf(); //repeat button callback
+	std::function<void(int result)> resultCallback; //callback receiving which button was pressed
+
+	void activate() override;
+	void show(Canvas & to) override;
+};
+
+/// Shows the stack queue
+class StackQueue : public CIntObject
+{
+	class StackBox : public CIntObject
+	{
+		StackQueue * owner;
+		std::optional<uint32_t> boundUnitID;
+
+		std::shared_ptr<CPicture> background;
+		std::shared_ptr<CAnimImage> icon;
+		std::shared_ptr<CLabel> amount;
+		std::shared_ptr<CAnimImage> stateIcon;
+
+		void show(Canvas & to) override;
+		void showAll(Canvas & to) override;
+
+		bool isBoundUnitHighlighted() const;
+	public:
+		StackBox(StackQueue * owner);
+		void setUnit(const battle::Unit * unit, size_t turn = 0);
+		std::optional<uint32_t> getBoundUnitID() const;
+
+	};
+
+	static const int QUEUE_SIZE = 10;
+	std::shared_ptr<CFilledTexture> background;
+	std::vector<std::shared_ptr<StackBox>> stackBoxes;
+	BattleInterface & owner;
+
+	std::shared_ptr<CAnimation> icons;
+	std::shared_ptr<CAnimation> stateIcons;
+
+	int32_t getSiegeShooterIconID();
+public:
+	const bool embedded;
+
+	StackQueue(bool Embedded, BattleInterface & owner);
+	void update();
+	std::optional<uint32_t> getHoveredUnitIdIfAny() const;
+
+	void show(Canvas & to) override;
+};

+ 228 - 228
client/battle/BattleObstacleController.cpp

@@ -1,228 +1,228 @@
-/*
- * BattleObstacleController.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 "BattleObstacleController.h"
-
-#include "BattleInterface.h"
-#include "BattleFieldController.h"
-#include "BattleAnimationClasses.h"
-#include "BattleStacksController.h"
-#include "BattleRenderer.h"
-#include "CreatureAnimation.h"
-
-#include "../CMusicHandler.h"
-#include "../CGameInfo.h"
-#include "../CPlayerInterface.h"
-#include "../gui/CGuiHandler.h"
-#include "../render/Canvas.h"
-#include "../render/IRenderHandler.h"
-
-#include "../../CCallback.h"
-#include "../../lib/battle/CObstacleInstance.h"
-#include "../../lib/ObstacleHandler.h"
-
-BattleObstacleController::BattleObstacleController(BattleInterface & owner):
-	owner(owner),
-	timePassed(0.f)
-{
-	auto obst = owner.getBattle()->battleGetAllObstacles();
-	for(auto & elem : obst)
-	{
-		if ( elem->obstacleType == CObstacleInstance::MOAT )
-			continue; // handled by siege controller;
-		loadObstacleImage(*elem);
-	}
-}
-
-void BattleObstacleController::loadObstacleImage(const CObstacleInstance & oi)
-{
-	AnimationPath animationName = oi.getAnimation();
-
-	if (animationsCache.count(animationName) == 0)
-	{
-		if (oi.obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE)
-		{
-			// obstacle uses single bitmap image for animations
-			auto animation = GH.renderHandler().createAnimation();
-			animation->setCustom(animationName.getName(), 0, 0);
-			animationsCache[animationName] = animation;
-			animation->preload();
-		}
-		else
-		{
-			auto animation = GH.renderHandler().loadAnimation(animationName);
-			animationsCache[animationName] = animation;
-			animation->preload();
-		}
-	}
-	obstacleAnimations[oi.uniqueID] = animationsCache[animationName];
-}
-
-void BattleObstacleController::obstacleRemoved(const std::vector<ObstacleChanges> & obstacles)
-{
-	for(const auto & oi : obstacles)
-	{
-		auto & obstacle = oi.data["obstacle"];
-
-		if (!obstacle.isStruct())
-		{
-			logGlobal->error("I don't know how to animate removal of this obstacle");
-			continue;
-		}
-
-		auto animation = GH.renderHandler().loadAnimation(AnimationPath::fromJson(obstacle["appearAnimation"]));
-		animation->preload();
-
-		auto first = animation->getImage(0, 0);
-		if(!first)
-			continue;
-
-		//we assume here that effect graphics have the same size as the usual obstacle image
-		// -> if we know how to blit obstacle, let's blit the effect in the same place
-		Point whereTo = getObstaclePosition(first, obstacle);
-		//AFAIK, in H3 there is no sound of obstacle removal
-		owner.stacksController->addNewAnim(new EffectAnimation(owner, AnimationPath::fromJson(obstacle["appearAnimation"]), whereTo, obstacle["position"].Integer(), 0, true));
-
-		obstacleAnimations.erase(oi.id);
-		//so when multiple obstacles are removed, they show up one after another
-		owner.waitForAnimations();
-	}
-}
-
-void BattleObstacleController::obstaclePlaced(const std::vector<std::shared_ptr<const CObstacleInstance>> & obstacles)
-{
-	for(const auto & oi : obstacles)
-	{
-		auto side = owner.getBattle()->playerToSide(owner.curInt->playerID);
-
-		if(!oi->visibleForSide(side.value(), owner.getBattle()->battleHasNativeStack(side.value())))
-			continue;
-
-		auto animation = GH.renderHandler().loadAnimation(oi->getAppearAnimation());
-		animation->preload();
-
-		auto first = animation->getImage(0, 0);
-		if(!first)
-			continue;
-
-		//we assume here that effect graphics have the same size as the usual obstacle image
-		// -> if we know how to blit obstacle, let's blit the effect in the same place
-		Point whereTo = getObstaclePosition(first, *oi);
-		CCS->soundh->playSound( oi->getAppearSound() );
-		owner.stacksController->addNewAnim(new EffectAnimation(owner, oi->getAppearAnimation(), whereTo, oi->pos));
-
-		//so when multiple obstacles are added, they show up one after another
-		owner.waitForAnimations();
-
-		loadObstacleImage(*oi);
-	}
-}
-
-void BattleObstacleController::showAbsoluteObstacles(Canvas & canvas)
-{
-	//Blit absolute obstacles
-	for(auto & obstacle : owner.getBattle()->battleGetAllObstacles())
-	{
-		if(obstacle->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE)
-		{
-			auto img = getObstacleImage(*obstacle);
-			if(img)
-				canvas.draw(img, Point(obstacle->getInfo().width, obstacle->getInfo().height));
-		}
-
-		if (obstacle->obstacleType == CObstacleInstance::USUAL)
-		{
-			if (obstacle->getInfo().isForegroundObstacle)
-				continue;
-
-			auto img = getObstacleImage(*obstacle);
-			if(img)
-			{
-				Point p = getObstaclePosition(img, *obstacle);
-				canvas.draw(img, p);
-			}
-		}
-	}
-}
-
-void BattleObstacleController::collectRenderableObjects(BattleRenderer & renderer)
-{
-	for (auto obstacle : owner.getBattle()->battleGetAllObstacles())
-	{
-		if (obstacle->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE)
-			continue;
-
-		if (obstacle->obstacleType == CObstacleInstance::MOAT)
-			continue;
-
-		if (obstacle->obstacleType == CObstacleInstance::USUAL && !obstacle->getInfo().isForegroundObstacle)
-			continue;
-
-		renderer.insert(EBattleFieldLayer::OBSTACLES, obstacle->pos, [this, obstacle]( BattleRenderer::RendererRef canvas ){
-			auto img = getObstacleImage(*obstacle);
-			if(img)
-			{
-				Point p = getObstaclePosition(img, *obstacle);
-				canvas.draw(img, p);
-			}
-		});
-	}
-}
-
-void BattleObstacleController::tick(uint32_t msPassed)
-{
-	timePassed += msPassed / 1000.f;
-}
-
-std::shared_ptr<IImage> BattleObstacleController::getObstacleImage(const CObstacleInstance & oi)
-{
-	int framesCount = timePassed * AnimationControls::getObstaclesSpeed();
-	std::shared_ptr<CAnimation> animation;
-
-	// obstacle is not loaded yet, don't show anything
-	if (obstacleAnimations.count(oi.uniqueID) == 0)
-		return nullptr;
-
-	animation = obstacleAnimations[oi.uniqueID];
-	assert(animation);
-
-	if(animation)
-	{
-		int frameIndex = framesCount % animation->size(0);
-		return animation->getImage(frameIndex, 0);
-	}
-	return nullptr;
-}
-
-Point BattleObstacleController::getObstaclePosition(std::shared_ptr<IImage> image, const CObstacleInstance & obstacle)
-{
-	int offset = obstacle.getAnimationYOffset(image->height());
-
-	Rect r = owner.fieldController->hexPositionLocal(obstacle.pos);
-	r.y += 42 - image->height() + offset;
-
-	return r.topLeft();
-}
-
-Point BattleObstacleController::getObstaclePosition(std::shared_ptr<IImage> image, const JsonNode & obstacle)
-{
-	auto animationYOffset = obstacle["animationYOffset"].Integer();
-	auto offset = image->height() % 42;
-
-	if(obstacle["needAnimationOffsetFix"].Bool() && offset > 37)
-		animationYOffset -= 42;
-
-	offset += animationYOffset;
-
-	Rect r = owner.fieldController->hexPositionLocal(obstacle["position"].Integer());
-	r.y += 42 - image->height() + offset;
-
-	return r.topLeft();
-}
+/*
+ * BattleObstacleController.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 "BattleObstacleController.h"
+
+#include "BattleInterface.h"
+#include "BattleFieldController.h"
+#include "BattleAnimationClasses.h"
+#include "BattleStacksController.h"
+#include "BattleRenderer.h"
+#include "CreatureAnimation.h"
+
+#include "../CMusicHandler.h"
+#include "../CGameInfo.h"
+#include "../CPlayerInterface.h"
+#include "../gui/CGuiHandler.h"
+#include "../render/Canvas.h"
+#include "../render/IRenderHandler.h"
+
+#include "../../CCallback.h"
+#include "../../lib/battle/CObstacleInstance.h"
+#include "../../lib/ObstacleHandler.h"
+
+BattleObstacleController::BattleObstacleController(BattleInterface & owner):
+	owner(owner),
+	timePassed(0.f)
+{
+	auto obst = owner.getBattle()->battleGetAllObstacles();
+	for(auto & elem : obst)
+	{
+		if ( elem->obstacleType == CObstacleInstance::MOAT )
+			continue; // handled by siege controller;
+		loadObstacleImage(*elem);
+	}
+}
+
+void BattleObstacleController::loadObstacleImage(const CObstacleInstance & oi)
+{
+	AnimationPath animationName = oi.getAnimation();
+
+	if (animationsCache.count(animationName) == 0)
+	{
+		if (oi.obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE)
+		{
+			// obstacle uses single bitmap image for animations
+			auto animation = GH.renderHandler().createAnimation();
+			animation->setCustom(animationName.getName(), 0, 0);
+			animationsCache[animationName] = animation;
+			animation->preload();
+		}
+		else
+		{
+			auto animation = GH.renderHandler().loadAnimation(animationName);
+			animationsCache[animationName] = animation;
+			animation->preload();
+		}
+	}
+	obstacleAnimations[oi.uniqueID] = animationsCache[animationName];
+}
+
+void BattleObstacleController::obstacleRemoved(const std::vector<ObstacleChanges> & obstacles)
+{
+	for(const auto & oi : obstacles)
+	{
+		auto & obstacle = oi.data["obstacle"];
+
+		if (!obstacle.isStruct())
+		{
+			logGlobal->error("I don't know how to animate removal of this obstacle");
+			continue;
+		}
+
+		auto animation = GH.renderHandler().loadAnimation(AnimationPath::fromJson(obstacle["appearAnimation"]));
+		animation->preload();
+
+		auto first = animation->getImage(0, 0);
+		if(!first)
+			continue;
+
+		//we assume here that effect graphics have the same size as the usual obstacle image
+		// -> if we know how to blit obstacle, let's blit the effect in the same place
+		Point whereTo = getObstaclePosition(first, obstacle);
+		//AFAIK, in H3 there is no sound of obstacle removal
+		owner.stacksController->addNewAnim(new EffectAnimation(owner, AnimationPath::fromJson(obstacle["appearAnimation"]), whereTo, obstacle["position"].Integer(), 0, true));
+
+		obstacleAnimations.erase(oi.id);
+		//so when multiple obstacles are removed, they show up one after another
+		owner.waitForAnimations();
+	}
+}
+
+void BattleObstacleController::obstaclePlaced(const std::vector<std::shared_ptr<const CObstacleInstance>> & obstacles)
+{
+	for(const auto & oi : obstacles)
+	{
+		auto side = owner.getBattle()->playerToSide(owner.curInt->playerID);
+
+		if(!oi->visibleForSide(side.value(), owner.getBattle()->battleHasNativeStack(side.value())))
+			continue;
+
+		auto animation = GH.renderHandler().loadAnimation(oi->getAppearAnimation());
+		animation->preload();
+
+		auto first = animation->getImage(0, 0);
+		if(!first)
+			continue;
+
+		//we assume here that effect graphics have the same size as the usual obstacle image
+		// -> if we know how to blit obstacle, let's blit the effect in the same place
+		Point whereTo = getObstaclePosition(first, *oi);
+		CCS->soundh->playSound( oi->getAppearSound() );
+		owner.stacksController->addNewAnim(new EffectAnimation(owner, oi->getAppearAnimation(), whereTo, oi->pos));
+
+		//so when multiple obstacles are added, they show up one after another
+		owner.waitForAnimations();
+
+		loadObstacleImage(*oi);
+	}
+}
+
+void BattleObstacleController::showAbsoluteObstacles(Canvas & canvas)
+{
+	//Blit absolute obstacles
+	for(auto & obstacle : owner.getBattle()->battleGetAllObstacles())
+	{
+		if(obstacle->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE)
+		{
+			auto img = getObstacleImage(*obstacle);
+			if(img)
+				canvas.draw(img, Point(obstacle->getInfo().width, obstacle->getInfo().height));
+		}
+
+		if (obstacle->obstacleType == CObstacleInstance::USUAL)
+		{
+			if (obstacle->getInfo().isForegroundObstacle)
+				continue;
+
+			auto img = getObstacleImage(*obstacle);
+			if(img)
+			{
+				Point p = getObstaclePosition(img, *obstacle);
+				canvas.draw(img, p);
+			}
+		}
+	}
+}
+
+void BattleObstacleController::collectRenderableObjects(BattleRenderer & renderer)
+{
+	for (auto obstacle : owner.getBattle()->battleGetAllObstacles())
+	{
+		if (obstacle->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE)
+			continue;
+
+		if (obstacle->obstacleType == CObstacleInstance::MOAT)
+			continue;
+
+		if (obstacle->obstacleType == CObstacleInstance::USUAL && !obstacle->getInfo().isForegroundObstacle)
+			continue;
+
+		renderer.insert(EBattleFieldLayer::OBSTACLES, obstacle->pos, [this, obstacle]( BattleRenderer::RendererRef canvas ){
+			auto img = getObstacleImage(*obstacle);
+			if(img)
+			{
+				Point p = getObstaclePosition(img, *obstacle);
+				canvas.draw(img, p);
+			}
+		});
+	}
+}
+
+void BattleObstacleController::tick(uint32_t msPassed)
+{
+	timePassed += msPassed / 1000.f;
+}
+
+std::shared_ptr<IImage> BattleObstacleController::getObstacleImage(const CObstacleInstance & oi)
+{
+	int framesCount = timePassed * AnimationControls::getObstaclesSpeed();
+	std::shared_ptr<CAnimation> animation;
+
+	// obstacle is not loaded yet, don't show anything
+	if (obstacleAnimations.count(oi.uniqueID) == 0)
+		return nullptr;
+
+	animation = obstacleAnimations[oi.uniqueID];
+	assert(animation);
+
+	if(animation)
+	{
+		int frameIndex = framesCount % animation->size(0);
+		return animation->getImage(frameIndex, 0);
+	}
+	return nullptr;
+}
+
+Point BattleObstacleController::getObstaclePosition(std::shared_ptr<IImage> image, const CObstacleInstance & obstacle)
+{
+	int offset = obstacle.getAnimationYOffset(image->height());
+
+	Rect r = owner.fieldController->hexPositionLocal(obstacle.pos);
+	r.y += 42 - image->height() + offset;
+
+	return r.topLeft();
+}
+
+Point BattleObstacleController::getObstaclePosition(std::shared_ptr<IImage> image, const JsonNode & obstacle)
+{
+	auto animationYOffset = obstacle["animationYOffset"].Integer();
+	auto offset = image->height() % 42;
+
+	if(obstacle["needAnimationOffsetFix"].Bool() && offset > 37)
+		animationYOffset -= 42;
+
+	offset += animationYOffset;
+
+	Rect r = owner.fieldController->hexPositionLocal(obstacle["position"].Integer());
+	r.y += 42 - image->height() + offset;
+
+	return r.topLeft();
+}

+ 68 - 68
client/battle/BattleObstacleController.h

@@ -1,68 +1,68 @@
-/*
- * BattleObstacleController.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/filesystem/ResourcePath.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-struct BattleHex;
-struct CObstacleInstance;
-class JsonNode;
-class ObstacleChanges;
-class Point;
-
-VCMI_LIB_NAMESPACE_END
-
-class IImage;
-class Canvas;
-class CAnimation;
-class BattleInterface;
-class BattleRenderer;
-
-/// Controls all currently active projectiles on the battlefield
-/// (with exception of moat, which is apparently handled by siege controller)
-class BattleObstacleController
-{
-	BattleInterface & owner;
-
-	/// total time, in seconds, since start of battle. Used for animating obstacles
-	float timePassed;
-
-	/// cached animations of all obstacles in current battle
-	std::map<AnimationPath, std::shared_ptr<CAnimation>> animationsCache;
-
-	/// list of all obstacles that are currently being rendered
-	std::map<si32, std::shared_ptr<CAnimation>> obstacleAnimations;
-
-	void loadObstacleImage(const CObstacleInstance & oi);
-
-	std::shared_ptr<IImage> getObstacleImage(const CObstacleInstance & oi);
-	Point getObstaclePosition(std::shared_ptr<IImage> image, const CObstacleInstance & obstacle);
-	Point getObstaclePosition(std::shared_ptr<IImage> image, const JsonNode & obstacle);
-
-public:
-	BattleObstacleController(BattleInterface & owner);
-
-	/// called every frame
-	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);
-
-	/// call-in from network pack, remove required obstacles with any required animations
-	void obstacleRemoved(const std::vector<ObstacleChanges> & obstacles);
-
-	/// renders all "absolute" obstacles
-	void showAbsoluteObstacles(Canvas & canvas);
-
-	/// adds all non-"absolute" visible obstacles for rendering
-	void collectRenderableObjects(BattleRenderer & renderer);
-};
+/*
+ * BattleObstacleController.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/filesystem/ResourcePath.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+struct BattleHex;
+struct CObstacleInstance;
+class JsonNode;
+class ObstacleChanges;
+class Point;
+
+VCMI_LIB_NAMESPACE_END
+
+class IImage;
+class Canvas;
+class CAnimation;
+class BattleInterface;
+class BattleRenderer;
+
+/// Controls all currently active projectiles on the battlefield
+/// (with exception of moat, which is apparently handled by siege controller)
+class BattleObstacleController
+{
+	BattleInterface & owner;
+
+	/// total time, in seconds, since start of battle. Used for animating obstacles
+	float timePassed;
+
+	/// cached animations of all obstacles in current battle
+	std::map<AnimationPath, std::shared_ptr<CAnimation>> animationsCache;
+
+	/// list of all obstacles that are currently being rendered
+	std::map<si32, std::shared_ptr<CAnimation>> obstacleAnimations;
+
+	void loadObstacleImage(const CObstacleInstance & oi);
+
+	std::shared_ptr<IImage> getObstacleImage(const CObstacleInstance & oi);
+	Point getObstaclePosition(std::shared_ptr<IImage> image, const CObstacleInstance & obstacle);
+	Point getObstaclePosition(std::shared_ptr<IImage> image, const JsonNode & obstacle);
+
+public:
+	BattleObstacleController(BattleInterface & owner);
+
+	/// called every frame
+	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);
+
+	/// call-in from network pack, remove required obstacles with any required animations
+	void obstacleRemoved(const std::vector<ObstacleChanges> & obstacles);
+
+	/// renders all "absolute" obstacles
+	void showAbsoluteObstacles(Canvas & canvas);
+
+	/// adds all non-"absolute" visible obstacles for rendering
+	void collectRenderableObjects(BattleRenderer & renderer);
+};

+ 386 - 386
client/battle/BattleProjectileController.cpp

@@ -1,386 +1,386 @@
-/*
- * BattleProjectileController.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 "BattleProjectileController.h"
-
-#include "BattleInterface.h"
-#include "BattleSiegeController.h"
-#include "BattleStacksController.h"
-#include "CreatureAnimation.h"
-
-#include "../render/Canvas.h"
-#include "../render/IRenderHandler.h"
-#include "../gui/CGuiHandler.h"
-#include "../CGameInfo.h"
-
-#include "../../lib/CStack.h"
-#include "../../lib/mapObjects/CGTownInstance.h"
-
-static double calculateCatapultParabolaY(const Point & from, const Point & dest, int x)
-{
-	double facA = 0.005; // seems to be constant
-
-	// system of 2 linear equations, solutions of which are missing coefficients
-	// for quadratic equation a*x*x + b*x + c
-	double eq[2][3] = {
-		{ static_cast<double>(from.x), 1.0, from.y - facA*from.x*from.x },
-		{ static_cast<double>(dest.x), 1.0, dest.y - facA*dest.x*dest.x }
-	};
-
-	// solve system via determinants
-	double det  = eq[0][0] *eq[1][1] - eq[1][0] *eq[0][1];
-	double detB = eq[0][2] *eq[1][1] - eq[1][2] *eq[0][1];
-	double detC = eq[0][0] *eq[1][2] - eq[1][0] *eq[0][2];
-
-	double facB = detB / det;
-	double facC = detC / det;
-
-	return facA *pow(x, 2.0) + facB *x + facC;
-}
-
-void ProjectileMissile::show(Canvas & canvas)
-{
-	size_t group = reverse ? 1 : 0;
-	auto image = animation->getImage(frameNum, group, true);
-
-	if(image)
-	{
-		Point pos {
-			vstd::lerp(from.x, dest.x, progress) - image->width() / 2,
-			vstd::lerp(from.y, dest.y, progress) - image->height() / 2,
-		};
-
-		canvas.draw(image, pos);
-	}
-}
-
-void ProjectileMissile::tick(uint32_t msPassed)
-{
-	float timePassed = msPassed / 1000.f;
-	progress += timePassed * speed;
-}
-
-void ProjectileAnimatedMissile::tick(uint32_t msPassed)
-{
-	ProjectileMissile::tick(msPassed);
-	frameProgress += AnimationControls::getSpellEffectSpeed() * msPassed / 1000;
-	size_t animationSize = animation->size(reverse ? 1 : 0);
-	while (frameProgress > animationSize)
-		frameProgress -= animationSize;
-
-	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)
-{
-	int frameCounter = std::floor(frameProgress);
-	int frameIndex = (frameCounter + 1) % animation->size(0);
-
-	auto image = animation->getImage(frameIndex, 0, true);
-
-	if(image)
-	{
-		int posX = vstd::lerp(from.x, dest.x, progress);
-		int posY = calculateCatapultParabolaY(from, dest, posX);
-		Point pos(posX, posY);
-
-		canvas.draw(image, pos);
-	}
-}
-
-void ProjectileRay::show(Canvas & canvas)
-{
-	Point curr {
-		vstd::lerp(from.x, dest.x, progress),
-		vstd::lerp(from.y, dest.y, progress),
-	};
-
-	Point length = curr - from;
-
-	//select axis to draw ray on, we want angle to be less than 45 degrees so individual sub-rays won't overlap each other
-
-	if (std::abs(length.x) > std::abs(length.y)) // draw in horizontal axis
-	{
-		int y1 =  from.y - rayConfig.size() / 2;
-		int y2 =  curr.y - rayConfig.size() / 2;
-
-		int x1 = from.x;
-		int x2 = curr.x;
-
-		for (size_t i = 0; i < rayConfig.size(); ++i)
-		{
-			auto ray = rayConfig[i];
-			canvas.drawLine(Point(x1, y1 + i), Point(x2, y2+i), ray.start, ray.end);
-		}
-	}
-	else // draw in vertical axis
-	{
-		int x1 = from.x - rayConfig.size() / 2;
-		int x2 = curr.x - rayConfig.size() / 2;
-
-		int y1 = from.y;
-		int y2 = curr.y;
-
-		for (size_t i = 0; i < rayConfig.size(); ++i)
-		{
-			auto ray = rayConfig[i];
-
-			canvas.drawLine(Point(x1 + i, y1), Point(x2 + i, y2), ray.start, ray.end);
-		}
-	}
-}
-
-void ProjectileRay::tick(uint32_t msPassed)
-{
-	float timePassed = msPassed / 1000.f;
-	progress += timePassed * speed;
-}
-
-BattleProjectileController::BattleProjectileController(BattleInterface & owner):
-	owner(owner)
-{}
-
-const CCreature & BattleProjectileController::getShooter(const CStack * stack) const
-{
-	const CCreature * creature = stack->unitType();
-
-	if(creature->getId() == CreatureID::ARROW_TOWERS)
-		creature = owner.siegeController->getTurretCreature();
-
-	if(creature->animation.missleFrameAngles.empty())
-	{
-		logAnim->error("Mod error: Creature '%s' on the Archer's tower is not a shooter. Mod should be fixed. Trying to use archer's data instead...", creature->getNameSingularTranslated());
-		creature = CGI->creh->objects[CreatureID::ARCHER];
-	}
-
-	return *creature;
-}
-
-bool BattleProjectileController::stackUsesRayProjectile(const CStack * stack) const
-{
-	return !getShooter(stack).animation.projectileRay.empty();
-}
-
-bool BattleProjectileController::stackUsesMissileProjectile(const CStack * stack) const
-{
-	return !getShooter(stack).animation.projectileImageName.empty();
-}
-
-void BattleProjectileController::initStackProjectile(const CStack * stack)
-{
-	if (!stackUsesMissileProjectile(stack))
-		return;
-
-	const CCreature & creature = getShooter(stack);
-	projectilesCache[creature.animation.projectileImageName] = createProjectileImage(creature.animation.projectileImageName);
-}
-
-std::shared_ptr<CAnimation> BattleProjectileController::createProjectileImage(const AnimationPath & path )
-{
-	std::shared_ptr<CAnimation> projectile = GH.renderHandler().loadAnimation(path);
-	projectile->preload();
-
-	if(projectile->size(1) != 0)
-		logAnim->error("Expected empty group 1 in stack projectile");
-	else
-		projectile->createFlippedGroup(0, 1);
-
-	return projectile;
-}
-
-std::shared_ptr<CAnimation> BattleProjectileController::getProjectileImage(const CStack * stack)
-{
-	const CCreature & creature = getShooter(stack);
-	AnimationPath imageName = creature.animation.projectileImageName;
-
-	if (!projectilesCache.count(imageName))
-		initStackProjectile(stack);
-
-	return projectilesCache[imageName];
-}
-
-void BattleProjectileController::emitStackProjectile(const CStack * stack)
-{
-	int stackID = stack ? stack->unitId() : -1;
-
-	for (auto projectile : projectiles)
-	{
-		if ( !projectile->playing && projectile->shooterID == stackID)
-		{
-			projectile->playing = true;
-			return;
-		}
-	}
-}
-
-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;
-	});
-}
-
-bool BattleProjectileController::hasActiveProjectile(const CStack * stack, bool emittedOnly) const
-{
-	int stackID = stack ? stack->unitId() : -1;
-
-	for(auto const & instance : projectiles)
-	{
-		if(instance->shooterID == stackID && (instance->playing || !emittedOnly))
-		{
-			return true;
-		}
-	}
-	return false;
-}
-
-float BattleProjectileController::computeProjectileFlightTime( Point from, Point dest, double animSpeed)
-{
-	float distanceSquared = (dest.x - from.x) * (dest.x - from.x) + (dest.y - from.y) * (dest.y - from.y);
-	float distance = sqrt(distanceSquared);
-
-	assert(distance > 1.f);
-
-	return animSpeed / std::max( 1.f, distance);
-}
-
-int BattleProjectileController::computeProjectileFrameID( Point from, Point dest, const CStack * stack)
-{
-	const CCreature & creature = getShooter(stack);
-
-	auto & angles = creature.animation.missleFrameAngles;
-	auto animation = getProjectileImage(stack);
-
-	// only frames below maxFrame are usable: anything  higher is either no present or we don't know when it should be used
-	size_t maxFrame = std::min<size_t>(angles.size(), animation->size(0));
-
-	assert(maxFrame > 0);
-	double projectileAngle = -atan2(dest.y - from.y, std::abs(dest.x - from.x));
-
-	// values in angles array indicate position from which this frame was rendered, in degrees.
-	// possible range is 90 ... -90, where projectile for +90 will be used for shooting upwards, +0 for shots towards right and -90 for downwards shots
-	// find frame that has closest angle to one that we need for this shot
-	int bestID = 0;
-	double bestDiff = fabs( angles[0] / 180 * M_PI - projectileAngle );
-
-	for (int i=1; i<maxFrame; i++)
-	{
-		double currentDiff = fabs( angles[i] / 180 * M_PI - projectileAngle );
-		if (currentDiff < bestDiff)
-		{
-			bestID = i;
-			bestDiff = currentDiff;
-		}
-	}
-	return bestID;
-}
-
-void BattleProjectileController::createCatapultProjectile(const CStack * shooter, Point from, Point dest)
-{
-	auto catapultProjectile       = new ProjectileCatapult();
-
-	catapultProjectile->animation = getProjectileImage(shooter);
-	catapultProjectile->progress  = 0;
-	catapultProjectile->speed     = computeProjectileFlightTime(from, dest, AnimationControls::getCatapultSpeed());
-	catapultProjectile->from      = from;
-	catapultProjectile->dest      = dest;
-	catapultProjectile->shooterID = shooter->unitId();
-	catapultProjectile->playing   = false;
-	catapultProjectile->frameProgress = 0.f;
-
-	projectiles.push_back(std::shared_ptr<ProjectileBase>(catapultProjectile));
-}
-
-void BattleProjectileController::createProjectile(const CStack * shooter, Point from, Point dest)
-{
-	const CCreature & shooterInfo = getShooter(shooter);
-
-	std::shared_ptr<ProjectileBase> projectile;
-	if (stackUsesRayProjectile(shooter) && stackUsesMissileProjectile(shooter))
-	{
-		logAnim->error("Mod error: Creature '%s' has both missile and ray projectiles configured. Mod should be fixed. Using ray projectile configuration...", shooterInfo.getNameSingularTranslated());
-	}
-
-	if (stackUsesRayProjectile(shooter))
-	{
-		auto rayProjectile = new ProjectileRay();
-		projectile.reset(rayProjectile);
-
-		rayProjectile->rayConfig = shooterInfo.animation.projectileRay;
-		rayProjectile->speed     = computeProjectileFlightTime(from, dest, AnimationControls::getRayProjectileSpeed());
-	}
-	else if (stackUsesMissileProjectile(shooter))
-	{
-		auto missileProjectile = new ProjectileMissile();
-		projectile.reset(missileProjectile);
-
-		missileProjectile->animation = getProjectileImage(shooter);
-		missileProjectile->reverse   = !owner.stacksController->facingRight(shooter);
-		missileProjectile->frameNum  = computeProjectileFrameID(from, dest, shooter);
-		missileProjectile->speed     = computeProjectileFlightTime(from, dest, AnimationControls::getProjectileSpeed());
-	}
-
-
-	projectile->from      = from;
-	projectile->dest      = dest;
-	projectile->shooterID = shooter->unitId();
-	projectile->progress  = 0;
-	projectile->playing   = false;
-
-	projectiles.push_back(projectile);
-}
-
-void BattleProjectileController::createSpellProjectile(const CStack * shooter, Point from, Point dest, const CSpell * spell)
-{
-	double projectileAngle = std::abs(atan2(dest.x - from.x, dest.y - from.y));
-	AnimationPath animToDisplay = spell->animationInfo.selectProjectile(projectileAngle);
-
-	assert(!animToDisplay.empty());
-
-	if(!animToDisplay.empty())
-	{
-		auto projectile = new ProjectileAnimatedMissile();
-
-		projectile->animation     = createProjectileImage(animToDisplay);
-		projectile->frameProgress = 0;
-		projectile->frameNum      = 0;
-		projectile->reverse       = from.x > dest.x;
-		projectile->from          = from;
-		projectile->dest          = dest;
-		projectile->shooterID     = shooter ? shooter->unitId() : -1;
-		projectile->progress      = 0;
-		projectile->speed         = computeProjectileFlightTime(from, dest, AnimationControls::getProjectileSpeed());
-		projectile->playing       = false;
-
-		projectiles.push_back(std::shared_ptr<ProjectileBase>(projectile));
-	}
-}
+/*
+ * BattleProjectileController.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 "BattleProjectileController.h"
+
+#include "BattleInterface.h"
+#include "BattleSiegeController.h"
+#include "BattleStacksController.h"
+#include "CreatureAnimation.h"
+
+#include "../render/Canvas.h"
+#include "../render/IRenderHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../CGameInfo.h"
+
+#include "../../lib/CStack.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
+
+static double calculateCatapultParabolaY(const Point & from, const Point & dest, int x)
+{
+	double facA = 0.005; // seems to be constant
+
+	// system of 2 linear equations, solutions of which are missing coefficients
+	// for quadratic equation a*x*x + b*x + c
+	double eq[2][3] = {
+		{ static_cast<double>(from.x), 1.0, from.y - facA*from.x*from.x },
+		{ static_cast<double>(dest.x), 1.0, dest.y - facA*dest.x*dest.x }
+	};
+
+	// solve system via determinants
+	double det  = eq[0][0] *eq[1][1] - eq[1][0] *eq[0][1];
+	double detB = eq[0][2] *eq[1][1] - eq[1][2] *eq[0][1];
+	double detC = eq[0][0] *eq[1][2] - eq[1][0] *eq[0][2];
+
+	double facB = detB / det;
+	double facC = detC / det;
+
+	return facA *pow(x, 2.0) + facB *x + facC;
+}
+
+void ProjectileMissile::show(Canvas & canvas)
+{
+	size_t group = reverse ? 1 : 0;
+	auto image = animation->getImage(frameNum, group, true);
+
+	if(image)
+	{
+		Point pos {
+			vstd::lerp(from.x, dest.x, progress) - image->width() / 2,
+			vstd::lerp(from.y, dest.y, progress) - image->height() / 2,
+		};
+
+		canvas.draw(image, pos);
+	}
+}
+
+void ProjectileMissile::tick(uint32_t msPassed)
+{
+	float timePassed = msPassed / 1000.f;
+	progress += timePassed * speed;
+}
+
+void ProjectileAnimatedMissile::tick(uint32_t msPassed)
+{
+	ProjectileMissile::tick(msPassed);
+	frameProgress += AnimationControls::getSpellEffectSpeed() * msPassed / 1000;
+	size_t animationSize = animation->size(reverse ? 1 : 0);
+	while (frameProgress > animationSize)
+		frameProgress -= animationSize;
+
+	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)
+{
+	int frameCounter = std::floor(frameProgress);
+	int frameIndex = (frameCounter + 1) % animation->size(0);
+
+	auto image = animation->getImage(frameIndex, 0, true);
+
+	if(image)
+	{
+		int posX = vstd::lerp(from.x, dest.x, progress);
+		int posY = calculateCatapultParabolaY(from, dest, posX);
+		Point pos(posX, posY);
+
+		canvas.draw(image, pos);
+	}
+}
+
+void ProjectileRay::show(Canvas & canvas)
+{
+	Point curr {
+		vstd::lerp(from.x, dest.x, progress),
+		vstd::lerp(from.y, dest.y, progress),
+	};
+
+	Point length = curr - from;
+
+	//select axis to draw ray on, we want angle to be less than 45 degrees so individual sub-rays won't overlap each other
+
+	if (std::abs(length.x) > std::abs(length.y)) // draw in horizontal axis
+	{
+		int y1 =  from.y - rayConfig.size() / 2;
+		int y2 =  curr.y - rayConfig.size() / 2;
+
+		int x1 = from.x;
+		int x2 = curr.x;
+
+		for (size_t i = 0; i < rayConfig.size(); ++i)
+		{
+			auto ray = rayConfig[i];
+			canvas.drawLine(Point(x1, y1 + i), Point(x2, y2+i), ray.start, ray.end);
+		}
+	}
+	else // draw in vertical axis
+	{
+		int x1 = from.x - rayConfig.size() / 2;
+		int x2 = curr.x - rayConfig.size() / 2;
+
+		int y1 = from.y;
+		int y2 = curr.y;
+
+		for (size_t i = 0; i < rayConfig.size(); ++i)
+		{
+			auto ray = rayConfig[i];
+
+			canvas.drawLine(Point(x1 + i, y1), Point(x2 + i, y2), ray.start, ray.end);
+		}
+	}
+}
+
+void ProjectileRay::tick(uint32_t msPassed)
+{
+	float timePassed = msPassed / 1000.f;
+	progress += timePassed * speed;
+}
+
+BattleProjectileController::BattleProjectileController(BattleInterface & owner):
+	owner(owner)
+{}
+
+const CCreature & BattleProjectileController::getShooter(const CStack * stack) const
+{
+	const CCreature * creature = stack->unitType();
+
+	if(creature->getId() == CreatureID::ARROW_TOWERS)
+		creature = owner.siegeController->getTurretCreature();
+
+	if(creature->animation.missleFrameAngles.empty())
+	{
+		logAnim->error("Mod error: Creature '%s' on the Archer's tower is not a shooter. Mod should be fixed. Trying to use archer's data instead...", creature->getNameSingularTranslated());
+		creature = CGI->creh->objects[CreatureID::ARCHER];
+	}
+
+	return *creature;
+}
+
+bool BattleProjectileController::stackUsesRayProjectile(const CStack * stack) const
+{
+	return !getShooter(stack).animation.projectileRay.empty();
+}
+
+bool BattleProjectileController::stackUsesMissileProjectile(const CStack * stack) const
+{
+	return !getShooter(stack).animation.projectileImageName.empty();
+}
+
+void BattleProjectileController::initStackProjectile(const CStack * stack)
+{
+	if (!stackUsesMissileProjectile(stack))
+		return;
+
+	const CCreature & creature = getShooter(stack);
+	projectilesCache[creature.animation.projectileImageName] = createProjectileImage(creature.animation.projectileImageName);
+}
+
+std::shared_ptr<CAnimation> BattleProjectileController::createProjectileImage(const AnimationPath & path )
+{
+	std::shared_ptr<CAnimation> projectile = GH.renderHandler().loadAnimation(path);
+	projectile->preload();
+
+	if(projectile->size(1) != 0)
+		logAnim->error("Expected empty group 1 in stack projectile");
+	else
+		projectile->createFlippedGroup(0, 1);
+
+	return projectile;
+}
+
+std::shared_ptr<CAnimation> BattleProjectileController::getProjectileImage(const CStack * stack)
+{
+	const CCreature & creature = getShooter(stack);
+	AnimationPath imageName = creature.animation.projectileImageName;
+
+	if (!projectilesCache.count(imageName))
+		initStackProjectile(stack);
+
+	return projectilesCache[imageName];
+}
+
+void BattleProjectileController::emitStackProjectile(const CStack * stack)
+{
+	int stackID = stack ? stack->unitId() : -1;
+
+	for (auto projectile : projectiles)
+	{
+		if ( !projectile->playing && projectile->shooterID == stackID)
+		{
+			projectile->playing = true;
+			return;
+		}
+	}
+}
+
+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;
+	});
+}
+
+bool BattleProjectileController::hasActiveProjectile(const CStack * stack, bool emittedOnly) const
+{
+	int stackID = stack ? stack->unitId() : -1;
+
+	for(auto const & instance : projectiles)
+	{
+		if(instance->shooterID == stackID && (instance->playing || !emittedOnly))
+		{
+			return true;
+		}
+	}
+	return false;
+}
+
+float BattleProjectileController::computeProjectileFlightTime( Point from, Point dest, double animSpeed)
+{
+	float distanceSquared = (dest.x - from.x) * (dest.x - from.x) + (dest.y - from.y) * (dest.y - from.y);
+	float distance = sqrt(distanceSquared);
+
+	assert(distance > 1.f);
+
+	return animSpeed / std::max( 1.f, distance);
+}
+
+int BattleProjectileController::computeProjectileFrameID( Point from, Point dest, const CStack * stack)
+{
+	const CCreature & creature = getShooter(stack);
+
+	auto & angles = creature.animation.missleFrameAngles;
+	auto animation = getProjectileImage(stack);
+
+	// only frames below maxFrame are usable: anything  higher is either no present or we don't know when it should be used
+	size_t maxFrame = std::min<size_t>(angles.size(), animation->size(0));
+
+	assert(maxFrame > 0);
+	double projectileAngle = -atan2(dest.y - from.y, std::abs(dest.x - from.x));
+
+	// values in angles array indicate position from which this frame was rendered, in degrees.
+	// possible range is 90 ... -90, where projectile for +90 will be used for shooting upwards, +0 for shots towards right and -90 for downwards shots
+	// find frame that has closest angle to one that we need for this shot
+	int bestID = 0;
+	double bestDiff = fabs( angles[0] / 180 * M_PI - projectileAngle );
+
+	for (int i=1; i<maxFrame; i++)
+	{
+		double currentDiff = fabs( angles[i] / 180 * M_PI - projectileAngle );
+		if (currentDiff < bestDiff)
+		{
+			bestID = i;
+			bestDiff = currentDiff;
+		}
+	}
+	return bestID;
+}
+
+void BattleProjectileController::createCatapultProjectile(const CStack * shooter, Point from, Point dest)
+{
+	auto catapultProjectile       = new ProjectileCatapult();
+
+	catapultProjectile->animation = getProjectileImage(shooter);
+	catapultProjectile->progress  = 0;
+	catapultProjectile->speed     = computeProjectileFlightTime(from, dest, AnimationControls::getCatapultSpeed());
+	catapultProjectile->from      = from;
+	catapultProjectile->dest      = dest;
+	catapultProjectile->shooterID = shooter->unitId();
+	catapultProjectile->playing   = false;
+	catapultProjectile->frameProgress = 0.f;
+
+	projectiles.push_back(std::shared_ptr<ProjectileBase>(catapultProjectile));
+}
+
+void BattleProjectileController::createProjectile(const CStack * shooter, Point from, Point dest)
+{
+	const CCreature & shooterInfo = getShooter(shooter);
+
+	std::shared_ptr<ProjectileBase> projectile;
+	if (stackUsesRayProjectile(shooter) && stackUsesMissileProjectile(shooter))
+	{
+		logAnim->error("Mod error: Creature '%s' has both missile and ray projectiles configured. Mod should be fixed. Using ray projectile configuration...", shooterInfo.getNameSingularTranslated());
+	}
+
+	if (stackUsesRayProjectile(shooter))
+	{
+		auto rayProjectile = new ProjectileRay();
+		projectile.reset(rayProjectile);
+
+		rayProjectile->rayConfig = shooterInfo.animation.projectileRay;
+		rayProjectile->speed     = computeProjectileFlightTime(from, dest, AnimationControls::getRayProjectileSpeed());
+	}
+	else if (stackUsesMissileProjectile(shooter))
+	{
+		auto missileProjectile = new ProjectileMissile();
+		projectile.reset(missileProjectile);
+
+		missileProjectile->animation = getProjectileImage(shooter);
+		missileProjectile->reverse   = !owner.stacksController->facingRight(shooter);
+		missileProjectile->frameNum  = computeProjectileFrameID(from, dest, shooter);
+		missileProjectile->speed     = computeProjectileFlightTime(from, dest, AnimationControls::getProjectileSpeed());
+	}
+
+
+	projectile->from      = from;
+	projectile->dest      = dest;
+	projectile->shooterID = shooter->unitId();
+	projectile->progress  = 0;
+	projectile->playing   = false;
+
+	projectiles.push_back(projectile);
+}
+
+void BattleProjectileController::createSpellProjectile(const CStack * shooter, Point from, Point dest, const CSpell * spell)
+{
+	double projectileAngle = std::abs(atan2(dest.x - from.x, dest.y - from.y));
+	AnimationPath animToDisplay = spell->animationInfo.selectProjectile(projectileAngle);
+
+	assert(!animToDisplay.empty());
+
+	if(!animToDisplay.empty())
+	{
+		auto projectile = new ProjectileAnimatedMissile();
+
+		projectile->animation     = createProjectileImage(animToDisplay);
+		projectile->frameProgress = 0;
+		projectile->frameNum      = 0;
+		projectile->reverse       = from.x > dest.x;
+		projectile->from          = from;
+		projectile->dest          = dest;
+		projectile->shooterID     = shooter ? shooter->unitId() : -1;
+		projectile->progress      = 0;
+		projectile->speed         = computeProjectileFlightTime(from, dest, AnimationControls::getProjectileSpeed());
+		projectile->playing       = false;
+
+		projectiles.push_back(std::shared_ptr<ProjectileBase>(projectile));
+	}
+}

+ 125 - 125
client/battle/BattleProjectileController.h

@@ -1,125 +1,125 @@
-/*
- * BattleSiegeController.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/CCreatureHandler.h"
-#include "../../lib/Point.h"
-#include "../../lib/filesystem/ResourcePath.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-class CStack;
-class CSpell;
-
-VCMI_LIB_NAMESPACE_END
-
-class CAnimation;
-class Canvas;
-class BattleInterface;
-
-/// Base class for projectiles
-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
-
-	float progress; // current position of projectile on from->dest line
-	float speed;    // how much progress is gained per second
-	int shooterID;  // ID of shooter stack
-	bool playing;   // if set to true, projectile animation is playing, e.g. flying to target
-};
-
-/// Projectile for most shooters - render pre-selected frame moving in straight line from origin to destination
-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
-	bool reverse;  // if true, projectile will be flipped by vertical axis
-};
-
-/// Projectile for spell - render animation moving in straight line from origin to destination
-struct ProjectileAnimatedMissile : ProjectileMissile
-{
-	void tick(uint32_t msPassed) override;
-	float frameProgress;
-};
-
-/// Projectile for catapult - render spinning projectile moving at parabolic trajectory to its destination
-struct ProjectileCatapult : ProjectileBase
-{
-	void show(Canvas & canvas) override;
-	void tick(uint32_t msPassed) override;
-
-	std::shared_ptr<CAnimation> animation;
-	float frameProgress;
-};
-
-/// Projectile for mages/evil eye - render ray expanding from origin position to destination
-struct ProjectileRay : ProjectileBase
-{
-	void show(Canvas & canvas) override;
-	void tick(uint32_t msPassed) override;
-
-	std::vector<CCreature::CreatureAnimation::RayColor> rayConfig;
-};
-
-/// Class that manages all ongoing projectiles in the game
-/// ... even though in H3 only 1 projectile can be on screen at any point of time
-class BattleProjectileController
-{
-	BattleInterface & owner;
-
-	/// all projectiles loaded during current battle
-	std::map<AnimationPath, std::shared_ptr<CAnimation>> projectilesCache;
-
-	/// projectiles currently flying on battlefield
-	std::vector<std::shared_ptr<ProjectileBase>> projectiles;
-
-	std::shared_ptr<CAnimation> getProjectileImage(const CStack * stack);
-	std::shared_ptr<CAnimation> createProjectileImage(const AnimationPath & path );
-	void initStackProjectile(const CStack * stack);
-
-	bool stackUsesRayProjectile(const CStack * stack) const;
-	bool stackUsesMissileProjectile(const CStack * stack) const;
-
-	void showProjectile(Canvas & canvas, std::shared_ptr<ProjectileBase> projectile);
-
-	const CCreature & getShooter(const CStack * stack) const;
-
-	int computeProjectileFrameID( Point from, Point dest, const CStack * stack);
-	float computeProjectileFlightTime( Point from, Point dest, double speed);
-
-public:
-	BattleProjectileController(BattleInterface & owner);
-
-	/// renders all currently active projectiles
-	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;
-
-	/// starts rendering previously created projectile
-	void emitStackProjectile(const CStack * stack);
-
-	/// creates (but not emits!) projectile and initializes it based on parameters
-	void createProjectile(const CStack * shooter, Point from, Point dest);
-	void createSpellProjectile(const CStack * shooter, Point from, Point dest, const CSpell * spell);
-	void createCatapultProjectile(const CStack * shooter, Point from, Point dest);
-};
+/*
+ * BattleSiegeController.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/CCreatureHandler.h"
+#include "../../lib/Point.h"
+#include "../../lib/filesystem/ResourcePath.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class CStack;
+class CSpell;
+
+VCMI_LIB_NAMESPACE_END
+
+class CAnimation;
+class Canvas;
+class BattleInterface;
+
+/// Base class for projectiles
+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
+
+	float progress; // current position of projectile on from->dest line
+	float speed;    // how much progress is gained per second
+	int shooterID;  // ID of shooter stack
+	bool playing;   // if set to true, projectile animation is playing, e.g. flying to target
+};
+
+/// Projectile for most shooters - render pre-selected frame moving in straight line from origin to destination
+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
+	bool reverse;  // if true, projectile will be flipped by vertical axis
+};
+
+/// Projectile for spell - render animation moving in straight line from origin to destination
+struct ProjectileAnimatedMissile : ProjectileMissile
+{
+	void tick(uint32_t msPassed) override;
+	float frameProgress;
+};
+
+/// Projectile for catapult - render spinning projectile moving at parabolic trajectory to its destination
+struct ProjectileCatapult : ProjectileBase
+{
+	void show(Canvas & canvas) override;
+	void tick(uint32_t msPassed) override;
+
+	std::shared_ptr<CAnimation> animation;
+	float frameProgress;
+};
+
+/// Projectile for mages/evil eye - render ray expanding from origin position to destination
+struct ProjectileRay : ProjectileBase
+{
+	void show(Canvas & canvas) override;
+	void tick(uint32_t msPassed) override;
+
+	std::vector<CCreature::CreatureAnimation::RayColor> rayConfig;
+};
+
+/// Class that manages all ongoing projectiles in the game
+/// ... even though in H3 only 1 projectile can be on screen at any point of time
+class BattleProjectileController
+{
+	BattleInterface & owner;
+
+	/// all projectiles loaded during current battle
+	std::map<AnimationPath, std::shared_ptr<CAnimation>> projectilesCache;
+
+	/// projectiles currently flying on battlefield
+	std::vector<std::shared_ptr<ProjectileBase>> projectiles;
+
+	std::shared_ptr<CAnimation> getProjectileImage(const CStack * stack);
+	std::shared_ptr<CAnimation> createProjectileImage(const AnimationPath & path );
+	void initStackProjectile(const CStack * stack);
+
+	bool stackUsesRayProjectile(const CStack * stack) const;
+	bool stackUsesMissileProjectile(const CStack * stack) const;
+
+	void showProjectile(Canvas & canvas, std::shared_ptr<ProjectileBase> projectile);
+
+	const CCreature & getShooter(const CStack * stack) const;
+
+	int computeProjectileFrameID( Point from, Point dest, const CStack * stack);
+	float computeProjectileFlightTime( Point from, Point dest, double speed);
+
+public:
+	BattleProjectileController(BattleInterface & owner);
+
+	/// renders all currently active projectiles
+	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;
+
+	/// starts rendering previously created projectile
+	void emitStackProjectile(const CStack * stack);
+
+	/// creates (but not emits!) projectile and initializes it based on parameters
+	void createProjectile(const CStack * shooter, Point from, Point dest);
+	void createSpellProjectile(const CStack * shooter, Point from, Point dest, const CSpell * spell);
+	void createCatapultProjectile(const CStack * shooter, Point from, Point dest);
+};

+ 76 - 76
client/battle/BattleRenderer.cpp

@@ -1,76 +1,76 @@
-/*
- * BattleFieldController.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 "BattleRenderer.h"
-
-#include "BattleInterface.h"
-#include "BattleInterfaceClasses.h"
-#include "BattleEffectsController.h"
-#include "BattleWindow.h"
-#include "BattleSiegeController.h"
-#include "BattleStacksController.h"
-#include "BattleObstacleController.h"
-
-void BattleRenderer::collectObjects()
-{
-	owner.effectsController->collectRenderableObjects(*this);
-	owner.obstacleController->collectRenderableObjects(*this);
-	owner.stacksController->collectRenderableObjects(*this);
-	if (owner.siegeController)
-		owner.siegeController->collectRenderableObjects(*this);
-	if (owner.defendingHero)
-		owner.defendingHero->collectRenderableObjects(*this);
-	if (owner.attackingHero)
-		owner.attackingHero->collectRenderableObjects(*this);
-}
-
-void BattleRenderer::sortObjects()
-{
-	auto getRow = [](const RenderableInstance & object) -> int
-	{
-		if (object.tile.isValid())
-			return object.tile.getY();
-
-		if ( object.tile == BattleHex::HEX_BEFORE_ALL )
-			return -1;
-
-		assert( object.tile == BattleHex::HEX_AFTER_ALL || object.tile == BattleHex::INVALID);
-		return GameConstants::BFIELD_HEIGHT;
-	};
-
-	std::stable_sort(objects.begin(), objects.end(), [&](const RenderableInstance & left, const RenderableInstance & right){
-		if ( getRow(left) != getRow(right))
-			return getRow(left) < getRow(right);
-		return left.layer < right.layer;
-	});
-}
-
-void BattleRenderer::renderObjects(BattleRenderer::RendererRef targetCanvas)
-{
-	for (auto const & object : objects)
-		object.functor(targetCanvas);
-}
-
-BattleRenderer::BattleRenderer(BattleInterface & owner):
-	owner(owner)
-{
-}
-
-void BattleRenderer::insert(EBattleFieldLayer layer, BattleHex tile, BattleRenderer::RenderFunctor functor)
-{
-	objects.push_back({functor, layer, tile});
-}
-
-void BattleRenderer::execute(BattleRenderer::RendererRef targetCanvas)
-{
-	collectObjects();
-	sortObjects();
-	renderObjects(targetCanvas);
-}
+/*
+ * BattleFieldController.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 "BattleRenderer.h"
+
+#include "BattleInterface.h"
+#include "BattleInterfaceClasses.h"
+#include "BattleEffectsController.h"
+#include "BattleWindow.h"
+#include "BattleSiegeController.h"
+#include "BattleStacksController.h"
+#include "BattleObstacleController.h"
+
+void BattleRenderer::collectObjects()
+{
+	owner.effectsController->collectRenderableObjects(*this);
+	owner.obstacleController->collectRenderableObjects(*this);
+	owner.stacksController->collectRenderableObjects(*this);
+	if (owner.siegeController)
+		owner.siegeController->collectRenderableObjects(*this);
+	if (owner.defendingHero)
+		owner.defendingHero->collectRenderableObjects(*this);
+	if (owner.attackingHero)
+		owner.attackingHero->collectRenderableObjects(*this);
+}
+
+void BattleRenderer::sortObjects()
+{
+	auto getRow = [](const RenderableInstance & object) -> int
+	{
+		if (object.tile.isValid())
+			return object.tile.getY();
+
+		if ( object.tile == BattleHex::HEX_BEFORE_ALL )
+			return -1;
+
+		assert( object.tile == BattleHex::HEX_AFTER_ALL || object.tile == BattleHex::INVALID);
+		return GameConstants::BFIELD_HEIGHT;
+	};
+
+	std::stable_sort(objects.begin(), objects.end(), [&](const RenderableInstance & left, const RenderableInstance & right){
+		if ( getRow(left) != getRow(right))
+			return getRow(left) < getRow(right);
+		return left.layer < right.layer;
+	});
+}
+
+void BattleRenderer::renderObjects(BattleRenderer::RendererRef targetCanvas)
+{
+	for (auto const & object : objects)
+		object.functor(targetCanvas);
+}
+
+BattleRenderer::BattleRenderer(BattleInterface & owner):
+	owner(owner)
+{
+}
+
+void BattleRenderer::insert(EBattleFieldLayer layer, BattleHex tile, BattleRenderer::RenderFunctor functor)
+{
+	objects.push_back({functor, layer, tile});
+}
+
+void BattleRenderer::execute(BattleRenderer::RendererRef targetCanvas)
+{
+	collectObjects();
+	sortObjects();
+	renderObjects(targetCanvas);
+}

+ 53 - 53
client/battle/BattleRenderer.h

@@ -1,53 +1,53 @@
-/*
- * BattleFieldController.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/battle/BattleHex.h"
-
-class Canvas;
-class BattleInterface;
-
-enum class EBattleFieldLayer {
-					   // confirmed ordering requirements:
-	CORPSES       = 0,
-	WALLS         = 1,
-	HEROES        = 2,
-	STACKS        = 2, // after corpses, obstacles, walls
-	OBSTACLES     = 3, // after stacks
-	STACK_AMOUNTS = 3, // after stacks, obstacles, corpses
-	EFFECTS       = 4, // after obstacles, battlements
-};
-
-class BattleRenderer
-{
-public:
-	using RendererRef = Canvas &;
-	using RenderFunctor = std::function<void(RendererRef)>;
-
-private:
-	BattleInterface & owner;
-
-	struct RenderableInstance
-	{
-		RenderFunctor functor;
-		EBattleFieldLayer layer;
-		BattleHex tile;
-	};
-	std::vector<RenderableInstance> objects;
-
-	void collectObjects();
-	void sortObjects();
-	void renderObjects(RendererRef targetCanvas);
-public:
-	BattleRenderer(BattleInterface & owner);
-
-	void insert(EBattleFieldLayer layer, BattleHex tile, RenderFunctor functor);
-	void execute(RendererRef targetCanvas);
-};
+/*
+ * BattleFieldController.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/battle/BattleHex.h"
+
+class Canvas;
+class BattleInterface;
+
+enum class EBattleFieldLayer {
+					   // confirmed ordering requirements:
+	CORPSES       = 0,
+	WALLS         = 1,
+	HEROES        = 2,
+	STACKS        = 2, // after corpses, obstacles, walls
+	OBSTACLES     = 3, // after stacks
+	STACK_AMOUNTS = 3, // after stacks, obstacles, corpses
+	EFFECTS       = 4, // after obstacles, battlements
+};
+
+class BattleRenderer
+{
+public:
+	using RendererRef = Canvas &;
+	using RenderFunctor = std::function<void(RendererRef)>;
+
+private:
+	BattleInterface & owner;
+
+	struct RenderableInstance
+	{
+		RenderFunctor functor;
+		EBattleFieldLayer layer;
+		BattleHex tile;
+	};
+	std::vector<RenderableInstance> objects;
+
+	void collectObjects();
+	void sortObjects();
+	void renderObjects(RendererRef targetCanvas);
+public:
+	BattleRenderer(BattleInterface & owner);
+
+	void insert(EBattleFieldLayer layer, BattleHex tile, RenderFunctor functor);
+	void execute(RendererRef targetCanvas);
+};

+ 367 - 367
client/battle/BattleSiegeController.cpp

@@ -1,367 +1,367 @@
-/*
- * BattleSiegeController.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 "BattleSiegeController.h"
-
-#include "BattleAnimationClasses.h"
-#include "BattleInterface.h"
-#include "BattleInterfaceClasses.h"
-#include "BattleStacksController.h"
-#include "BattleFieldController.h"
-#include "BattleRenderer.h"
-
-#include "../CMusicHandler.h"
-#include "../CGameInfo.h"
-#include "../CPlayerInterface.h"
-#include "../gui/CGuiHandler.h"
-#include "../render/Canvas.h"
-#include "../render/IImage.h"
-#include "../render/IRenderHandler.h"
-
-#include "../../CCallback.h"
-#include "../../lib/NetPacks.h"
-#include "../../lib/CStack.h"
-#include "../../lib/mapObjects/CGTownInstance.h"
-
-ImagePath BattleSiegeController::getWallPieceImageName(EWallVisual::EWallVisual what, EWallState state) const
-{
-	auto getImageIndex = [&]() -> int
-	{
-		bool isTower = (what == EWallVisual::KEEP || what == EWallVisual::BOTTOM_TOWER || what == EWallVisual::UPPER_TOWER);
-
-		switch (state)
-		{
-		case EWallState::REINFORCED :
-			return 1;
-		case EWallState::INTACT :
-			if (town->hasBuilt(BuildingID::CASTLE))
-				return 2; // reinforced walls were damaged
-			else
-				return 1;
-		case EWallState::DAMAGED :
-			// towers don't have separate image here - INTACT and DAMAGED is 1, DESTROYED is 2
-			if (isTower)
-				return 1;
-			else
-				return 2;
-		case EWallState::DESTROYED :
-			if (isTower)
-				return 2;
-			else
-				return 3;
-		}
-		return 1;
-	};
-
-	const std::string & prefix = town->town->clientInfo.siegePrefix;
-	std::string addit = std::to_string(getImageIndex());
-
-	switch(what)
-	{
-	case EWallVisual::BACKGROUND_WALL:
-		{
-			auto faction = town->town->faction->getIndex();
-
-			if (faction == ETownType::RAMPART || faction == ETownType::NECROPOLIS || faction == ETownType::DUNGEON || faction == ETownType::STRONGHOLD)
-				return ImagePath::builtinTODO(prefix + "TPW1.BMP");
-			else
-				return ImagePath::builtinTODO(prefix + "TPWL.BMP");
-		}
-	case EWallVisual::KEEP:
-		return ImagePath::builtinTODO(prefix + "MAN" + addit + ".BMP");
-	case EWallVisual::BOTTOM_TOWER:
-		return ImagePath::builtinTODO(prefix + "TW1" + addit + ".BMP");
-	case EWallVisual::BOTTOM_WALL:
-		return ImagePath::builtinTODO(prefix + "WA1" + addit + ".BMP");
-	case EWallVisual::WALL_BELLOW_GATE:
-		return ImagePath::builtinTODO(prefix + "WA3" + addit + ".BMP");
-	case EWallVisual::WALL_OVER_GATE:
-		return ImagePath::builtinTODO(prefix + "WA4" + addit + ".BMP");
-	case EWallVisual::UPPER_WALL:
-		return ImagePath::builtinTODO(prefix + "WA6" + addit + ".BMP");
-	case EWallVisual::UPPER_TOWER:
-		return ImagePath::builtinTODO(prefix + "TW2" + addit + ".BMP");
-	case EWallVisual::GATE:
-		return ImagePath::builtinTODO(prefix + "DRW" + addit + ".BMP");
-	case EWallVisual::GATE_ARCH:
-		return ImagePath::builtinTODO(prefix + "ARCH.BMP");
-	case EWallVisual::BOTTOM_STATIC_WALL:
-		return ImagePath::builtinTODO(prefix + "WA2.BMP");
-	case EWallVisual::UPPER_STATIC_WALL:
-		return ImagePath::builtinTODO(prefix + "WA5.BMP");
-	case EWallVisual::MOAT:
-		return ImagePath::builtinTODO(prefix + "MOAT.BMP");
-	case EWallVisual::MOAT_BANK:
-		return ImagePath::builtinTODO(prefix + "MLIP.BMP");
-	case EWallVisual::KEEP_BATTLEMENT:
-		return ImagePath::builtinTODO(prefix + "MANC.BMP");
-	case EWallVisual::BOTTOM_BATTLEMENT:
-		return ImagePath::builtinTODO(prefix + "TW1C.BMP");
-	case EWallVisual::UPPER_BATTLEMENT:
-		return ImagePath::builtinTODO(prefix + "TW2C.BMP");
-	default:
-		return ImagePath();
-	}
-}
-
-void BattleSiegeController::showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what)
-{
-	auto & ci = town->town->clientInfo;
-	auto const & pos = ci.siegePositions[what];
-
-	if ( wallPieceImages[what] && pos.isValid())
-		canvas.draw(wallPieceImages[what], Point(pos.x, pos.y));
-}
-
-ImagePath BattleSiegeController::getBattleBackgroundName() const
-{
-	const std::string & prefix = town->town->clientInfo.siegePrefix;
-	return ImagePath::builtinTODO(prefix + "BACK.BMP");
-}
-
-bool BattleSiegeController::getWallPieceExistance(EWallVisual::EWallVisual what) const
-{
-	//FIXME: use this instead of buildings test?
-	//ui8 siegeLevel = owner.curInt->cb->battleGetSiegeLevel();
-
-	switch (what)
-	{
-	case EWallVisual::MOAT:              return town->hasBuilt(BuildingID::CITADEL) && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT).isValid();
-	case EWallVisual::MOAT_BANK:         return town->hasBuilt(BuildingID::CITADEL) && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT_BANK).isValid();
-	case EWallVisual::KEEP_BATTLEMENT:   return town->hasBuilt(BuildingID::CITADEL) && owner.getBattle()->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED;
-	case EWallVisual::UPPER_BATTLEMENT:  return town->hasBuilt(BuildingID::CASTLE) && owner.getBattle()->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED;
-	case EWallVisual::BOTTOM_BATTLEMENT: return town->hasBuilt(BuildingID::CASTLE) && owner.getBattle()->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED;
-	default:                             return true;
-	}
-}
-
-BattleHex BattleSiegeController::getWallPiecePosition(EWallVisual::EWallVisual what) const
-{
-	static const std::array<BattleHex, 18> wallsPositions = {
-		BattleHex::INVALID,        // BACKGROUND,         // handled separately
-		BattleHex::HEX_BEFORE_ALL, // BACKGROUND_WALL,
-		135,                       // KEEP,
-		BattleHex::HEX_AFTER_ALL,  // BOTTOM_TOWER,
-		182,                       // BOTTOM_WALL,
-		130,                       // WALL_BELLOW_GATE,
-		62,                        // WALL_OVER_GATE,
-		12,                        // UPPER_WALL,
-		BattleHex::HEX_BEFORE_ALL, // UPPER_TOWER,
-		BattleHex::HEX_BEFORE_ALL, // GATE,               // 94
-		112,                       // GATE_ARCH,
-		165,                       // BOTTOM_STATIC_WALL,
-		45,                        // UPPER_STATIC_WALL,
-		BattleHex::INVALID,        // MOAT,               // printed as absolute obstacle
-		BattleHex::INVALID,        // MOAT_BANK,          // printed as absolute obstacle
-		135,                       // KEEP_BATTLEMENT,
-		BattleHex::HEX_AFTER_ALL,  // BOTTOM_BATTLEMENT,
-		BattleHex::HEX_BEFORE_ALL, // UPPER_BATTLEMENT,
-	};
-
-	return wallsPositions[what];
-}
-
-BattleSiegeController::BattleSiegeController(BattleInterface & owner, const CGTownInstance *siegeTown):
-	owner(owner),
-	town(siegeTown)
-{
-	assert(owner.fieldController.get() == nullptr); // must be created after this
-
-	for (int g = 0; g < wallPieceImages.size(); ++g)
-	{
-		if ( g == EWallVisual::GATE ) // gate is initially closed and has no image to display in this state
-			continue;
-
-		if ( !getWallPieceExistance(EWallVisual::EWallVisual(g)) )
-			continue;
-
-		wallPieceImages[g] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::EWallVisual(g), EWallState::REINFORCED));
-	}
-}
-
-const CCreature *BattleSiegeController::getTurretCreature() const
-{
-	return CGI->creh->objects[town->town->clientInfo.siegeShooter];
-}
-
-Point BattleSiegeController::getTurretCreaturePosition( BattleHex position ) const
-{
-	// Turret positions are read out of the config/wall_pos.txt
-	int posID = 0;
-	switch (position)
-	{
-	case BattleHex::CASTLE_CENTRAL_TOWER: // keep creature
-		posID = EWallVisual::CREATURE_KEEP;
-		break;
-	case BattleHex::CASTLE_BOTTOM_TOWER: // bottom creature
-		posID = EWallVisual::CREATURE_BOTTOM_TOWER;
-		break;
-	case BattleHex::CASTLE_UPPER_TOWER: // upper creature
-		posID = EWallVisual::CREATURE_UPPER_TOWER;
-		break;
-	}
-
-	if (posID != 0)
-	{
-		return {
-			town->town->clientInfo.siegePositions[posID].x,
-			town->town->clientInfo.siegePositions[posID].y
-		};
-	}
-
-	assert(0);
-	return Point(0,0);
-}
-
-void BattleSiegeController::gateStateChanged(const EGateState state)
-{
-	auto oldState = owner.getBattle()->battleGetGateState();
-	bool playSound = false;
-	auto stateId = EWallState::NONE;
-	switch(state)
-	{
-	case EGateState::CLOSED:
-		if (oldState != EGateState::BLOCKED)
-			playSound = true;
-		break;
-	case EGateState::BLOCKED:
-		if (oldState != EGateState::CLOSED)
-			playSound = true;
-		break;
-	case EGateState::OPENED:
-		playSound = true;
-		stateId = EWallState::DAMAGED;
-		break;
-	case EGateState::DESTROYED:
-		stateId = EWallState::DESTROYED;
-		break;
-	}
-
-	if (oldState != EGateState::NONE && oldState != EGateState::CLOSED && oldState != EGateState::BLOCKED)
-		wallPieceImages[EWallVisual::GATE] = nullptr;
-
-	if (stateId != EWallState::NONE)
-		wallPieceImages[EWallVisual::GATE] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::GATE,  stateId));
-
-	if (playSound)
-		CCS->soundh->playSound(soundBase::DRAWBRG);
-}
-
-void BattleSiegeController::showAbsoluteObstacles(Canvas & canvas)
-{
-	if (getWallPieceExistance(EWallVisual::MOAT))
-		showWallPiece(canvas, EWallVisual::MOAT);
-
-	if (getWallPieceExistance(EWallVisual::MOAT_BANK))
-		showWallPiece(canvas, EWallVisual::MOAT_BANK);
-}
-
-BattleHex BattleSiegeController::getTurretBattleHex(EWallVisual::EWallVisual wallPiece) const
-{
-	switch(wallPiece)
-	{
-	case EWallVisual::KEEP_BATTLEMENT:   return BattleHex::CASTLE_CENTRAL_TOWER;
-	case EWallVisual::BOTTOM_BATTLEMENT: return BattleHex::CASTLE_BOTTOM_TOWER;
-	case EWallVisual::UPPER_BATTLEMENT:  return BattleHex::CASTLE_UPPER_TOWER;
-	}
-	assert(0);
-	return BattleHex::INVALID;
-}
-
-const CStack * BattleSiegeController::getTurretStack(EWallVisual::EWallVisual wallPiece) const
-{
-	for (auto & stack : owner.getBattle()->battleGetAllStacks(true))
-	{
-		if ( stack->initialPosition == getTurretBattleHex(wallPiece))
-			return stack;
-	}
-	assert(0);
-	return nullptr;
-}
-
-void BattleSiegeController::collectRenderableObjects(BattleRenderer & renderer)
-{
-	for (int i = EWallVisual::WALL_FIRST; i <= EWallVisual::WALL_LAST; ++i)
-	{
-		auto wallPiece = EWallVisual::EWallVisual(i);
-
-		if ( !getWallPieceExistance(wallPiece))
-			continue;
-
-		if ( getWallPiecePosition(wallPiece) == BattleHex::INVALID)
-			continue;
-
-		if (wallPiece == EWallVisual::KEEP_BATTLEMENT ||
-			wallPiece == EWallVisual::BOTTOM_BATTLEMENT ||
-			wallPiece == EWallVisual::UPPER_BATTLEMENT)
-		{
-			renderer.insert( EBattleFieldLayer::STACKS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){
-				owner.stacksController->showStack(canvas, getTurretStack(wallPiece));
-			});
-			renderer.insert( EBattleFieldLayer::OBSTACLES, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){
-				showWallPiece(canvas, wallPiece);
-			});
-		}
-		renderer.insert( EBattleFieldLayer::WALLS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){
-			showWallPiece(canvas, wallPiece);
-		});
-	}
-}
-
-bool BattleSiegeController::isAttackableByCatapult(BattleHex hex) const
-{
-	if (owner.tacticsMode)
-		return false;
-
-	auto wallPart = owner.getBattle()->battleHexToWallPart(hex);
-	return owner.getBattle()->isWallPartAttackable(wallPart);
-}
-
-void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca)
-{
-	if (ca.attacker != -1)
-	{
-		const CStack *stack = owner.getBattle()->battleGetStackByID(ca.attacker);
-		for (auto attackInfo : ca.attackedParts)
-		{
-			owner.stacksController->addNewAnim(new CatapultAnimation(owner, stack, attackInfo.destinationTile, nullptr, attackInfo.damageDealt));
-		}
-	}
-	else
-	{
-		std::vector<Point> positions;
-
-		//no attacker stack, assume spell-related (earthquake) - only hit animation
-		for (auto attackInfo : ca.attackedParts)
-			positions.push_back(owner.stacksController->getStackPositionAtHex(attackInfo.destinationTile, nullptr) + Point(99, 120));
-
-		CCS->soundh->playSound( AudioPath::builtin("WALLHIT") );
-		owner.stacksController->addNewAnim(new EffectAnimation(owner, AnimationPath::builtin("SGEXPL.DEF"), positions));
-	}
-
-	owner.waitForAnimations();
-
-	for (auto attackInfo : ca.attackedParts)
-	{
-		int wallId = static_cast<int>(attackInfo.attackedPart) + EWallVisual::DESTRUCTIBLE_FIRST;
-		//gate state changing handled separately
-		if (wallId == EWallVisual::GATE)
-			continue;
-
-		auto wallState = EWallState(owner.getBattle()->battleGetWallState(attackInfo.attackedPart));
-
-		wallPieceImages[wallId] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::EWallVisual(wallId), wallState));
-	}
-}
-
-const CGTownInstance *BattleSiegeController::getSiegedTown() const
-{
-	return town;
-}
+/*
+ * BattleSiegeController.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 "BattleSiegeController.h"
+
+#include "BattleAnimationClasses.h"
+#include "BattleInterface.h"
+#include "BattleInterfaceClasses.h"
+#include "BattleStacksController.h"
+#include "BattleFieldController.h"
+#include "BattleRenderer.h"
+
+#include "../CMusicHandler.h"
+#include "../CGameInfo.h"
+#include "../CPlayerInterface.h"
+#include "../gui/CGuiHandler.h"
+#include "../render/Canvas.h"
+#include "../render/IImage.h"
+#include "../render/IRenderHandler.h"
+
+#include "../../CCallback.h"
+#include "../../lib/NetPacks.h"
+#include "../../lib/CStack.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
+
+ImagePath BattleSiegeController::getWallPieceImageName(EWallVisual::EWallVisual what, EWallState state) const
+{
+	auto getImageIndex = [&]() -> int
+	{
+		bool isTower = (what == EWallVisual::KEEP || what == EWallVisual::BOTTOM_TOWER || what == EWallVisual::UPPER_TOWER);
+
+		switch (state)
+		{
+		case EWallState::REINFORCED :
+			return 1;
+		case EWallState::INTACT :
+			if (town->hasBuilt(BuildingID::CASTLE))
+				return 2; // reinforced walls were damaged
+			else
+				return 1;
+		case EWallState::DAMAGED :
+			// towers don't have separate image here - INTACT and DAMAGED is 1, DESTROYED is 2
+			if (isTower)
+				return 1;
+			else
+				return 2;
+		case EWallState::DESTROYED :
+			if (isTower)
+				return 2;
+			else
+				return 3;
+		}
+		return 1;
+	};
+
+	const std::string & prefix = town->town->clientInfo.siegePrefix;
+	std::string addit = std::to_string(getImageIndex());
+
+	switch(what)
+	{
+	case EWallVisual::BACKGROUND_WALL:
+		{
+			auto faction = town->town->faction->getIndex();
+
+			if (faction == ETownType::RAMPART || faction == ETownType::NECROPOLIS || faction == ETownType::DUNGEON || faction == ETownType::STRONGHOLD)
+				return ImagePath::builtinTODO(prefix + "TPW1.BMP");
+			else
+				return ImagePath::builtinTODO(prefix + "TPWL.BMP");
+		}
+	case EWallVisual::KEEP:
+		return ImagePath::builtinTODO(prefix + "MAN" + addit + ".BMP");
+	case EWallVisual::BOTTOM_TOWER:
+		return ImagePath::builtinTODO(prefix + "TW1" + addit + ".BMP");
+	case EWallVisual::BOTTOM_WALL:
+		return ImagePath::builtinTODO(prefix + "WA1" + addit + ".BMP");
+	case EWallVisual::WALL_BELLOW_GATE:
+		return ImagePath::builtinTODO(prefix + "WA3" + addit + ".BMP");
+	case EWallVisual::WALL_OVER_GATE:
+		return ImagePath::builtinTODO(prefix + "WA4" + addit + ".BMP");
+	case EWallVisual::UPPER_WALL:
+		return ImagePath::builtinTODO(prefix + "WA6" + addit + ".BMP");
+	case EWallVisual::UPPER_TOWER:
+		return ImagePath::builtinTODO(prefix + "TW2" + addit + ".BMP");
+	case EWallVisual::GATE:
+		return ImagePath::builtinTODO(prefix + "DRW" + addit + ".BMP");
+	case EWallVisual::GATE_ARCH:
+		return ImagePath::builtinTODO(prefix + "ARCH.BMP");
+	case EWallVisual::BOTTOM_STATIC_WALL:
+		return ImagePath::builtinTODO(prefix + "WA2.BMP");
+	case EWallVisual::UPPER_STATIC_WALL:
+		return ImagePath::builtinTODO(prefix + "WA5.BMP");
+	case EWallVisual::MOAT:
+		return ImagePath::builtinTODO(prefix + "MOAT.BMP");
+	case EWallVisual::MOAT_BANK:
+		return ImagePath::builtinTODO(prefix + "MLIP.BMP");
+	case EWallVisual::KEEP_BATTLEMENT:
+		return ImagePath::builtinTODO(prefix + "MANC.BMP");
+	case EWallVisual::BOTTOM_BATTLEMENT:
+		return ImagePath::builtinTODO(prefix + "TW1C.BMP");
+	case EWallVisual::UPPER_BATTLEMENT:
+		return ImagePath::builtinTODO(prefix + "TW2C.BMP");
+	default:
+		return ImagePath();
+	}
+}
+
+void BattleSiegeController::showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what)
+{
+	auto & ci = town->town->clientInfo;
+	auto const & pos = ci.siegePositions[what];
+
+	if ( wallPieceImages[what] && pos.isValid())
+		canvas.draw(wallPieceImages[what], Point(pos.x, pos.y));
+}
+
+ImagePath BattleSiegeController::getBattleBackgroundName() const
+{
+	const std::string & prefix = town->town->clientInfo.siegePrefix;
+	return ImagePath::builtinTODO(prefix + "BACK.BMP");
+}
+
+bool BattleSiegeController::getWallPieceExistance(EWallVisual::EWallVisual what) const
+{
+	//FIXME: use this instead of buildings test?
+	//ui8 siegeLevel = owner.curInt->cb->battleGetSiegeLevel();
+
+	switch (what)
+	{
+	case EWallVisual::MOAT:              return town->hasBuilt(BuildingID::CITADEL) && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT).isValid();
+	case EWallVisual::MOAT_BANK:         return town->hasBuilt(BuildingID::CITADEL) && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT_BANK).isValid();
+	case EWallVisual::KEEP_BATTLEMENT:   return town->hasBuilt(BuildingID::CITADEL) && owner.getBattle()->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED;
+	case EWallVisual::UPPER_BATTLEMENT:  return town->hasBuilt(BuildingID::CASTLE) && owner.getBattle()->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED;
+	case EWallVisual::BOTTOM_BATTLEMENT: return town->hasBuilt(BuildingID::CASTLE) && owner.getBattle()->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED;
+	default:                             return true;
+	}
+}
+
+BattleHex BattleSiegeController::getWallPiecePosition(EWallVisual::EWallVisual what) const
+{
+	static const std::array<BattleHex, 18> wallsPositions = {
+		BattleHex::INVALID,        // BACKGROUND,         // handled separately
+		BattleHex::HEX_BEFORE_ALL, // BACKGROUND_WALL,
+		135,                       // KEEP,
+		BattleHex::HEX_AFTER_ALL,  // BOTTOM_TOWER,
+		182,                       // BOTTOM_WALL,
+		130,                       // WALL_BELLOW_GATE,
+		62,                        // WALL_OVER_GATE,
+		12,                        // UPPER_WALL,
+		BattleHex::HEX_BEFORE_ALL, // UPPER_TOWER,
+		BattleHex::HEX_BEFORE_ALL, // GATE,               // 94
+		112,                       // GATE_ARCH,
+		165,                       // BOTTOM_STATIC_WALL,
+		45,                        // UPPER_STATIC_WALL,
+		BattleHex::INVALID,        // MOAT,               // printed as absolute obstacle
+		BattleHex::INVALID,        // MOAT_BANK,          // printed as absolute obstacle
+		135,                       // KEEP_BATTLEMENT,
+		BattleHex::HEX_AFTER_ALL,  // BOTTOM_BATTLEMENT,
+		BattleHex::HEX_BEFORE_ALL, // UPPER_BATTLEMENT,
+	};
+
+	return wallsPositions[what];
+}
+
+BattleSiegeController::BattleSiegeController(BattleInterface & owner, const CGTownInstance *siegeTown):
+	owner(owner),
+	town(siegeTown)
+{
+	assert(owner.fieldController.get() == nullptr); // must be created after this
+
+	for (int g = 0; g < wallPieceImages.size(); ++g)
+	{
+		if ( g == EWallVisual::GATE ) // gate is initially closed and has no image to display in this state
+			continue;
+
+		if ( !getWallPieceExistance(EWallVisual::EWallVisual(g)) )
+			continue;
+
+		wallPieceImages[g] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::EWallVisual(g), EWallState::REINFORCED));
+	}
+}
+
+const CCreature *BattleSiegeController::getTurretCreature() const
+{
+	return CGI->creh->objects[town->town->clientInfo.siegeShooter];
+}
+
+Point BattleSiegeController::getTurretCreaturePosition( BattleHex position ) const
+{
+	// Turret positions are read out of the config/wall_pos.txt
+	int posID = 0;
+	switch (position)
+	{
+	case BattleHex::CASTLE_CENTRAL_TOWER: // keep creature
+		posID = EWallVisual::CREATURE_KEEP;
+		break;
+	case BattleHex::CASTLE_BOTTOM_TOWER: // bottom creature
+		posID = EWallVisual::CREATURE_BOTTOM_TOWER;
+		break;
+	case BattleHex::CASTLE_UPPER_TOWER: // upper creature
+		posID = EWallVisual::CREATURE_UPPER_TOWER;
+		break;
+	}
+
+	if (posID != 0)
+	{
+		return {
+			town->town->clientInfo.siegePositions[posID].x,
+			town->town->clientInfo.siegePositions[posID].y
+		};
+	}
+
+	assert(0);
+	return Point(0,0);
+}
+
+void BattleSiegeController::gateStateChanged(const EGateState state)
+{
+	auto oldState = owner.getBattle()->battleGetGateState();
+	bool playSound = false;
+	auto stateId = EWallState::NONE;
+	switch(state)
+	{
+	case EGateState::CLOSED:
+		if (oldState != EGateState::BLOCKED)
+			playSound = true;
+		break;
+	case EGateState::BLOCKED:
+		if (oldState != EGateState::CLOSED)
+			playSound = true;
+		break;
+	case EGateState::OPENED:
+		playSound = true;
+		stateId = EWallState::DAMAGED;
+		break;
+	case EGateState::DESTROYED:
+		stateId = EWallState::DESTROYED;
+		break;
+	}
+
+	if (oldState != EGateState::NONE && oldState != EGateState::CLOSED && oldState != EGateState::BLOCKED)
+		wallPieceImages[EWallVisual::GATE] = nullptr;
+
+	if (stateId != EWallState::NONE)
+		wallPieceImages[EWallVisual::GATE] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::GATE,  stateId));
+
+	if (playSound)
+		CCS->soundh->playSound(soundBase::DRAWBRG);
+}
+
+void BattleSiegeController::showAbsoluteObstacles(Canvas & canvas)
+{
+	if (getWallPieceExistance(EWallVisual::MOAT))
+		showWallPiece(canvas, EWallVisual::MOAT);
+
+	if (getWallPieceExistance(EWallVisual::MOAT_BANK))
+		showWallPiece(canvas, EWallVisual::MOAT_BANK);
+}
+
+BattleHex BattleSiegeController::getTurretBattleHex(EWallVisual::EWallVisual wallPiece) const
+{
+	switch(wallPiece)
+	{
+	case EWallVisual::KEEP_BATTLEMENT:   return BattleHex::CASTLE_CENTRAL_TOWER;
+	case EWallVisual::BOTTOM_BATTLEMENT: return BattleHex::CASTLE_BOTTOM_TOWER;
+	case EWallVisual::UPPER_BATTLEMENT:  return BattleHex::CASTLE_UPPER_TOWER;
+	}
+	assert(0);
+	return BattleHex::INVALID;
+}
+
+const CStack * BattleSiegeController::getTurretStack(EWallVisual::EWallVisual wallPiece) const
+{
+	for (auto & stack : owner.getBattle()->battleGetAllStacks(true))
+	{
+		if ( stack->initialPosition == getTurretBattleHex(wallPiece))
+			return stack;
+	}
+	assert(0);
+	return nullptr;
+}
+
+void BattleSiegeController::collectRenderableObjects(BattleRenderer & renderer)
+{
+	for (int i = EWallVisual::WALL_FIRST; i <= EWallVisual::WALL_LAST; ++i)
+	{
+		auto wallPiece = EWallVisual::EWallVisual(i);
+
+		if ( !getWallPieceExistance(wallPiece))
+			continue;
+
+		if ( getWallPiecePosition(wallPiece) == BattleHex::INVALID)
+			continue;
+
+		if (wallPiece == EWallVisual::KEEP_BATTLEMENT ||
+			wallPiece == EWallVisual::BOTTOM_BATTLEMENT ||
+			wallPiece == EWallVisual::UPPER_BATTLEMENT)
+		{
+			renderer.insert( EBattleFieldLayer::STACKS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){
+				owner.stacksController->showStack(canvas, getTurretStack(wallPiece));
+			});
+			renderer.insert( EBattleFieldLayer::OBSTACLES, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){
+				showWallPiece(canvas, wallPiece);
+			});
+		}
+		renderer.insert( EBattleFieldLayer::WALLS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){
+			showWallPiece(canvas, wallPiece);
+		});
+	}
+}
+
+bool BattleSiegeController::isAttackableByCatapult(BattleHex hex) const
+{
+	if (owner.tacticsMode)
+		return false;
+
+	auto wallPart = owner.getBattle()->battleHexToWallPart(hex);
+	return owner.getBattle()->isWallPartAttackable(wallPart);
+}
+
+void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca)
+{
+	if (ca.attacker != -1)
+	{
+		const CStack *stack = owner.getBattle()->battleGetStackByID(ca.attacker);
+		for (auto attackInfo : ca.attackedParts)
+		{
+			owner.stacksController->addNewAnim(new CatapultAnimation(owner, stack, attackInfo.destinationTile, nullptr, attackInfo.damageDealt));
+		}
+	}
+	else
+	{
+		std::vector<Point> positions;
+
+		//no attacker stack, assume spell-related (earthquake) - only hit animation
+		for (auto attackInfo : ca.attackedParts)
+			positions.push_back(owner.stacksController->getStackPositionAtHex(attackInfo.destinationTile, nullptr) + Point(99, 120));
+
+		CCS->soundh->playSound( AudioPath::builtin("WALLHIT") );
+		owner.stacksController->addNewAnim(new EffectAnimation(owner, AnimationPath::builtin("SGEXPL.DEF"), positions));
+	}
+
+	owner.waitForAnimations();
+
+	for (auto attackInfo : ca.attackedParts)
+	{
+		int wallId = static_cast<int>(attackInfo.attackedPart) + EWallVisual::DESTRUCTIBLE_FIRST;
+		//gate state changing handled separately
+		if (wallId == EWallVisual::GATE)
+			continue;
+
+		auto wallState = EWallState(owner.getBattle()->battleGetWallState(attackInfo.attackedPart));
+
+		wallPieceImages[wallId] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::EWallVisual(wallId), wallState));
+	}
+}
+
+const CGTownInstance *BattleSiegeController::getSiegedTown() const
+{
+	return town;
+}

+ 111 - 111
client/battle/BattleSiegeController.h

@@ -1,111 +1,111 @@
-/*
- * BattleObstacleController.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/GameConstants.h"
-#include "../../lib/battle/BattleHex.h"
-#include "../../lib/filesystem/ResourcePath.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-struct CatapultAttack;
-class CCreature;
-class CStack;
-class CGTownInstance;
-class Point;
-
-VCMI_LIB_NAMESPACE_END
-
-class Canvas;
-class BattleInterface;
-class BattleRenderer;
-class IImage;
-
-namespace EWallVisual
-{
-	enum EWallVisual
-	{
-		BACKGROUND,
-		BACKGROUND_WALL,
-
-		KEEP,
-		BOTTOM_TOWER,
-		BOTTOM_WALL,
-		WALL_BELLOW_GATE,
-		WALL_OVER_GATE,
-		UPPER_WALL,
-		UPPER_TOWER,
-		GATE,
-
-		GATE_ARCH,
-		BOTTOM_STATIC_WALL,
-		UPPER_STATIC_WALL,
-		MOAT,
-		MOAT_BANK,
-		KEEP_BATTLEMENT,
-		BOTTOM_BATTLEMENT,
-		UPPER_BATTLEMENT,
-
-		CREATURE_KEEP,
-		CREATURE_BOTTOM_TOWER,
-		CREATURE_UPPER_TOWER,
-
-		WALL_FIRST = BACKGROUND_WALL,
-		WALL_LAST  = UPPER_BATTLEMENT,
-
-		// these entries are mapped to EWallPart enum
-		DESTRUCTIBLE_FIRST = KEEP,
-		DESTRUCTIBLE_LAST = GATE,
-	};
-}
-
-class BattleSiegeController
-{
-	BattleInterface & owner;
-
-	/// besieged town
-	const CGTownInstance *town;
-
-	/// sections of castle walls, in their currently visible state
-	std::array<std::shared_ptr<IImage>, EWallVisual::WALL_LAST + 1> wallPieceImages;
-
-	/// return URI for image for a wall piece
-	ImagePath getWallPieceImageName(EWallVisual::EWallVisual what, EWallState state) const;
-
-	/// returns BattleHex to which chosen wall piece is bound
-	BattleHex getWallPiecePosition(EWallVisual::EWallVisual what) const;
-
-	/// returns true if chosen wall piece should be present in current battle
-	bool getWallPieceExistance(EWallVisual::EWallVisual what) const;
-
-	void showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what);
-
-	BattleHex getTurretBattleHex(EWallVisual::EWallVisual wallPiece) const;
-	const CStack * getTurretStack(EWallVisual::EWallVisual wallPiece) const;
-
-public:
-	BattleSiegeController(BattleInterface & owner, const CGTownInstance *siegeTown);
-
-	/// call-ins from server
-	void gateStateChanged(const EGateState state);
-	void stackIsCatapulting(const CatapultAttack & ca);
-
-	/// call-ins from other battle controllers
-	void showAbsoluteObstacles(Canvas & canvas);
-	void collectRenderableObjects(BattleRenderer & renderer);
-
-	/// queries from other battle controllers
-	bool isAttackableByCatapult(BattleHex hex) const;
-	ImagePath getBattleBackgroundName() const;
-	const CCreature *getTurretCreature() const;
-	Point getTurretCreaturePosition( BattleHex position ) const;
-
-	const CGTownInstance *getSiegedTown() const;
-};
+/*
+ * BattleObstacleController.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/GameConstants.h"
+#include "../../lib/battle/BattleHex.h"
+#include "../../lib/filesystem/ResourcePath.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+struct CatapultAttack;
+class CCreature;
+class CStack;
+class CGTownInstance;
+class Point;
+
+VCMI_LIB_NAMESPACE_END
+
+class Canvas;
+class BattleInterface;
+class BattleRenderer;
+class IImage;
+
+namespace EWallVisual
+{
+	enum EWallVisual
+	{
+		BACKGROUND,
+		BACKGROUND_WALL,
+
+		KEEP,
+		BOTTOM_TOWER,
+		BOTTOM_WALL,
+		WALL_BELLOW_GATE,
+		WALL_OVER_GATE,
+		UPPER_WALL,
+		UPPER_TOWER,
+		GATE,
+
+		GATE_ARCH,
+		BOTTOM_STATIC_WALL,
+		UPPER_STATIC_WALL,
+		MOAT,
+		MOAT_BANK,
+		KEEP_BATTLEMENT,
+		BOTTOM_BATTLEMENT,
+		UPPER_BATTLEMENT,
+
+		CREATURE_KEEP,
+		CREATURE_BOTTOM_TOWER,
+		CREATURE_UPPER_TOWER,
+
+		WALL_FIRST = BACKGROUND_WALL,
+		WALL_LAST  = UPPER_BATTLEMENT,
+
+		// these entries are mapped to EWallPart enum
+		DESTRUCTIBLE_FIRST = KEEP,
+		DESTRUCTIBLE_LAST = GATE,
+	};
+}
+
+class BattleSiegeController
+{
+	BattleInterface & owner;
+
+	/// besieged town
+	const CGTownInstance *town;
+
+	/// sections of castle walls, in their currently visible state
+	std::array<std::shared_ptr<IImage>, EWallVisual::WALL_LAST + 1> wallPieceImages;
+
+	/// return URI for image for a wall piece
+	ImagePath getWallPieceImageName(EWallVisual::EWallVisual what, EWallState state) const;
+
+	/// returns BattleHex to which chosen wall piece is bound
+	BattleHex getWallPiecePosition(EWallVisual::EWallVisual what) const;
+
+	/// returns true if chosen wall piece should be present in current battle
+	bool getWallPieceExistance(EWallVisual::EWallVisual what) const;
+
+	void showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what);
+
+	BattleHex getTurretBattleHex(EWallVisual::EWallVisual wallPiece) const;
+	const CStack * getTurretStack(EWallVisual::EWallVisual wallPiece) const;
+
+public:
+	BattleSiegeController(BattleInterface & owner, const CGTownInstance *siegeTown);
+
+	/// call-ins from server
+	void gateStateChanged(const EGateState state);
+	void stackIsCatapulting(const CatapultAttack & ca);
+
+	/// call-ins from other battle controllers
+	void showAbsoluteObstacles(Canvas & canvas);
+	void collectRenderableObjects(BattleRenderer & renderer);
+
+	/// queries from other battle controllers
+	bool isAttackableByCatapult(BattleHex hex) const;
+	ImagePath getBattleBackgroundName() const;
+	const CCreature *getTurretCreature() const;
+	Point getTurretCreaturePosition( BattleHex position ) const;
+
+	const CGTownInstance *getSiegedTown() const;
+};

+ 894 - 894
client/battle/BattleStacksController.cpp

@@ -1,894 +1,894 @@
-/*
- * BattleStacksController.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 "BattleStacksController.h"
-
-#include "BattleSiegeController.h"
-#include "BattleInterfaceClasses.h"
-#include "BattleInterface.h"
-#include "BattleActionsController.h"
-#include "BattleAnimationClasses.h"
-#include "BattleFieldController.h"
-#include "BattleEffectsController.h"
-#include "BattleProjectileController.h"
-#include "BattleWindow.h"
-#include "BattleRenderer.h"
-#include "CreatureAnimation.h"
-
-#include "../CPlayerInterface.h"
-#include "../CMusicHandler.h"
-#include "../CGameInfo.h"
-#include "../gui/CGuiHandler.h"
-#include "../render/Colors.h"
-#include "../render/Canvas.h"
-#include "../render/IRenderHandler.h"
-
-#include "../../CCallback.h"
-#include "../../lib/spells/ISpellMechanics.h"
-#include "../../lib/battle/BattleAction.h"
-#include "../../lib/battle/BattleHex.h"
-#include "../../lib/CStack.h"
-#include "../../lib/CondSh.h"
-#include "../../lib/TextOperations.h"
-
-static void onAnimationFinished(const CStack *stack, std::weak_ptr<CreatureAnimation> anim)
-{
-	std::shared_ptr<CreatureAnimation> animation = anim.lock();
-	if(!animation)
-		return;
-
-	if (!stack->isFrozen() && animation->getType() == ECreatureAnimType::FROZEN)
-		animation->setType(ECreatureAnimType::HOLDING);
-
-	if (animation->isIdle())
-	{
-		const CCreature *creature = stack->unitType();
-
-		if (stack->isFrozen())
-			animation->setType(ECreatureAnimType::FROZEN);
-		else
-		if (animation->framesInGroup(ECreatureAnimType::MOUSEON) > 0)
-		{
-			if (CRandomGenerator::getDefault().nextDouble(99.0) < creature->animation.timeBetweenFidgets *10)
-				animation->playOnce(ECreatureAnimType::MOUSEON);
-			else
-				animation->setType(ECreatureAnimType::HOLDING);
-		}
-		else
-		{
-			animation->setType(ECreatureAnimType::HOLDING);
-		}
-	}
-	// always reset callback
-	animation->onAnimationReset += std::bind(&onAnimationFinished, stack, anim);
-}
-
-BattleStacksController::BattleStacksController(BattleInterface & owner):
-	owner(owner),
-	activeStack(nullptr),
-	stackToActivate(nullptr),
-	animIDhelper(0)
-{
-	//preparing graphics for displaying amounts of creatures
-	amountNormal     = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY);
-	amountPositive   = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY);
-	amountNegative   = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY);
-	amountEffNeutral = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY);
-
-	static const auto shifterNormal   = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.6f, 0.2f, 1.0f );
-	static const auto shifterPositive = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.2f, 1.0f, 0.2f );
-	static const auto shifterNegative = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 0.2f, 0.2f );
-	static const auto shifterNeutral  = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 1.0f, 0.2f );
-
-	// do not change border color
-	static const int32_t ignoredMask = 1 << 26;
-
-	amountNormal->adjustPalette(shifterNormal, ignoredMask);
-	amountPositive->adjustPalette(shifterPositive, ignoredMask);
-	amountNegative->adjustPalette(shifterNegative, ignoredMask);
-	amountEffNeutral->adjustPalette(shifterNeutral, ignoredMask);
-
-	std::vector<const CStack*> stacks = owner.getBattle()->battleGetAllStacks(true);
-	for(const CStack * s : stacks)
-	{
-		stackAdded(s, true);
-	}
-}
-
-BattleHex BattleStacksController::getStackCurrentPosition(const CStack * stack) const
-{
-	if ( !stackAnimation.at(stack->unitId())->isMoving())
-		return stack->getPosition();
-
-	if (stack->hasBonusOfType(BonusType::FLYING) && stackAnimation.at(stack->unitId())->getType() == ECreatureAnimType::MOVING )
-		return BattleHex::HEX_AFTER_ALL;
-
-	for (auto & anim : currentAnimations)
-	{
-		// certainly ugly workaround but fixes quite annoying bug
-		// stack position will be updated only *after* movement is finished
-		// before this - stack is always at its initial position. Thus we need to find
-		// its current position. Which can be found only in this class
-		if (StackMoveAnimation *move = dynamic_cast<StackMoveAnimation*>(anim))
-		{
-			if (move->stack == stack)
-				return std::max(move->prevHex, move->nextHex);
-		}
-	}
-	return stack->getPosition();
-}
-
-void BattleStacksController::collectRenderableObjects(BattleRenderer & renderer)
-{
-	auto stacks = owner.getBattle()->battleGetAllStacks(false);
-
-	for (auto stack : stacks)
-	{
-		if (stackAnimation.find(stack->unitId()) == stackAnimation.end()) //e.g. for summoned but not yet handled stacks
-			continue;
-
-		//FIXME: hack to ignore ghost stacks
-		if ((stackAnimation[stack->unitId()]->getType() == ECreatureAnimType::DEAD || stackAnimation[stack->unitId()]->getType() == ECreatureAnimType::HOLDING) && stack->isGhost())
-			continue;
-
-		auto layer = stackAnimation[stack->unitId()]->isDead() ? EBattleFieldLayer::CORPSES : EBattleFieldLayer::STACKS;
-		auto location = getStackCurrentPosition(stack);
-
-		renderer.insert(layer, location, [this, stack]( BattleRenderer::RendererRef renderer ){
-			showStack(renderer, stack);
-		});
-
-		if (stackNeedsAmountBox(stack))
-		{
-			renderer.insert(EBattleFieldLayer::STACK_AMOUNTS, location, [this, stack]( BattleRenderer::RendererRef renderer ){
-				showStackAmountBox(renderer, stack);
-			});
-		}
-	}
-}
-
-void BattleStacksController::stackReset(const CStack * stack)
-{
-	auto iter = stackAnimation.find(stack->unitId());
-
-	if(iter == stackAnimation.end())
-	{
-		logGlobal->error("Unit %d have no animation", stack->unitId());
-		return;
-	}
-
-	auto animation = iter->second;
-
-	if(stack->alive() && animation->isDeadOrDying())
-	{
-		owner.addToAnimationStage(EAnimationEvents::HIT, [=]()
-		{
-			addNewAnim(new ResurrectionAnimation(owner, stack));
-		});
-	}
-}
-
-void BattleStacksController::stackAdded(const CStack * stack, bool instant)
-{
-	// Tower shooters have only their upper half visible
-	static const int turretCreatureAnimationHeight = 232;
-
-	stackFacingRight[stack->unitId()] = stack->unitSide() == BattleSide::ATTACKER; // must be set before getting stack position
-
-	Point coords = getStackPositionAtHex(stack->getPosition(), stack);
-
-	if(stack->initialPosition < 0) //turret
-	{
-		assert(owner.siegeController);
-
-		const CCreature *turretCreature = owner.siegeController->getTurretCreature();
-
-		stackAnimation[stack->unitId()] = AnimationControls::getAnimation(turretCreature);
-		stackAnimation[stack->unitId()]->pos.h = turretCreatureAnimationHeight;
-		stackAnimation[stack->unitId()]->pos.w = stackAnimation[stack->unitId()]->getWidth();
-
-		// FIXME: workaround for visible animation of Medusa tails (animation disabled in H3)
-		if (turretCreature->getId() == CreatureID::MEDUSA )
-			stackAnimation[stack->unitId()]->pos.w = 250;
-
-		coords = owner.siegeController->getTurretCreaturePosition(stack->initialPosition);
-	}
-	else
-	{
-		stackAnimation[stack->unitId()] = AnimationControls::getAnimation(stack->unitType());
-		stackAnimation[stack->unitId()]->onAnimationReset += std::bind(&onAnimationFinished, stack, stackAnimation[stack->unitId()]);
-		stackAnimation[stack->unitId()]->pos.h = stackAnimation[stack->unitId()]->getHeight();
-		stackAnimation[stack->unitId()]->pos.w = stackAnimation[stack->unitId()]->getWidth();
-	}
-	stackAnimation[stack->unitId()]->pos.x = coords.x;
-	stackAnimation[stack->unitId()]->pos.y = coords.y;
-	stackAnimation[stack->unitId()]->setType(ECreatureAnimType::HOLDING);
-
-	if (!instant)
-	{
-		// immediately make stack transparent, giving correct shifter time to start
-		auto shifterFade = ColorFilter::genAlphaShifter(0);
-		setStackColorFilter(shifterFade, stack, nullptr, true);
-
-		owner.addToAnimationStage(EAnimationEvents::HIT, [=]()
-		{
-			addNewAnim(new ColorTransformAnimation(owner, stack, "summonFadeIn", nullptr));
-			if (stack->isClone())
-				addNewAnim(new ColorTransformAnimation(owner, stack, "cloning", SpellID(SpellID::CLONE).toSpell() ));
-		});
-	}
-}
-
-void BattleStacksController::setActiveStack(const CStack *stack)
-{
-	if (activeStack) // update UI
-		stackAnimation[activeStack->unitId()]->setBorderColor(AnimationControls::getNoBorder());
-
-	activeStack = stack;
-
-	if (activeStack) // update UI
-		stackAnimation[activeStack->unitId()]->setBorderColor(AnimationControls::getGoldBorder());
-
-	owner.windowObject->blockUI(activeStack == nullptr);
-
-	if (activeStack)
-		stackAmountBoxHidden.clear();
-}
-
-bool BattleStacksController::stackNeedsAmountBox(const CStack * stack) const
-{
-	//do not show box for singular war machines, stacked war machines with box shown are supported as extension feature
-	if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON) && stack->getCount() == 1)
-		return false;
-
-	if(!stack->alive())
-		return false;
-
-	//hide box when target is going to die anyway - do not display "0 creatures"
-	if(stack->getCount() == 0)
-		return false;
-
-	// if stack has any ongoing animation - hide the box
-	if (stackAmountBoxHidden.count(stack->unitId()))
-		return false;
-
-	return true;
-}
-
-std::shared_ptr<IImage> BattleStacksController::getStackAmountBox(const CStack * stack)
-{
-	std::vector<SpellID> activeSpells = stack->activeSpells();
-
-	if ( activeSpells.empty())
-		return amountNormal;
-
-	int effectsPositivness = 0;
-
-	for(const auto & spellID : activeSpells)
-	{
-		auto positiveness = CGI->spells()->getByIndex(spellID)->getPositiveness();
-		if(!boost::logic::indeterminate(positiveness))
-		{
-			if(positiveness)
-				effectsPositivness++;
-			else
-				effectsPositivness--;
-		}
-	}
-
-	if (effectsPositivness > 0)
-		return amountPositive;
-
-	if (effectsPositivness < 0)
-		return amountNegative;
-
-	return amountEffNeutral;
-}
-
-void BattleStacksController::showStackAmountBox(Canvas & canvas, const CStack * stack)
-{
-	auto amountBG = getStackAmountBox(stack);
-
-	bool doubleWide = stack->doubleWide();
-	bool turnedRight = facingRight(stack);
-	bool attacker = stack->unitSide() == BattleSide::ATTACKER;
-
-	BattleHex stackPos = stack->getPosition();
-
-	// double-wide unit turned around - use opposite hex for stack label
-	if (doubleWide && turnedRight != attacker)
-		stackPos = stack->occupiedHex();
-
-	BattleHex frontPos = turnedRight ?
-		stackPos.cloneInDirection(BattleHex::RIGHT) :
-		stackPos.cloneInDirection(BattleHex::LEFT);
-
-	bool moveInside = !owner.fieldController->stackCountOutsideHex(frontPos);
-
-	Point boxPosition;
-
-	if (moveInside)
-	{
-		boxPosition = owner.fieldController->hexPositionLocal(stackPos).center() + Point(-15, 1);
-	}
-	else
-	{
-		if (turnedRight)
-			boxPosition = owner.fieldController->hexPositionLocal(frontPos).center() + Point (-22, 1);
-		else
-			boxPosition = owner.fieldController->hexPositionLocal(frontPos).center() + Point(-8, -14);
-	}
-
-	Point textPosition = amountBG->dimensions()/2 + boxPosition;
-
-	canvas.draw(amountBG, boxPosition);
-	canvas.drawText(textPosition, EFonts::FONT_TINY, Colors::WHITE, ETextAlignment::CENTER, TextOperations::formatMetric(stack->getCount(), 4));
-}
-
-void BattleStacksController::showStack(Canvas & canvas, const CStack * stack)
-{
-	ColorFilter fullFilter = ColorFilter::genEmptyShifter();
-	for(const auto & filter : stackFilterEffects)
-	{
-		if (filter.target == stack)
-			fullFilter = ColorFilter::genCombined(fullFilter, filter.effect);
-	}
-
-	stackAnimation[stack->unitId()]->nextFrame(canvas, fullFilter, facingRight(stack)); // do actual blit
-}
-
-void BattleStacksController::tick(uint32_t msPassed)
-{
-	updateHoveredStacks();
-	updateBattleAnimations(msPassed);
-}
-
-void BattleStacksController::initializeBattleAnimations()
-{
-	auto copiedVector = currentAnimations;
-	for (auto & elem : copiedVector)
-		if (elem && !elem->isInitialized())
-			elem->tryInitialize();
-}
-
-void BattleStacksController::tickFrameBattleAnimations(uint32_t msPassed)
-{
-	for (auto stack : owner.getBattle()->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->tick(msPassed);
-}
-
-void BattleStacksController::updateBattleAnimations(uint32_t msPassed)
-{
-	bool hadAnimations = !currentAnimations.empty();
-	initializeBattleAnimations();
-	tickFrameBattleAnimations(msPassed);
-	vstd::erase(currentAnimations, nullptr);
-
-	if (currentAnimations.empty())
-		owner.executeStagedAnimations();
-
-	if (hadAnimations && currentAnimations.empty())
-		owner.onAnimationsFinished();
-
-	initializeBattleAnimations();
-}
-
-void BattleStacksController::addNewAnim(BattleAnimation *anim)
-{
-	if (currentAnimations.empty())
-		stackAmountBoxHidden.clear();
-
-	owner.onAnimationsStarted();
-	currentAnimations.push_back(anim);
-
-	auto stackAnimation = dynamic_cast<BattleStackAnimation*>(anim);
-	if(stackAnimation)
-		stackAmountBoxHidden.insert(stackAnimation->stack->unitId());
-}
-
-void BattleStacksController::stackRemoved(uint32_t stackID)
-{
-	if (getActiveStack() && getActiveStack()->unitId() == stackID)
-		setActiveStack(nullptr);
-}
-
-void BattleStacksController::stacksAreAttacked(std::vector<StackAttackedInfo> attackedInfos)
-{
-	owner.addToAnimationStage(EAnimationEvents::HIT, [=](){
-		// remove any potentially erased petrification effect
-		removeExpiredColorFilters();
-	});
-
-	for(auto & attackedInfo : attackedInfos)
-	{
-		if (!attackedInfo.attacker)
-			continue;
-
-		// In H3, attacked stack will not reverse on ranged attack
-		if (attackedInfo.indirectAttack)
-			continue;
-
-		// Another type of indirect attack - dragon breath
-		if (!CStack::isMeleeAttackPossible(attackedInfo.attacker, attackedInfo.defender))
-			continue;
-
-		// defender need to face in direction opposited to out attacker
-		bool needsReverse = shouldAttackFacingRight(attackedInfo.attacker, attackedInfo.defender) == facingRight(attackedInfo.defender);
-
-		// FIXME: this check is better, however not usable since stacksAreAttacked is called after net pack is applyed - petrification is already removed
-		// if (needsReverse && !attackedInfo.defender->isFrozen())
-		if (needsReverse && stackAnimation[attackedInfo.defender->unitId()]->getType() != ECreatureAnimType::FROZEN)
-		{
-			owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [=]()
-			{
-				addNewAnim(new ReverseAnimation(owner, attackedInfo.defender, attackedInfo.defender->getPosition()));
-			});
-		}
-	}
-
-	for(auto & attackedInfo : attackedInfos)
-	{
-		bool useDeathAnim   = attackedInfo.killed;
-		bool useDefenceAnim = attackedInfo.defender->defendingAnim && !attackedInfo.indirectAttack && !attackedInfo.killed;
-
-		EAnimationEvents usedEvent = useDefenceAnim ? EAnimationEvents::ATTACK : EAnimationEvents::HIT;
-
-		owner.addToAnimationStage(usedEvent, [=]()
-		{
-			if (useDeathAnim)
-				addNewAnim(new DeathAnimation(owner, attackedInfo.defender, attackedInfo.indirectAttack));
-			else if(useDefenceAnim)
-				addNewAnim(new DefenceAnimation(owner, attackedInfo.defender));
-			else
-				addNewAnim(new HittedAnimation(owner, attackedInfo.defender));
-
-			if (attackedInfo.fireShield)
-				owner.effectsController->displayEffect(EBattleEffect::FIRE_SHIELD, AudioPath::builtin("FIRESHIE"), attackedInfo.attacker->getPosition());
-
-			if (attackedInfo.spellEffect != SpellID::NONE)
-			{
-				auto spell = attackedInfo.spellEffect.toSpell();
-				if (!spell->getCastSound().empty())
-					CCS->soundh->playSound(spell->getCastSound());
-
-
-				owner.displaySpellEffect(spell, attackedInfo.defender->getPosition());
-			}
-		});
-	}
-
-	for (auto & attackedInfo : attackedInfos)
-	{
-		if (attackedInfo.rebirth)
-		{
-			owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){
-				owner.effectsController->displayEffect(EBattleEffect::RESURRECT, AudioPath::builtin("RESURECT"), attackedInfo.defender->getPosition());
-				addNewAnim(new ResurrectionAnimation(owner, attackedInfo.defender));
-			});
-		}
-
-		if (attackedInfo.killed && attackedInfo.defender->summoned)
-		{
-			owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){
-				addNewAnim(new ColorTransformAnimation(owner, attackedInfo.defender, "summonFadeOut", nullptr));
-				stackRemoved(attackedInfo.defender->unitId());
-			});
-		}
-	}
-	owner.executeStagedAnimations();
-	owner.waitForAnimations();
-}
-
-void BattleStacksController::stackTeleported(const CStack *stack, std::vector<BattleHex> destHex, int distance)
-{
-	assert(destHex.size() > 0);
-	//owner.checkForAnimations(); // NOTE: at this point spellcast animations were added, but not executed
-
-	owner.addToAnimationStage(EAnimationEvents::HIT, [=](){
-		addNewAnim( new ColorTransformAnimation(owner, stack, "teleportFadeOut", nullptr) );
-	});
-
-	owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){
-		stackAnimation[stack->unitId()]->pos.moveTo(getStackPositionAtHex(destHex.back(), stack));
-		addNewAnim( new ColorTransformAnimation(owner, stack, "teleportFadeIn", nullptr) );
-	});
-
-	// animations will be executed by spell
-}
-
-void BattleStacksController::stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance)
-{
-	assert(destHex.size() > 0);
-	owner.checkForAnimations();
-
-	if(shouldRotate(stack, stack->getPosition(), destHex[0]))
-	{
-		owner.addToAnimationStage(EAnimationEvents::ROTATE, [&]()
-		{
-			addNewAnim(new ReverseAnimation(owner, stack, stack->getPosition()));
-		});
-	}
-
-	owner.addToAnimationStage(EAnimationEvents::MOVE_START, [&]()
-	{
-		addNewAnim(new MovementStartAnimation(owner, stack));
-	});
-
-	if (!stack->hasBonus(Selector::typeSubtype(BonusType::FLYING, BonusCustomSubtype::movementFlying)))
-	{
-		owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [&]()
-		{
-			addNewAnim(new MovementAnimation(owner, stack, destHex, distance));
-		});
-	}
-
-	owner.addToAnimationStage(EAnimationEvents::MOVE_END, [&]()
-	{
-		addNewAnim(new MovementEndAnimation(owner, stack, destHex.back()));
-	});
-
-	owner.executeStagedAnimations();
-	owner.waitForAnimations();
-}
-
-bool BattleStacksController::shouldAttackFacingRight(const CStack * attacker, const CStack * defender)
-{
-	bool mustReverse = owner.getBattle()->isToReverse(attacker, defender);
-
-	if (attacker->unitSide() == BattleSide::ATTACKER)
-		return !mustReverse;
-	else
-		return mustReverse;
-}
-
-void BattleStacksController::stackAttacking( const StackAttackInfo & info )
-{
-	owner.checkForAnimations();
-
-	auto attacker    = info.attacker;
-	auto defender    = info.defender;
-	auto tile        = info.tile;
-	auto spellEffect = info.spellEffect;
-	auto multiAttack = !info.secondaryDefender.empty();
-	bool needsReverse = false;
-
-	if (info.indirectAttack)
-	{
-		needsReverse = shouldRotate(attacker, attacker->position, info.tile);
-	}
-	else
-	{
-		needsReverse = shouldAttackFacingRight(attacker, defender) != facingRight(attacker);
-	}
-
-	if (needsReverse)
-	{
-		owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [=]()
-		{
-			addNewAnim(new ReverseAnimation(owner, attacker, attacker->getPosition()));
-		});
-	}
-
-	if(info.lucky)
-	{
-		owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() {
-			owner.appendBattleLog(info.attacker->formatGeneralMessage(-45));
-			owner.effectsController->displayEffect(EBattleEffect::GOOD_LUCK, AudioPath::builtin("GOODLUCK"), attacker->getPosition());
-		});
-	}
-
-	if(info.unlucky)
-	{
-		owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() {
-			owner.appendBattleLog(info.attacker->formatGeneralMessage(-44));
-			owner.effectsController->displayEffect(EBattleEffect::BAD_LUCK, AudioPath::builtin("BADLUCK"), attacker->getPosition());
-		});
-	}
-
-	if(info.deathBlow)
-	{
-		owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() {
-			owner.appendBattleLog(info.attacker->formatGeneralMessage(365));
-			owner.effectsController->displayEffect(EBattleEffect::DEATH_BLOW, AudioPath::builtin("DEATHBLO"), defender->getPosition());
-		});
-
-		for(auto elem : info.secondaryDefender)
-		{
-			owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() {
-				owner.effectsController->displayEffect(EBattleEffect::DEATH_BLOW, elem->getPosition());
-			});
-		}
-	}
-
-	owner.addToAnimationStage(EAnimationEvents::ATTACK, [=]()
-	{
-		if (info.indirectAttack)
-		{
-			addNewAnim(new ShootingAnimation(owner, attacker, tile, defender));
-		}
-		else
-		{
-			addNewAnim(new MeleeAttackAnimation(owner, attacker, tile, defender, multiAttack));
-		}
-	});
-
-	if (info.spellEffect != SpellID::NONE)
-	{
-		owner.addToAnimationStage(EAnimationEvents::HIT, [=]()
-		{
-			owner.displaySpellHit(spellEffect.toSpell(), tile);
-		});
-	}
-
-	if (info.lifeDrain)
-	{
-		owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=]()
-		{
-			owner.effectsController->displayEffect(EBattleEffect::DRAIN_LIFE, AudioPath::builtin("DRAINLIF"), attacker->getPosition());
-		});
-	}
-
-	//return, animation playback will be handled by stacksAreAttacked
-}
-
-bool BattleStacksController::shouldRotate(const CStack * stack, const BattleHex & oldPos, const BattleHex & nextHex) const
-{
-	Point begPosition = getStackPositionAtHex(oldPos,stack);
-	Point endPosition = getStackPositionAtHex(nextHex, stack);
-
-	if((begPosition.x > endPosition.x) && facingRight(stack))
-		return true;
-	else if((begPosition.x < endPosition.x) && !facingRight(stack))
-		return true;
-
-	return false;
-}
-
-void BattleStacksController::endAction(const BattleAction & action)
-{
-	owner.checkForAnimations();
-
-	//check if we should reverse stacks
-	TStacks stacks = owner.getBattle()->battleGetStacks(CPlayerBattleCallback::MINE_AND_ENEMY);
-
-	for (const CStack *s : stacks)
-	{
-		bool shouldFaceRight  = s && s->unitSide() == BattleSide::ATTACKER;
-
-		if (s && facingRight(s) != shouldFaceRight && s->alive() && stackAnimation[s->unitId()]->isIdle())
-		{
-			addNewAnim(new ReverseAnimation(owner, s, s->getPosition()));
-		}
-	}
-	owner.executeStagedAnimations();
-	owner.waitForAnimations();
-
-	stackAmountBoxHidden.clear();
-
-	owner.windowObject->blockUI(activeStack == nullptr);
-	removeExpiredColorFilters();
-}
-
-void BattleStacksController::startAction(const BattleAction & action)
-{
-	removeExpiredColorFilters();
-}
-
-void BattleStacksController::stackActivated(const CStack *stack)
-{
-	stackToActivate = stack;
-	owner.waitForAnimations();
-	logAnim->debug("Activating next stack");
-	owner.activateStack();
-}
-
-void BattleStacksController::deactivateStack()
-{
-	if (!activeStack) {
-		return;
-	}
-	stackToActivate = activeStack;
-	setActiveStack(nullptr);
-}
-
-void BattleStacksController::activateStack()
-{
-	if ( !currentAnimations.empty())
-		return;
-
-	if ( !stackToActivate)
-		return;
-
-	owner.trySetActivePlayer(stackToActivate->unitOwner());
-
-	setActiveStack(stackToActivate);
-	stackToActivate = nullptr;
-
-	const CStack * s = getActiveStack();
-	if(!s)
-		return;
-}
-
-const CStack* BattleStacksController::getActiveStack() const
-{
-	return activeStack;
-}
-
-bool BattleStacksController::facingRight(const CStack * stack) const
-{
-	return stackFacingRight.at(stack->unitId());
-}
-
-Point BattleStacksController::getStackPositionAtHex(BattleHex hexNum, const CStack * stack) const
-{
-	Point ret(-500, -500); //returned value
-	if(stack && stack->initialPosition < 0) //creatures in turrets
-		return owner.siegeController->getTurretCreaturePosition(stack->initialPosition);
-
-	static const Point basePos(-189, -139); // position of creature in topleft corner
-	static const int imageShiftX = 29; // X offset to base pos for facing right stacks, negative for facing left
-
-	ret.x = basePos.x + 22 * ( (hexNum.getY() + 1)%2 ) + 44 * hexNum.getX();
-	ret.y = basePos.y + 42 * hexNum.getY();
-
-	if (stack)
-	{
-		if(facingRight(stack))
-			ret.x += imageShiftX;
-		else
-			ret.x -= imageShiftX;
-
-		//shifting position for double - hex creatures
-		if(stack->doubleWide())
-		{
-			if(stack->unitSide() == BattleSide::ATTACKER)
-			{
-				if(facingRight(stack))
-					ret.x -= 44;
-			}
-			else
-			{
-				if(!facingRight(stack))
-					ret.x += 44;
-			}
-		}
-	}
-	//returning
-	return ret;
-}
-
-void BattleStacksController::setStackColorFilter(const ColorFilter & effect, const CStack * target, const CSpell * source, bool persistent)
-{
-	for (auto & filter : stackFilterEffects)
-	{
-		if (filter.target == target && filter.source == source)
-		{
-			filter.effect     = effect;
-			filter.persistent = persistent;
-			return;
-		}
-	}
-	stackFilterEffects.push_back({ effect, target, source, persistent });
-}
-
-void BattleStacksController::removeExpiredColorFilters()
-{
-	vstd::erase_if(stackFilterEffects, [&](const BattleStackFilterEffect & filter)
-	{
-		if (!filter.persistent)
-		{
-			if (filter.source && !filter.target->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(filter.source->id)), Selector::all))
-				return true;
-			if (filter.effect == ColorFilter::genEmptyShifter())
-				return true;
-		}
-		return false;
-	});
-}
-
-void BattleStacksController::updateHoveredStacks()
-{
-	auto newStacks = selectHoveredStacks();
-
-	for(const auto * stack : mouseHoveredStacks)
-	{
-		if (vstd::contains(newStacks, stack))
-			continue;
-
-		if (stack == activeStack)
-			stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getGoldBorder());
-		else
-			stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getNoBorder());
-	}
-
-	for(const auto * stack : newStacks)
-	{
-		if (vstd::contains(mouseHoveredStacks, stack))
-			continue;
-
-		stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getBlueBorder());
-		if (stackAnimation[stack->unitId()]->framesInGroup(ECreatureAnimType::MOUSEON) > 0 && stack->alive() && !stack->isFrozen())
-			stackAnimation[stack->unitId()]->playOnce(ECreatureAnimType::MOUSEON);
-	}
-
-	mouseHoveredStacks = newStacks;
-}
-
-std::vector<const CStack *> BattleStacksController::selectHoveredStacks()
-{
-	// only allow during our turn - do not try to highlight creatures while they are in the middle of actions
-	if (!activeStack)
-		return {};
-
-	if(owner.hasAnimations())
-		return {};
-
-	auto hoveredQueueUnitId = owner.windowObject->getQueueHoveredUnitId();
-	if(hoveredQueueUnitId.has_value())
-	{
-		return { owner.getBattle()->battleGetStackByID(hoveredQueueUnitId.value(), true) };
-	}
-
-	auto hoveredHex = owner.fieldController->getHoveredHex();
-
-	if (!hoveredHex.isValid())
-		return {};
-
-	const spells::Caster *caster = nullptr;
-	const CSpell *spell = nullptr;
-
-	spells::Mode mode = owner.actionsController->getCurrentCastMode();
-	spell = owner.actionsController->getCurrentSpell(hoveredHex);
-	caster = owner.actionsController->getCurrentSpellcaster();
-
-	if(caster && spell && owner.actionsController->currentActionSpellcasting(hoveredHex) ) //when casting spell
-	{
-		spells::Target target;
-		target.emplace_back(hoveredHex);
-
-		spells::BattleCast event(owner.getBattle().get(), caster, mode, spell);
-		auto mechanics = spell->battleMechanics(&event);
-		return mechanics->getAffectedStacks(target);
-	}
-
-	if(hoveredHex.isValid())
-	{
-		const CStack * const stack = owner.getBattle()->battleGetStackByPos(hoveredHex, true);
-
-		if (stack)
-			return {stack};
-	}
-
-	return {};
-}
-
-const std::vector<uint32_t> BattleStacksController::getHoveredStacksUnitIds() const
-{
-	auto result = std::vector<uint32_t>();
-	for(const auto * stack : mouseHoveredStacks)
-	{
-		result.push_back(stack->unitId());
-	}
-
-	return result;
-}
+/*
+ * BattleStacksController.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 "BattleStacksController.h"
+
+#include "BattleSiegeController.h"
+#include "BattleInterfaceClasses.h"
+#include "BattleInterface.h"
+#include "BattleActionsController.h"
+#include "BattleAnimationClasses.h"
+#include "BattleFieldController.h"
+#include "BattleEffectsController.h"
+#include "BattleProjectileController.h"
+#include "BattleWindow.h"
+#include "BattleRenderer.h"
+#include "CreatureAnimation.h"
+
+#include "../CPlayerInterface.h"
+#include "../CMusicHandler.h"
+#include "../CGameInfo.h"
+#include "../gui/CGuiHandler.h"
+#include "../render/Colors.h"
+#include "../render/Canvas.h"
+#include "../render/IRenderHandler.h"
+
+#include "../../CCallback.h"
+#include "../../lib/spells/ISpellMechanics.h"
+#include "../../lib/battle/BattleAction.h"
+#include "../../lib/battle/BattleHex.h"
+#include "../../lib/CStack.h"
+#include "../../lib/CondSh.h"
+#include "../../lib/TextOperations.h"
+
+static void onAnimationFinished(const CStack *stack, std::weak_ptr<CreatureAnimation> anim)
+{
+	std::shared_ptr<CreatureAnimation> animation = anim.lock();
+	if(!animation)
+		return;
+
+	if (!stack->isFrozen() && animation->getType() == ECreatureAnimType::FROZEN)
+		animation->setType(ECreatureAnimType::HOLDING);
+
+	if (animation->isIdle())
+	{
+		const CCreature *creature = stack->unitType();
+
+		if (stack->isFrozen())
+			animation->setType(ECreatureAnimType::FROZEN);
+		else
+		if (animation->framesInGroup(ECreatureAnimType::MOUSEON) > 0)
+		{
+			if (CRandomGenerator::getDefault().nextDouble(99.0) < creature->animation.timeBetweenFidgets *10)
+				animation->playOnce(ECreatureAnimType::MOUSEON);
+			else
+				animation->setType(ECreatureAnimType::HOLDING);
+		}
+		else
+		{
+			animation->setType(ECreatureAnimType::HOLDING);
+		}
+	}
+	// always reset callback
+	animation->onAnimationReset += std::bind(&onAnimationFinished, stack, anim);
+}
+
+BattleStacksController::BattleStacksController(BattleInterface & owner):
+	owner(owner),
+	activeStack(nullptr),
+	stackToActivate(nullptr),
+	animIDhelper(0)
+{
+	//preparing graphics for displaying amounts of creatures
+	amountNormal     = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY);
+	amountPositive   = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY);
+	amountNegative   = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY);
+	amountEffNeutral = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY);
+
+	static const auto shifterNormal   = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.6f, 0.2f, 1.0f );
+	static const auto shifterPositive = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.2f, 1.0f, 0.2f );
+	static const auto shifterNegative = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 0.2f, 0.2f );
+	static const auto shifterNeutral  = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 1.0f, 0.2f );
+
+	// do not change border color
+	static const int32_t ignoredMask = 1 << 26;
+
+	amountNormal->adjustPalette(shifterNormal, ignoredMask);
+	amountPositive->adjustPalette(shifterPositive, ignoredMask);
+	amountNegative->adjustPalette(shifterNegative, ignoredMask);
+	amountEffNeutral->adjustPalette(shifterNeutral, ignoredMask);
+
+	std::vector<const CStack*> stacks = owner.getBattle()->battleGetAllStacks(true);
+	for(const CStack * s : stacks)
+	{
+		stackAdded(s, true);
+	}
+}
+
+BattleHex BattleStacksController::getStackCurrentPosition(const CStack * stack) const
+{
+	if ( !stackAnimation.at(stack->unitId())->isMoving())
+		return stack->getPosition();
+
+	if (stack->hasBonusOfType(BonusType::FLYING) && stackAnimation.at(stack->unitId())->getType() == ECreatureAnimType::MOVING )
+		return BattleHex::HEX_AFTER_ALL;
+
+	for (auto & anim : currentAnimations)
+	{
+		// certainly ugly workaround but fixes quite annoying bug
+		// stack position will be updated only *after* movement is finished
+		// before this - stack is always at its initial position. Thus we need to find
+		// its current position. Which can be found only in this class
+		if (StackMoveAnimation *move = dynamic_cast<StackMoveAnimation*>(anim))
+		{
+			if (move->stack == stack)
+				return std::max(move->prevHex, move->nextHex);
+		}
+	}
+	return stack->getPosition();
+}
+
+void BattleStacksController::collectRenderableObjects(BattleRenderer & renderer)
+{
+	auto stacks = owner.getBattle()->battleGetAllStacks(false);
+
+	for (auto stack : stacks)
+	{
+		if (stackAnimation.find(stack->unitId()) == stackAnimation.end()) //e.g. for summoned but not yet handled stacks
+			continue;
+
+		//FIXME: hack to ignore ghost stacks
+		if ((stackAnimation[stack->unitId()]->getType() == ECreatureAnimType::DEAD || stackAnimation[stack->unitId()]->getType() == ECreatureAnimType::HOLDING) && stack->isGhost())
+			continue;
+
+		auto layer = stackAnimation[stack->unitId()]->isDead() ? EBattleFieldLayer::CORPSES : EBattleFieldLayer::STACKS;
+		auto location = getStackCurrentPosition(stack);
+
+		renderer.insert(layer, location, [this, stack]( BattleRenderer::RendererRef renderer ){
+			showStack(renderer, stack);
+		});
+
+		if (stackNeedsAmountBox(stack))
+		{
+			renderer.insert(EBattleFieldLayer::STACK_AMOUNTS, location, [this, stack]( BattleRenderer::RendererRef renderer ){
+				showStackAmountBox(renderer, stack);
+			});
+		}
+	}
+}
+
+void BattleStacksController::stackReset(const CStack * stack)
+{
+	auto iter = stackAnimation.find(stack->unitId());
+
+	if(iter == stackAnimation.end())
+	{
+		logGlobal->error("Unit %d have no animation", stack->unitId());
+		return;
+	}
+
+	auto animation = iter->second;
+
+	if(stack->alive() && animation->isDeadOrDying())
+	{
+		owner.addToAnimationStage(EAnimationEvents::HIT, [=]()
+		{
+			addNewAnim(new ResurrectionAnimation(owner, stack));
+		});
+	}
+}
+
+void BattleStacksController::stackAdded(const CStack * stack, bool instant)
+{
+	// Tower shooters have only their upper half visible
+	static const int turretCreatureAnimationHeight = 232;
+
+	stackFacingRight[stack->unitId()] = stack->unitSide() == BattleSide::ATTACKER; // must be set before getting stack position
+
+	Point coords = getStackPositionAtHex(stack->getPosition(), stack);
+
+	if(stack->initialPosition < 0) //turret
+	{
+		assert(owner.siegeController);
+
+		const CCreature *turretCreature = owner.siegeController->getTurretCreature();
+
+		stackAnimation[stack->unitId()] = AnimationControls::getAnimation(turretCreature);
+		stackAnimation[stack->unitId()]->pos.h = turretCreatureAnimationHeight;
+		stackAnimation[stack->unitId()]->pos.w = stackAnimation[stack->unitId()]->getWidth();
+
+		// FIXME: workaround for visible animation of Medusa tails (animation disabled in H3)
+		if (turretCreature->getId() == CreatureID::MEDUSA )
+			stackAnimation[stack->unitId()]->pos.w = 250;
+
+		coords = owner.siegeController->getTurretCreaturePosition(stack->initialPosition);
+	}
+	else
+	{
+		stackAnimation[stack->unitId()] = AnimationControls::getAnimation(stack->unitType());
+		stackAnimation[stack->unitId()]->onAnimationReset += std::bind(&onAnimationFinished, stack, stackAnimation[stack->unitId()]);
+		stackAnimation[stack->unitId()]->pos.h = stackAnimation[stack->unitId()]->getHeight();
+		stackAnimation[stack->unitId()]->pos.w = stackAnimation[stack->unitId()]->getWidth();
+	}
+	stackAnimation[stack->unitId()]->pos.x = coords.x;
+	stackAnimation[stack->unitId()]->pos.y = coords.y;
+	stackAnimation[stack->unitId()]->setType(ECreatureAnimType::HOLDING);
+
+	if (!instant)
+	{
+		// immediately make stack transparent, giving correct shifter time to start
+		auto shifterFade = ColorFilter::genAlphaShifter(0);
+		setStackColorFilter(shifterFade, stack, nullptr, true);
+
+		owner.addToAnimationStage(EAnimationEvents::HIT, [=]()
+		{
+			addNewAnim(new ColorTransformAnimation(owner, stack, "summonFadeIn", nullptr));
+			if (stack->isClone())
+				addNewAnim(new ColorTransformAnimation(owner, stack, "cloning", SpellID(SpellID::CLONE).toSpell() ));
+		});
+	}
+}
+
+void BattleStacksController::setActiveStack(const CStack *stack)
+{
+	if (activeStack) // update UI
+		stackAnimation[activeStack->unitId()]->setBorderColor(AnimationControls::getNoBorder());
+
+	activeStack = stack;
+
+	if (activeStack) // update UI
+		stackAnimation[activeStack->unitId()]->setBorderColor(AnimationControls::getGoldBorder());
+
+	owner.windowObject->blockUI(activeStack == nullptr);
+
+	if (activeStack)
+		stackAmountBoxHidden.clear();
+}
+
+bool BattleStacksController::stackNeedsAmountBox(const CStack * stack) const
+{
+	//do not show box for singular war machines, stacked war machines with box shown are supported as extension feature
+	if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON) && stack->getCount() == 1)
+		return false;
+
+	if(!stack->alive())
+		return false;
+
+	//hide box when target is going to die anyway - do not display "0 creatures"
+	if(stack->getCount() == 0)
+		return false;
+
+	// if stack has any ongoing animation - hide the box
+	if (stackAmountBoxHidden.count(stack->unitId()))
+		return false;
+
+	return true;
+}
+
+std::shared_ptr<IImage> BattleStacksController::getStackAmountBox(const CStack * stack)
+{
+	std::vector<SpellID> activeSpells = stack->activeSpells();
+
+	if ( activeSpells.empty())
+		return amountNormal;
+
+	int effectsPositivness = 0;
+
+	for(const auto & spellID : activeSpells)
+	{
+		auto positiveness = CGI->spells()->getByIndex(spellID)->getPositiveness();
+		if(!boost::logic::indeterminate(positiveness))
+		{
+			if(positiveness)
+				effectsPositivness++;
+			else
+				effectsPositivness--;
+		}
+	}
+
+	if (effectsPositivness > 0)
+		return amountPositive;
+
+	if (effectsPositivness < 0)
+		return amountNegative;
+
+	return amountEffNeutral;
+}
+
+void BattleStacksController::showStackAmountBox(Canvas & canvas, const CStack * stack)
+{
+	auto amountBG = getStackAmountBox(stack);
+
+	bool doubleWide = stack->doubleWide();
+	bool turnedRight = facingRight(stack);
+	bool attacker = stack->unitSide() == BattleSide::ATTACKER;
+
+	BattleHex stackPos = stack->getPosition();
+
+	// double-wide unit turned around - use opposite hex for stack label
+	if (doubleWide && turnedRight != attacker)
+		stackPos = stack->occupiedHex();
+
+	BattleHex frontPos = turnedRight ?
+		stackPos.cloneInDirection(BattleHex::RIGHT) :
+		stackPos.cloneInDirection(BattleHex::LEFT);
+
+	bool moveInside = !owner.fieldController->stackCountOutsideHex(frontPos);
+
+	Point boxPosition;
+
+	if (moveInside)
+	{
+		boxPosition = owner.fieldController->hexPositionLocal(stackPos).center() + Point(-15, 1);
+	}
+	else
+	{
+		if (turnedRight)
+			boxPosition = owner.fieldController->hexPositionLocal(frontPos).center() + Point (-22, 1);
+		else
+			boxPosition = owner.fieldController->hexPositionLocal(frontPos).center() + Point(-8, -14);
+	}
+
+	Point textPosition = amountBG->dimensions()/2 + boxPosition;
+
+	canvas.draw(amountBG, boxPosition);
+	canvas.drawText(textPosition, EFonts::FONT_TINY, Colors::WHITE, ETextAlignment::CENTER, TextOperations::formatMetric(stack->getCount(), 4));
+}
+
+void BattleStacksController::showStack(Canvas & canvas, const CStack * stack)
+{
+	ColorFilter fullFilter = ColorFilter::genEmptyShifter();
+	for(const auto & filter : stackFilterEffects)
+	{
+		if (filter.target == stack)
+			fullFilter = ColorFilter::genCombined(fullFilter, filter.effect);
+	}
+
+	stackAnimation[stack->unitId()]->nextFrame(canvas, fullFilter, facingRight(stack)); // do actual blit
+}
+
+void BattleStacksController::tick(uint32_t msPassed)
+{
+	updateHoveredStacks();
+	updateBattleAnimations(msPassed);
+}
+
+void BattleStacksController::initializeBattleAnimations()
+{
+	auto copiedVector = currentAnimations;
+	for (auto & elem : copiedVector)
+		if (elem && !elem->isInitialized())
+			elem->tryInitialize();
+}
+
+void BattleStacksController::tickFrameBattleAnimations(uint32_t msPassed)
+{
+	for (auto stack : owner.getBattle()->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->tick(msPassed);
+}
+
+void BattleStacksController::updateBattleAnimations(uint32_t msPassed)
+{
+	bool hadAnimations = !currentAnimations.empty();
+	initializeBattleAnimations();
+	tickFrameBattleAnimations(msPassed);
+	vstd::erase(currentAnimations, nullptr);
+
+	if (currentAnimations.empty())
+		owner.executeStagedAnimations();
+
+	if (hadAnimations && currentAnimations.empty())
+		owner.onAnimationsFinished();
+
+	initializeBattleAnimations();
+}
+
+void BattleStacksController::addNewAnim(BattleAnimation *anim)
+{
+	if (currentAnimations.empty())
+		stackAmountBoxHidden.clear();
+
+	owner.onAnimationsStarted();
+	currentAnimations.push_back(anim);
+
+	auto stackAnimation = dynamic_cast<BattleStackAnimation*>(anim);
+	if(stackAnimation)
+		stackAmountBoxHidden.insert(stackAnimation->stack->unitId());
+}
+
+void BattleStacksController::stackRemoved(uint32_t stackID)
+{
+	if (getActiveStack() && getActiveStack()->unitId() == stackID)
+		setActiveStack(nullptr);
+}
+
+void BattleStacksController::stacksAreAttacked(std::vector<StackAttackedInfo> attackedInfos)
+{
+	owner.addToAnimationStage(EAnimationEvents::HIT, [=](){
+		// remove any potentially erased petrification effect
+		removeExpiredColorFilters();
+	});
+
+	for(auto & attackedInfo : attackedInfos)
+	{
+		if (!attackedInfo.attacker)
+			continue;
+
+		// In H3, attacked stack will not reverse on ranged attack
+		if (attackedInfo.indirectAttack)
+			continue;
+
+		// Another type of indirect attack - dragon breath
+		if (!CStack::isMeleeAttackPossible(attackedInfo.attacker, attackedInfo.defender))
+			continue;
+
+		// defender need to face in direction opposited to out attacker
+		bool needsReverse = shouldAttackFacingRight(attackedInfo.attacker, attackedInfo.defender) == facingRight(attackedInfo.defender);
+
+		// FIXME: this check is better, however not usable since stacksAreAttacked is called after net pack is applyed - petrification is already removed
+		// if (needsReverse && !attackedInfo.defender->isFrozen())
+		if (needsReverse && stackAnimation[attackedInfo.defender->unitId()]->getType() != ECreatureAnimType::FROZEN)
+		{
+			owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [=]()
+			{
+				addNewAnim(new ReverseAnimation(owner, attackedInfo.defender, attackedInfo.defender->getPosition()));
+			});
+		}
+	}
+
+	for(auto & attackedInfo : attackedInfos)
+	{
+		bool useDeathAnim   = attackedInfo.killed;
+		bool useDefenceAnim = attackedInfo.defender->defendingAnim && !attackedInfo.indirectAttack && !attackedInfo.killed;
+
+		EAnimationEvents usedEvent = useDefenceAnim ? EAnimationEvents::ATTACK : EAnimationEvents::HIT;
+
+		owner.addToAnimationStage(usedEvent, [=]()
+		{
+			if (useDeathAnim)
+				addNewAnim(new DeathAnimation(owner, attackedInfo.defender, attackedInfo.indirectAttack));
+			else if(useDefenceAnim)
+				addNewAnim(new DefenceAnimation(owner, attackedInfo.defender));
+			else
+				addNewAnim(new HittedAnimation(owner, attackedInfo.defender));
+
+			if (attackedInfo.fireShield)
+				owner.effectsController->displayEffect(EBattleEffect::FIRE_SHIELD, AudioPath::builtin("FIRESHIE"), attackedInfo.attacker->getPosition());
+
+			if (attackedInfo.spellEffect != SpellID::NONE)
+			{
+				auto spell = attackedInfo.spellEffect.toSpell();
+				if (!spell->getCastSound().empty())
+					CCS->soundh->playSound(spell->getCastSound());
+
+
+				owner.displaySpellEffect(spell, attackedInfo.defender->getPosition());
+			}
+		});
+	}
+
+	for (auto & attackedInfo : attackedInfos)
+	{
+		if (attackedInfo.rebirth)
+		{
+			owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){
+				owner.effectsController->displayEffect(EBattleEffect::RESURRECT, AudioPath::builtin("RESURECT"), attackedInfo.defender->getPosition());
+				addNewAnim(new ResurrectionAnimation(owner, attackedInfo.defender));
+			});
+		}
+
+		if (attackedInfo.killed && attackedInfo.defender->summoned)
+		{
+			owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){
+				addNewAnim(new ColorTransformAnimation(owner, attackedInfo.defender, "summonFadeOut", nullptr));
+				stackRemoved(attackedInfo.defender->unitId());
+			});
+		}
+	}
+	owner.executeStagedAnimations();
+	owner.waitForAnimations();
+}
+
+void BattleStacksController::stackTeleported(const CStack *stack, std::vector<BattleHex> destHex, int distance)
+{
+	assert(destHex.size() > 0);
+	//owner.checkForAnimations(); // NOTE: at this point spellcast animations were added, but not executed
+
+	owner.addToAnimationStage(EAnimationEvents::HIT, [=](){
+		addNewAnim( new ColorTransformAnimation(owner, stack, "teleportFadeOut", nullptr) );
+	});
+
+	owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){
+		stackAnimation[stack->unitId()]->pos.moveTo(getStackPositionAtHex(destHex.back(), stack));
+		addNewAnim( new ColorTransformAnimation(owner, stack, "teleportFadeIn", nullptr) );
+	});
+
+	// animations will be executed by spell
+}
+
+void BattleStacksController::stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance)
+{
+	assert(destHex.size() > 0);
+	owner.checkForAnimations();
+
+	if(shouldRotate(stack, stack->getPosition(), destHex[0]))
+	{
+		owner.addToAnimationStage(EAnimationEvents::ROTATE, [&]()
+		{
+			addNewAnim(new ReverseAnimation(owner, stack, stack->getPosition()));
+		});
+	}
+
+	owner.addToAnimationStage(EAnimationEvents::MOVE_START, [&]()
+	{
+		addNewAnim(new MovementStartAnimation(owner, stack));
+	});
+
+	if (!stack->hasBonus(Selector::typeSubtype(BonusType::FLYING, BonusCustomSubtype::movementFlying)))
+	{
+		owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [&]()
+		{
+			addNewAnim(new MovementAnimation(owner, stack, destHex, distance));
+		});
+	}
+
+	owner.addToAnimationStage(EAnimationEvents::MOVE_END, [&]()
+	{
+		addNewAnim(new MovementEndAnimation(owner, stack, destHex.back()));
+	});
+
+	owner.executeStagedAnimations();
+	owner.waitForAnimations();
+}
+
+bool BattleStacksController::shouldAttackFacingRight(const CStack * attacker, const CStack * defender)
+{
+	bool mustReverse = owner.getBattle()->isToReverse(attacker, defender);
+
+	if (attacker->unitSide() == BattleSide::ATTACKER)
+		return !mustReverse;
+	else
+		return mustReverse;
+}
+
+void BattleStacksController::stackAttacking( const StackAttackInfo & info )
+{
+	owner.checkForAnimations();
+
+	auto attacker    = info.attacker;
+	auto defender    = info.defender;
+	auto tile        = info.tile;
+	auto spellEffect = info.spellEffect;
+	auto multiAttack = !info.secondaryDefender.empty();
+	bool needsReverse = false;
+
+	if (info.indirectAttack)
+	{
+		needsReverse = shouldRotate(attacker, attacker->position, info.tile);
+	}
+	else
+	{
+		needsReverse = shouldAttackFacingRight(attacker, defender) != facingRight(attacker);
+	}
+
+	if (needsReverse)
+	{
+		owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [=]()
+		{
+			addNewAnim(new ReverseAnimation(owner, attacker, attacker->getPosition()));
+		});
+	}
+
+	if(info.lucky)
+	{
+		owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() {
+			owner.appendBattleLog(info.attacker->formatGeneralMessage(-45));
+			owner.effectsController->displayEffect(EBattleEffect::GOOD_LUCK, AudioPath::builtin("GOODLUCK"), attacker->getPosition());
+		});
+	}
+
+	if(info.unlucky)
+	{
+		owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() {
+			owner.appendBattleLog(info.attacker->formatGeneralMessage(-44));
+			owner.effectsController->displayEffect(EBattleEffect::BAD_LUCK, AudioPath::builtin("BADLUCK"), attacker->getPosition());
+		});
+	}
+
+	if(info.deathBlow)
+	{
+		owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() {
+			owner.appendBattleLog(info.attacker->formatGeneralMessage(365));
+			owner.effectsController->displayEffect(EBattleEffect::DEATH_BLOW, AudioPath::builtin("DEATHBLO"), defender->getPosition());
+		});
+
+		for(auto elem : info.secondaryDefender)
+		{
+			owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() {
+				owner.effectsController->displayEffect(EBattleEffect::DEATH_BLOW, elem->getPosition());
+			});
+		}
+	}
+
+	owner.addToAnimationStage(EAnimationEvents::ATTACK, [=]()
+	{
+		if (info.indirectAttack)
+		{
+			addNewAnim(new ShootingAnimation(owner, attacker, tile, defender));
+		}
+		else
+		{
+			addNewAnim(new MeleeAttackAnimation(owner, attacker, tile, defender, multiAttack));
+		}
+	});
+
+	if (info.spellEffect != SpellID::NONE)
+	{
+		owner.addToAnimationStage(EAnimationEvents::HIT, [=]()
+		{
+			owner.displaySpellHit(spellEffect.toSpell(), tile);
+		});
+	}
+
+	if (info.lifeDrain)
+	{
+		owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=]()
+		{
+			owner.effectsController->displayEffect(EBattleEffect::DRAIN_LIFE, AudioPath::builtin("DRAINLIF"), attacker->getPosition());
+		});
+	}
+
+	//return, animation playback will be handled by stacksAreAttacked
+}
+
+bool BattleStacksController::shouldRotate(const CStack * stack, const BattleHex & oldPos, const BattleHex & nextHex) const
+{
+	Point begPosition = getStackPositionAtHex(oldPos,stack);
+	Point endPosition = getStackPositionAtHex(nextHex, stack);
+
+	if((begPosition.x > endPosition.x) && facingRight(stack))
+		return true;
+	else if((begPosition.x < endPosition.x) && !facingRight(stack))
+		return true;
+
+	return false;
+}
+
+void BattleStacksController::endAction(const BattleAction & action)
+{
+	owner.checkForAnimations();
+
+	//check if we should reverse stacks
+	TStacks stacks = owner.getBattle()->battleGetStacks(CPlayerBattleCallback::MINE_AND_ENEMY);
+
+	for (const CStack *s : stacks)
+	{
+		bool shouldFaceRight  = s && s->unitSide() == BattleSide::ATTACKER;
+
+		if (s && facingRight(s) != shouldFaceRight && s->alive() && stackAnimation[s->unitId()]->isIdle())
+		{
+			addNewAnim(new ReverseAnimation(owner, s, s->getPosition()));
+		}
+	}
+	owner.executeStagedAnimations();
+	owner.waitForAnimations();
+
+	stackAmountBoxHidden.clear();
+
+	owner.windowObject->blockUI(activeStack == nullptr);
+	removeExpiredColorFilters();
+}
+
+void BattleStacksController::startAction(const BattleAction & action)
+{
+	removeExpiredColorFilters();
+}
+
+void BattleStacksController::stackActivated(const CStack *stack)
+{
+	stackToActivate = stack;
+	owner.waitForAnimations();
+	logAnim->debug("Activating next stack");
+	owner.activateStack();
+}
+
+void BattleStacksController::deactivateStack()
+{
+	if (!activeStack) {
+		return;
+	}
+	stackToActivate = activeStack;
+	setActiveStack(nullptr);
+}
+
+void BattleStacksController::activateStack()
+{
+	if ( !currentAnimations.empty())
+		return;
+
+	if ( !stackToActivate)
+		return;
+
+	owner.trySetActivePlayer(stackToActivate->unitOwner());
+
+	setActiveStack(stackToActivate);
+	stackToActivate = nullptr;
+
+	const CStack * s = getActiveStack();
+	if(!s)
+		return;
+}
+
+const CStack* BattleStacksController::getActiveStack() const
+{
+	return activeStack;
+}
+
+bool BattleStacksController::facingRight(const CStack * stack) const
+{
+	return stackFacingRight.at(stack->unitId());
+}
+
+Point BattleStacksController::getStackPositionAtHex(BattleHex hexNum, const CStack * stack) const
+{
+	Point ret(-500, -500); //returned value
+	if(stack && stack->initialPosition < 0) //creatures in turrets
+		return owner.siegeController->getTurretCreaturePosition(stack->initialPosition);
+
+	static const Point basePos(-189, -139); // position of creature in topleft corner
+	static const int imageShiftX = 29; // X offset to base pos for facing right stacks, negative for facing left
+
+	ret.x = basePos.x + 22 * ( (hexNum.getY() + 1)%2 ) + 44 * hexNum.getX();
+	ret.y = basePos.y + 42 * hexNum.getY();
+
+	if (stack)
+	{
+		if(facingRight(stack))
+			ret.x += imageShiftX;
+		else
+			ret.x -= imageShiftX;
+
+		//shifting position for double - hex creatures
+		if(stack->doubleWide())
+		{
+			if(stack->unitSide() == BattleSide::ATTACKER)
+			{
+				if(facingRight(stack))
+					ret.x -= 44;
+			}
+			else
+			{
+				if(!facingRight(stack))
+					ret.x += 44;
+			}
+		}
+	}
+	//returning
+	return ret;
+}
+
+void BattleStacksController::setStackColorFilter(const ColorFilter & effect, const CStack * target, const CSpell * source, bool persistent)
+{
+	for (auto & filter : stackFilterEffects)
+	{
+		if (filter.target == target && filter.source == source)
+		{
+			filter.effect     = effect;
+			filter.persistent = persistent;
+			return;
+		}
+	}
+	stackFilterEffects.push_back({ effect, target, source, persistent });
+}
+
+void BattleStacksController::removeExpiredColorFilters()
+{
+	vstd::erase_if(stackFilterEffects, [&](const BattleStackFilterEffect & filter)
+	{
+		if (!filter.persistent)
+		{
+			if (filter.source && !filter.target->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(filter.source->id)), Selector::all))
+				return true;
+			if (filter.effect == ColorFilter::genEmptyShifter())
+				return true;
+		}
+		return false;
+	});
+}
+
+void BattleStacksController::updateHoveredStacks()
+{
+	auto newStacks = selectHoveredStacks();
+
+	for(const auto * stack : mouseHoveredStacks)
+	{
+		if (vstd::contains(newStacks, stack))
+			continue;
+
+		if (stack == activeStack)
+			stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getGoldBorder());
+		else
+			stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getNoBorder());
+	}
+
+	for(const auto * stack : newStacks)
+	{
+		if (vstd::contains(mouseHoveredStacks, stack))
+			continue;
+
+		stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getBlueBorder());
+		if (stackAnimation[stack->unitId()]->framesInGroup(ECreatureAnimType::MOUSEON) > 0 && stack->alive() && !stack->isFrozen())
+			stackAnimation[stack->unitId()]->playOnce(ECreatureAnimType::MOUSEON);
+	}
+
+	mouseHoveredStacks = newStacks;
+}
+
+std::vector<const CStack *> BattleStacksController::selectHoveredStacks()
+{
+	// only allow during our turn - do not try to highlight creatures while they are in the middle of actions
+	if (!activeStack)
+		return {};
+
+	if(owner.hasAnimations())
+		return {};
+
+	auto hoveredQueueUnitId = owner.windowObject->getQueueHoveredUnitId();
+	if(hoveredQueueUnitId.has_value())
+	{
+		return { owner.getBattle()->battleGetStackByID(hoveredQueueUnitId.value(), true) };
+	}
+
+	auto hoveredHex = owner.fieldController->getHoveredHex();
+
+	if (!hoveredHex.isValid())
+		return {};
+
+	const spells::Caster *caster = nullptr;
+	const CSpell *spell = nullptr;
+
+	spells::Mode mode = owner.actionsController->getCurrentCastMode();
+	spell = owner.actionsController->getCurrentSpell(hoveredHex);
+	caster = owner.actionsController->getCurrentSpellcaster();
+
+	if(caster && spell && owner.actionsController->currentActionSpellcasting(hoveredHex) ) //when casting spell
+	{
+		spells::Target target;
+		target.emplace_back(hoveredHex);
+
+		spells::BattleCast event(owner.getBattle().get(), caster, mode, spell);
+		auto mechanics = spell->battleMechanics(&event);
+		return mechanics->getAffectedStacks(target);
+	}
+
+	if(hoveredHex.isValid())
+	{
+		const CStack * const stack = owner.getBattle()->battleGetStackByPos(hoveredHex, true);
+
+		if (stack)
+			return {stack};
+	}
+
+	return {};
+}
+
+const std::vector<uint32_t> BattleStacksController::getHoveredStacksUnitIds() const
+{
+	auto result = std::vector<uint32_t>();
+	for(const auto * stack : mouseHoveredStacks)
+	{
+		result.push_back(stack->unitId());
+	}
+
+	return result;
+}

+ 147 - 147
client/battle/BattleStacksController.h

@@ -1,147 +1,147 @@
-/*
- * BattleStacksController.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 "../render/ColorFilter.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-struct BattleHex;
-class BattleAction;
-class CStack;
-class CSpell;
-class SpellID;
-class Point;
-
-VCMI_LIB_NAMESPACE_END
-
-struct StackAttackedInfo;
-struct StackAttackInfo;
-
-class ColorFilter;
-class Canvas;
-class BattleInterface;
-class BattleAnimation;
-class CreatureAnimation;
-class BattleAnimation;
-class BattleRenderer;
-class IImage;
-
-struct BattleStackFilterEffect
-{
-	ColorFilter effect;
-	const CStack * target;
-	const CSpell * source;
-	bool persistent;
-};
-
-/// Class responsible for handling stacks in battle
-/// Handles ordering of stacks animation
-/// As well as rendering of stacks, their amount boxes
-/// And any other effect applied to stacks
-class BattleStacksController
-{
-	BattleInterface & owner;
-
-	std::shared_ptr<IImage> amountNormal;
-	std::shared_ptr<IImage> amountNegative;
-	std::shared_ptr<IImage> amountPositive;
-	std::shared_ptr<IImage> amountEffNeutral;
-
-	/// currently displayed animations <anim, initialized>
-	std::vector<BattleAnimation *> currentAnimations;
-
-	/// currently active color effects on stacks, in order of their addition (which corresponds to their apply order)
-	std::vector<BattleStackFilterEffect> stackFilterEffects;
-
-	/// animations of creatures from fighting armies (order by BattleInfo's stacks' ID)
-	std::map<int32_t, std::shared_ptr<CreatureAnimation>> stackAnimation;
-
-	/// <creatureID, if false reverse creature's animation> //TODO: move it to battle callback
-	std::map<int, bool> stackFacingRight;
-
-	/// Stacks have amount box hidden due to ongoing animations
-	std::set<int> stackAmountBoxHidden;
-
-	/// currently active stack; nullptr - no one
-	const CStack *activeStack;
-
-	/// stacks or their battle queue images below mouse pointer (multiple stacks possible while spellcasting), used for border animation
-	std::vector<const CStack *> mouseHoveredStacks;
-
-	///when animation is playing, we should wait till the end to make the next stack active; nullptr of none
-	const CStack *stackToActivate;
-
-	/// for giving IDs for animations
-	ui32 animIDhelper;
-
-	bool stackNeedsAmountBox(const CStack * stack) const;
-	void showStackAmountBox(Canvas & canvas, const CStack * stack);
-	BattleHex getStackCurrentPosition(const CStack * stack) const;
-
-	std::shared_ptr<IImage> getStackAmountBox(const CStack * stack);
-
-	void removeExpiredColorFilters();
-
-	void initializeBattleAnimations();
-	void tickFrameBattleAnimations(uint32_t msPassed);
-
-	void updateBattleAnimations(uint32_t msPassed);
-	void updateHoveredStacks();
-
-	std::vector<const CStack *> selectHoveredStacks();
-
-	bool shouldAttackFacingRight(const CStack * attacker, const CStack * defender);
-
-public:
-	BattleStacksController(BattleInterface & owner);
-
-	bool shouldRotate(const CStack * stack, const BattleHex & oldPos, const BattleHex & nextHex) const;
-	bool facingRight(const CStack * stack) const;
-
-	void stackReset(const CStack * stack);
-	void stackAdded(const CStack * stack, bool instant); //new stack appeared on battlefield
-	void stackRemoved(uint32_t stackID); //stack disappeared from batlefiled
-	void stackActivated(const CStack *stack); //active stack has been changed
-	void stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance); //stack with id number moved to destHex
-	void stackTeleported(const CStack *stack, std::vector<BattleHex> destHex, int distance); //stack with id number moved to destHex
-	void stacksAreAttacked(std::vector<StackAttackedInfo> attackedInfos); //called when a certain amount of stacks has been attacked
-	void stackAttacking(const StackAttackInfo & info); //called when stack with id ID is attacking something on hex dest
-
-	void startAction(const BattleAction & action);
-	void endAction(const BattleAction & action);
-
-	void deactivateStack(); //copy activeStack to stackToActivate, then set activeStack to nullptr to temporary disable current stack
-
-	void activateStack(); //copy stackToActivate to activeStack to enable controls of the stack
-
-	void setActiveStack(const CStack *stack);
-
-	void showAliveStack(Canvas & canvas, const CStack * stack);
-	void showStack(Canvas & canvas, const CStack * stack);
-
-	void collectRenderableObjects(BattleRenderer & renderer);
-
-	/// Adds new color filter effect targeting stack
-	/// Effect will last as long as stack is affected by specified spell (unless effect is persistent)
-	/// If effect from same (target, source) already exists, it will be updated
-	void setStackColorFilter(const ColorFilter & effect, const CStack * target, const CSpell *source, bool persistent);
-	void addNewAnim(BattleAnimation *anim); //adds new anim to pendingAnims
-
-	const CStack* getActiveStack() const;
-	const std::vector<uint32_t> getHoveredStacksUnitIds() const;
-
-	void tick(uint32_t msPassed);
-
-	/// returns position of animation needed to place stack in specific hex
-	Point getStackPositionAtHex(BattleHex hexNum, const CStack * creature) const;
-
-	friend class BattleAnimation; // for exposing pendingAnims/creAnims/creDir to animations
-};
+/*
+ * BattleStacksController.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 "../render/ColorFilter.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+struct BattleHex;
+class BattleAction;
+class CStack;
+class CSpell;
+class SpellID;
+class Point;
+
+VCMI_LIB_NAMESPACE_END
+
+struct StackAttackedInfo;
+struct StackAttackInfo;
+
+class ColorFilter;
+class Canvas;
+class BattleInterface;
+class BattleAnimation;
+class CreatureAnimation;
+class BattleAnimation;
+class BattleRenderer;
+class IImage;
+
+struct BattleStackFilterEffect
+{
+	ColorFilter effect;
+	const CStack * target;
+	const CSpell * source;
+	bool persistent;
+};
+
+/// Class responsible for handling stacks in battle
+/// Handles ordering of stacks animation
+/// As well as rendering of stacks, their amount boxes
+/// And any other effect applied to stacks
+class BattleStacksController
+{
+	BattleInterface & owner;
+
+	std::shared_ptr<IImage> amountNormal;
+	std::shared_ptr<IImage> amountNegative;
+	std::shared_ptr<IImage> amountPositive;
+	std::shared_ptr<IImage> amountEffNeutral;
+
+	/// currently displayed animations <anim, initialized>
+	std::vector<BattleAnimation *> currentAnimations;
+
+	/// currently active color effects on stacks, in order of their addition (which corresponds to their apply order)
+	std::vector<BattleStackFilterEffect> stackFilterEffects;
+
+	/// animations of creatures from fighting armies (order by BattleInfo's stacks' ID)
+	std::map<int32_t, std::shared_ptr<CreatureAnimation>> stackAnimation;
+
+	/// <creatureID, if false reverse creature's animation> //TODO: move it to battle callback
+	std::map<int, bool> stackFacingRight;
+
+	/// Stacks have amount box hidden due to ongoing animations
+	std::set<int> stackAmountBoxHidden;
+
+	/// currently active stack; nullptr - no one
+	const CStack *activeStack;
+
+	/// stacks or their battle queue images below mouse pointer (multiple stacks possible while spellcasting), used for border animation
+	std::vector<const CStack *> mouseHoveredStacks;
+
+	///when animation is playing, we should wait till the end to make the next stack active; nullptr of none
+	const CStack *stackToActivate;
+
+	/// for giving IDs for animations
+	ui32 animIDhelper;
+
+	bool stackNeedsAmountBox(const CStack * stack) const;
+	void showStackAmountBox(Canvas & canvas, const CStack * stack);
+	BattleHex getStackCurrentPosition(const CStack * stack) const;
+
+	std::shared_ptr<IImage> getStackAmountBox(const CStack * stack);
+
+	void removeExpiredColorFilters();
+
+	void initializeBattleAnimations();
+	void tickFrameBattleAnimations(uint32_t msPassed);
+
+	void updateBattleAnimations(uint32_t msPassed);
+	void updateHoveredStacks();
+
+	std::vector<const CStack *> selectHoveredStacks();
+
+	bool shouldAttackFacingRight(const CStack * attacker, const CStack * defender);
+
+public:
+	BattleStacksController(BattleInterface & owner);
+
+	bool shouldRotate(const CStack * stack, const BattleHex & oldPos, const BattleHex & nextHex) const;
+	bool facingRight(const CStack * stack) const;
+
+	void stackReset(const CStack * stack);
+	void stackAdded(const CStack * stack, bool instant); //new stack appeared on battlefield
+	void stackRemoved(uint32_t stackID); //stack disappeared from batlefiled
+	void stackActivated(const CStack *stack); //active stack has been changed
+	void stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance); //stack with id number moved to destHex
+	void stackTeleported(const CStack *stack, std::vector<BattleHex> destHex, int distance); //stack with id number moved to destHex
+	void stacksAreAttacked(std::vector<StackAttackedInfo> attackedInfos); //called when a certain amount of stacks has been attacked
+	void stackAttacking(const StackAttackInfo & info); //called when stack with id ID is attacking something on hex dest
+
+	void startAction(const BattleAction & action);
+	void endAction(const BattleAction & action);
+
+	void deactivateStack(); //copy activeStack to stackToActivate, then set activeStack to nullptr to temporary disable current stack
+
+	void activateStack(); //copy stackToActivate to activeStack to enable controls of the stack
+
+	void setActiveStack(const CStack *stack);
+
+	void showAliveStack(Canvas & canvas, const CStack * stack);
+	void showStack(Canvas & canvas, const CStack * stack);
+
+	void collectRenderableObjects(BattleRenderer & renderer);
+
+	/// Adds new color filter effect targeting stack
+	/// Effect will last as long as stack is affected by specified spell (unless effect is persistent)
+	/// If effect from same (target, source) already exists, it will be updated
+	void setStackColorFilter(const ColorFilter & effect, const CStack * target, const CSpell *source, bool persistent);
+	void addNewAnim(BattleAnimation *anim); //adds new anim to pendingAnims
+
+	const CStack* getActiveStack() const;
+	const std::vector<uint32_t> getHoveredStacksUnitIds() const;
+
+	void tick(uint32_t msPassed);
+
+	/// returns position of animation needed to place stack in specific hex
+	Point getStackPositionAtHex(BattleHex hexNum, const CStack * creature) const;
+
+	friend class BattleAnimation; // for exposing pendingAnims/creAnims/creDir to animations
+};

+ 687 - 687
client/battle/BattleWindow.cpp

@@ -1,687 +1,687 @@
-/*
- * BattleWindow.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 "BattleWindow.h"
-
-#include "BattleInterface.h"
-#include "BattleInterfaceClasses.h"
-#include "BattleFieldController.h"
-#include "BattleStacksController.h"
-#include "BattleActionsController.h"
-
-#include "../CGameInfo.h"
-#include "../CPlayerInterface.h"
-#include "../CMusicHandler.h"
-#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"
-#include "../windows/CMessage.h"
-#include "../render/CAnimation.h"
-#include "../render/Canvas.h"
-#include "../render/IRenderHandler.h"
-#include "../adventureMap/CInGameConsole.h"
-
-#include "../../CCallback.h"
-#include "../../lib/CGeneralTextHandler.h"
-#include "../../lib/gameState/InfoAboutArmy.h"
-#include "../../lib/mapObjects/CGHeroInstance.h"
-#include "../../lib/CStack.h"
-#include "../../lib/CConfigHandler.h"
-#include "../../lib/filesystem/ResourcePath.h"
-#include "../windows/settings/SettingsMainWindow.h"
-
-BattleWindow::BattleWindow(BattleInterface & owner):
-	owner(owner),
-	defaultAction(PossiblePlayerBattleAction::INVALID)
-{
-	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
-	pos.w = 800;
-	pos.h = 600;
-	pos = center();
-
-	REGISTER_BUILDER("battleConsole", &BattleWindow::buildBattleConsole);
-	
-	const JsonNode config(JsonPath::builtin("config/widgets/BattleWindow2.json"));
-	
-	addShortcut(EShortcut::GLOBAL_OPTIONS, std::bind(&BattleWindow::bOptionsf, this));
-	addShortcut(EShortcut::BATTLE_SURRENDER, std::bind(&BattleWindow::bSurrenderf, this));
-	addShortcut(EShortcut::BATTLE_RETREAT, std::bind(&BattleWindow::bFleef, this));
-	addShortcut(EShortcut::BATTLE_AUTOCOMBAT, std::bind(&BattleWindow::bAutofightf, this));
-	addShortcut(EShortcut::BATTLE_CAST_SPELL, std::bind(&BattleWindow::bSpellf, this));
-	addShortcut(EShortcut::BATTLE_WAIT, std::bind(&BattleWindow::bWaitf, this));
-	addShortcut(EShortcut::BATTLE_DEFEND, std::bind(&BattleWindow::bDefencef, this));
-	addShortcut(EShortcut::BATTLE_CONSOLE_UP, std::bind(&BattleWindow::bConsoleUpf, this));
-	addShortcut(EShortcut::BATTLE_CONSOLE_DOWN, std::bind(&BattleWindow::bConsoleDownf, this));
-	addShortcut(EShortcut::BATTLE_TACTICS_NEXT, std::bind(&BattleWindow::bTacticNextStack, this));
-	addShortcut(EShortcut::BATTLE_TACTICS_END, std::bind(&BattleWindow::bTacticPhaseEnd, this));
-	addShortcut(EShortcut::BATTLE_SELECT_ACTION, std::bind(&BattleWindow::bSwitchActionf, this));
-
-	addShortcut(EShortcut::BATTLE_TOGGLE_QUEUE, [this](){ this->toggleQueueVisibility();});
-	addShortcut(EShortcut::BATTLE_TOGGLE_HEROES_STATS, [this](){ this->toggleStickyHeroWindowsVisibility();});
-	addShortcut(EShortcut::BATTLE_USE_CREATURE_SPELL, [this](){ this->owner.actionsController->enterCreatureCastingMode(); });
-	addShortcut(EShortcut::GLOBAL_CANCEL, [this](){ this->owner.actionsController->endCastingSpell(); });
-
-	build(config);
-	
-	console = widget<BattleConsole>("console");
-
-	owner.console = console;
-
-	owner.fieldController.reset( new BattleFieldController(owner));
-	owner.fieldController->createHeroes();
-
-	createQueue();
-	createStickyHeroInfoWindows();
-
-	if ( owner.tacticsMode )
-		tacticPhaseStarted();
-	else
-		tacticPhaseEnded();
-
-	addUsedEvents(LCLICK | KEYBOARD);
-}
-
-void BattleWindow::createQueue()
-{
-	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
-
-	//create stack queue and adjust our own position
-	bool embedQueue;
-	bool showQueue = settings["battle"]["showQueue"].Bool();
-	std::string queueSize = settings["battle"]["queueSize"].String();
-
-	if(queueSize == "auto")
-		embedQueue = GH.screenDimensions().y < 700;
-	else
-		embedQueue = GH.screenDimensions().y < 700 || queueSize == "small";
-
-	queue = std::make_shared<StackQueue>(embedQueue, owner);
-	if(!embedQueue && showQueue)
-	{
-		//re-center, taking into account stack queue position
-		pos.y -= queue->pos.h;
-		pos.h += queue->pos.h;
-		pos = center();
-	}
-
-	if (!showQueue)
-		queue->disable();
-}
-
-void BattleWindow::createStickyHeroInfoWindows()
-{
-	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
-
-	if(owner.defendingHeroInstance)
-	{
-		InfoAboutHero info;
-		info.initFromHero(owner.defendingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE);
-		Point position = (GH.screenDimensions().x >= 1000)
-				? Point(pos.x + pos.w + 15, pos.y)
-				: Point(pos.x + pos.w -79, pos.y + 135);
-		defenderHeroWindow = std::make_shared<HeroInfoBasicPanel>(info, &position);
-	}
-	if(owner.attackingHeroInstance)
-	{
-		InfoAboutHero info;
-		info.initFromHero(owner.attackingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE);
-		Point position = (GH.screenDimensions().x >= 1000)
-				? Point(pos.x - 93, pos.y)
-				: Point(pos.x + 1, pos.y + 135);
-		attackerHeroWindow = std::make_shared<HeroInfoBasicPanel>(info, &position);
-	}
-
-	bool showInfoWindows = settings["battle"]["stickyHeroInfoWindows"].Bool();
-
-	if(!showInfoWindows)
-	{
-		if(attackerHeroWindow)
-			attackerHeroWindow->disable();
-
-		if(defenderHeroWindow)
-			defenderHeroWindow->disable();
-	}
-}
-
-BattleWindow::~BattleWindow()
-{
-	CPlayerInterface::battleInt = nullptr;
-}
-
-std::shared_ptr<BattleConsole> BattleWindow::buildBattleConsole(const JsonNode & config) const
-{
-	auto rect = readRect(config["rect"]);
-	auto offset = readPosition(config["imagePosition"]);
-	auto background = widget<CPicture>("menuBattle");
-	return std::make_shared<BattleConsole>(background, rect.topLeft(), offset, rect.dimensions() );
-}
-
-void BattleWindow::toggleQueueVisibility()
-{
-	if(settings["battle"]["showQueue"].Bool())
-		hideQueue();
-	else
-		showQueue();
-}
-
-void BattleWindow::hideQueue()
-{
-	if(settings["battle"]["showQueue"].Bool() == false)
-		return;
-
-	Settings showQueue = settings.write["battle"]["showQueue"];
-	showQueue->Bool() = false;
-
-	queue->disable();
-
-	if (!queue->embedded)
-	{
-		//re-center, taking into account stack queue position
-		pos.y += queue->pos.h;
-		pos.h -= queue->pos.h;
-		pos = center();
-	}
-	GH.windows().totalRedraw();
-}
-
-void BattleWindow::showQueue()
-{
-	if(settings["battle"]["showQueue"].Bool() == true)
-		return;
-
-	Settings showQueue = settings.write["battle"]["showQueue"];
-	showQueue->Bool() = true;
-
-	createQueue();
-	updateQueue();
-	GH.windows().totalRedraw();
-}
-
-void BattleWindow::toggleStickyHeroWindowsVisibility()
-{
-	if(settings["battle"]["stickyHeroInfoWindows"].Bool())
-		hideStickyHeroWindows();
-	else
-		showStickyHeroWindows();
-}
-
-void BattleWindow::hideStickyHeroWindows()
-{
-	if(settings["battle"]["stickyHeroInfoWindows"].Bool() == false)
-		return;
-
-	Settings showStickyHeroInfoWindows = settings.write["battle"]["stickyHeroInfoWindows"];
-	showStickyHeroInfoWindows->Bool() = false;
-
-	if(attackerHeroWindow)
-		attackerHeroWindow->disable();
-
-	if(defenderHeroWindow)
-		defenderHeroWindow->disable();
-
-	GH.windows().totalRedraw();
-}
-
-void BattleWindow::showStickyHeroWindows()
-{
-	if(settings["battle"]["stickyHeroInfoWindows"].Bool() == true)
-		return;
-
-	Settings showStickyHeroInfoWindows = settings.write["battle"]["stickyHeroInfoWindows"];
-	showStickyHeroInfoWindows->Bool() = true;
-
-
-	createStickyHeroInfoWindows();
-	GH.windows().totalRedraw();
-}
-
-void BattleWindow::updateQueue()
-{
-	queue->update();
-}
-
-void BattleWindow::updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero)
-{
-	std::shared_ptr<HeroInfoBasicPanel> panelToUpdate = side == 0 ? attackerHeroWindow : defenderHeroWindow;
-	panelToUpdate->update(hero);
-}
-
-void BattleWindow::heroManaPointsChanged(const CGHeroInstance * hero)
-{
-	if(hero == owner.attackingHeroInstance || hero == owner.defendingHeroInstance)
-	{
-		InfoAboutHero heroInfo = InfoAboutHero();
-		heroInfo.initFromHero(hero, InfoAboutHero::INBATTLE);
-
-		updateHeroInfoWindow(hero == owner.attackingHeroInstance ? 0 : 1, heroInfo);
-	}
-	else
-	{
-		logGlobal->error("BattleWindow::heroManaPointsChanged: 'Mana points changed' called for hero not belonging to current battle window");
-	}
-}
-
-void BattleWindow::activate()
-{
-	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())
-	{
-		owner.openingEnd();
-		return;
-	}
-	InterfaceObjectConfigurable::keyPressed(key);
-}
-
-void BattleWindow::clickPressed(const Point & cursorPosition)
-{
-	if (owner.openingPlaying())
-	{
-		owner.openingEnd();
-		return;
-	}
-	InterfaceObjectConfigurable::clickPressed(cursorPosition);
-}
-
-void BattleWindow::tacticPhaseStarted()
-{
-	auto menuBattle = widget<CIntObject>("menuBattle");
-	auto console = widget<CIntObject>("console");
-	auto menuTactics = widget<CIntObject>("menuTactics");
-	auto tacticNext = widget<CIntObject>("tacticNext");
-	auto tacticEnd = widget<CIntObject>("tacticEnd");
-	auto alternativeAction = widget<CIntObject>("alternativeAction");
-
-	menuBattle->disable();
-	console->disable();
-	if (alternativeAction)
-		alternativeAction->disable();
-
-	menuTactics->enable();
-	tacticNext->enable();
-	tacticEnd->enable();
-
-	redraw();
-}
-
-void BattleWindow::tacticPhaseEnded()
-{
-	auto menuBattle = widget<CIntObject>("menuBattle");
-	auto console = widget<CIntObject>("console");
-	auto menuTactics = widget<CIntObject>("menuTactics");
-	auto tacticNext = widget<CIntObject>("tacticNext");
-	auto tacticEnd = widget<CIntObject>("tacticEnd");
-	auto alternativeAction = widget<CIntObject>("alternativeAction");
-
-	menuBattle->enable();
-	console->enable();
-	if (alternativeAction)
-		alternativeAction->enable();
-
-	menuTactics->disable();
-	tacticNext->disable();
-	tacticEnd->disable();
-
-	redraw();
-}
-
-void BattleWindow::bOptionsf()
-{
-	if (owner.actionsController->spellcastingModeActive())
-		return;
-
-	CCS->curh->set(Cursor::Map::POINTER);
-
-	GH.windows().createAndPushWindow<SettingsMainWindow>(&owner);
-}
-
-void BattleWindow::bSurrenderf()
-{
-	if (owner.actionsController->spellcastingModeActive())
-		return;
-
-	int cost = owner.getBattle()->battleGetSurrenderCost();
-	if(cost >= 0)
-	{
-		std::string enemyHeroName = owner.getBattle()->battleGetEnemyHero().name;
-		if(enemyHeroName.empty())
-		{
-			logGlobal->warn("Surrender performed without enemy hero, should not happen!");
-			enemyHeroName = "#ENEMY#";
-		}
-
-		std::string surrenderMessage = boost::str(boost::format(CGI->generaltexth->allTexts[32]) % enemyHeroName % cost); //%s states: "I will accept your surrender and grant you and your troops safe passage for the price of %d gold."
-		owner.curInt->showYesNoDialog(surrenderMessage, [this](){ reallySurrender(); }, nullptr);
-	}
-}
-
-void BattleWindow::bFleef()
-{
-	if (owner.actionsController->spellcastingModeActive())
-		return;
-
-	if ( owner.getBattle()->battleCanFlee() )
-	{
-		CFunctionList<void()> ony = std::bind(&BattleWindow::reallyFlee,this);
-		owner.curInt->showYesNoDialog(CGI->generaltexth->allTexts[28], ony, nullptr); //Are you sure you want to retreat?
-	}
-	else
-	{
-		std::vector<std::shared_ptr<CComponent>> comps;
-		std::string heroName;
-		//calculating fleeing hero's name
-		if (owner.attackingHeroInstance)
-			if (owner.attackingHeroInstance->tempOwner == owner.curInt->cb->getPlayerID())
-				heroName = owner.attackingHeroInstance->getNameTranslated();
-		if (owner.defendingHeroInstance)
-			if (owner.defendingHeroInstance->tempOwner == owner.curInt->cb->getPlayerID())
-				heroName = owner.defendingHeroInstance->getNameTranslated();
-		//calculating text
-		auto txt = boost::format(CGI->generaltexth->allTexts[340]) % heroName; //The Shackles of War are present.  %s can not retreat!
-
-		//printing message
-		owner.curInt->showInfoDialog(boost::str(txt), comps);
-	}
-}
-
-void BattleWindow::reallyFlee()
-{
-	owner.giveCommand(EActionType::RETREAT);
-	CCS->curh->set(Cursor::Map::POINTER);
-}
-
-void BattleWindow::reallySurrender()
-{
-	if (owner.curInt->cb->getResourceAmount(EGameResID::GOLD) < owner.getBattle()->battleGetSurrenderCost())
-	{
-		owner.curInt->showInfoDialog(CGI->generaltexth->allTexts[29]); //You don't have enough gold!
-	}
-	else
-	{
-		owner.giveCommand(EActionType::SURRENDER);
-		CCS->curh->set(Cursor::Map::POINTER);
-	}
-}
-
-void BattleWindow::showAlternativeActionIcon(PossiblePlayerBattleAction action)
-{
-	auto w = widget<CButton>("alternativeAction");
-	if(!w)
-		return;
-	
-	AnimationPath iconName = AnimationPath::fromJson(variables["actionIconDefault"]);
-	switch(action.get())
-	{
-		case PossiblePlayerBattleAction::ATTACK:
-			iconName = AnimationPath::fromJson(variables["actionIconAttack"]);
-			break;
-			
-		case PossiblePlayerBattleAction::SHOOT:
-			iconName = AnimationPath::fromJson(variables["actionIconShoot"]);
-			break;
-			
-		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
-			iconName = AnimationPath::fromJson(variables["actionIconSpell"]);
-			break;
-
-		case PossiblePlayerBattleAction::ANY_LOCATION:
-			iconName = AnimationPath::fromJson(variables["actionIconSpell"]);
-			break;
-			
-		//TODO: figure out purpose of this icon
-		//case PossiblePlayerBattleAction::???:
-			//iconName = variables["actionIconWalk"].String();
-			//break;
-			
-		case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
-			iconName = AnimationPath::fromJson(variables["actionIconReturn"]);
-			break;
-			
-		case PossiblePlayerBattleAction::WALK_AND_ATTACK:
-			iconName = AnimationPath::fromJson(variables["actionIconNoReturn"]);
-			break;
-	}
-		
-	auto anim = GH.renderHandler().loadAnimation(iconName);
-	w->setImage(anim);
-	w->redraw();
-}
-
-void BattleWindow::setAlternativeActions(const std::list<PossiblePlayerBattleAction> & actions)
-{
-	alternativeActions = actions;
-	defaultAction = PossiblePlayerBattleAction::INVALID;
-	if(alternativeActions.size() > 1)
-		defaultAction = alternativeActions.back();
-	if(!alternativeActions.empty())
-		showAlternativeActionIcon(alternativeActions.front());
-	else
-		showAlternativeActionIcon(defaultAction);
-}
-
-void BattleWindow::bAutofightf()
-{
-	if (owner.actionsController->spellcastingModeActive())
-		return;
-
-	//Stop auto-fight mode
-	if(owner.curInt->isAutoFightOn)
-	{
-		assert(owner.curInt->autofightingAI);
-		owner.curInt->isAutoFightOn = false;
-		logGlobal->trace("Stopping the autofight...");
-	}
-	else if(!owner.curInt->autofightingAI)
-	{
-		owner.curInt->isAutoFightOn = true;
-		blockUI(true);
-
-		auto ai = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String());
-
-		AutocombatPreferences autocombatPreferences = AutocombatPreferences();
-		autocombatPreferences.enableSpellsUsage = settings["battle"]["enableAutocombatSpells"].Bool();
-
-		ai->initBattleInterface(owner.curInt->env, owner.curInt->cb, autocombatPreferences);
-		ai->battleStart(owner.getBattleID(), owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.getBattle()->battleGetMySide(), false);
-		owner.curInt->autofightingAI = ai;
-		owner.curInt->cb->registerBattleInterface(ai);
-
-		owner.requestAutofightingAIToTakeAction();
-	}
-}
-
-void BattleWindow::bSpellf()
-{
-	if (owner.actionsController->spellcastingModeActive())
-		return;
-
-	if (!owner.makingTurn())
-		return;
-
-	auto myHero = owner.currentHero();
-	if(!myHero)
-		return;
-
-	CCS->curh->set(Cursor::Map::POINTER);
-
-	ESpellCastProblem spellCastProblem = owner.getBattle()->battleCanCastSpell(myHero, spells::Mode::HERO);
-
-	if(spellCastProblem == ESpellCastProblem::OK)
-	{
-		GH.windows().createAndPushWindow<CSpellWindow>(myHero, owner.curInt.get());
-	}
-	else if (spellCastProblem == ESpellCastProblem::MAGIC_IS_BLOCKED)
-	{
-		//TODO: move to spell mechanics, add more information to spell cast problem
-		//Handle Orb of Inhibition-like effects -> we want to display dialog with info, why casting is impossible
-		auto blockingBonus = owner.currentHero()->getBonusLocalFirst(Selector::type()(BonusType::BLOCK_ALL_MAGIC));
-		if (!blockingBonus)
-			return;
-
-		if (blockingBonus->source == BonusSource::ARTIFACT)
-		{
-			const auto artID = blockingBonus->sid.as<ArtifactID>();
-			//If we have artifact, put name of our hero. Otherwise assume it's the enemy.
-			//TODO check who *really* is source of bonus
-			std::string heroName = myHero->hasArt(artID) ? myHero->getNameTranslated() : owner.enemyHero().name;
-
-			//%s wields the %s, an ancient artifact which creates a p dead to all magic.
-			LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[683])
-										% heroName % CGI->artifacts()->getByIndex(artID)->getNameTranslated()));
-		}
-	}
-}
-
-void BattleWindow::bSwitchActionf()
-{
-	if(alternativeActions.empty())
-		return;
-	
-	if(alternativeActions.front() == defaultAction)
-	{
-		alternativeActions.push_back(alternativeActions.front());
-		alternativeActions.pop_front();
-	}
-	
-	auto actions = owner.actionsController->getPossibleActions();
-	if(!actions.empty() && actions.front() == alternativeActions.front())
-	{
-		owner.actionsController->removePossibleAction(alternativeActions.front());
-		showAlternativeActionIcon(defaultAction);
-	}
-	else
-	{
-		owner.actionsController->pushFrontPossibleAction(alternativeActions.front());
-		showAlternativeActionIcon(alternativeActions.front());
-	}
-	
-	alternativeActions.push_back(alternativeActions.front());
-	alternativeActions.pop_front();
-}
-
-void BattleWindow::bWaitf()
-{
-	if (owner.actionsController->spellcastingModeActive())
-		return;
-
-	if (owner.stacksController->getActiveStack() != nullptr)
-		owner.giveCommand(EActionType::WAIT);
-}
-
-void BattleWindow::bDefencef()
-{
-	if (owner.actionsController->spellcastingModeActive())
-		return;
-
-	if (owner.stacksController->getActiveStack() != nullptr)
-		owner.giveCommand(EActionType::DEFEND);
-}
-
-void BattleWindow::bConsoleUpf()
-{
-	if (owner.actionsController->spellcastingModeActive())
-		return;
-
-	console->scrollUp();
-}
-
-void BattleWindow::bConsoleDownf()
-{
-	if (owner.actionsController->spellcastingModeActive())
-		return;
-
-	console->scrollDown();
-}
-
-void BattleWindow::bTacticNextStack()
-{
-	owner.tacticNextStack(nullptr);
-}
-
-void BattleWindow::bTacticPhaseEnd()
-{
-	owner.tacticPhaseEnd();
-}
-
-void BattleWindow::blockUI(bool on)
-{
-	bool canCastSpells = false;
-	auto hero = owner.getBattle()->battleGetMyHero();
-
-	if(hero)
-	{
-		ESpellCastProblem spellcastingProblem = owner.getBattle()->battleCanCastSpell(hero, spells::Mode::HERO);
-
-		//if magic is blocked, we leave button active, so the message can be displayed after button click
-		canCastSpells = spellcastingProblem == ESpellCastProblem::OK || spellcastingProblem == ESpellCastProblem::MAGIC_IS_BLOCKED;
-	}
-
-	bool canWait = owner.stacksController->getActiveStack() ? !owner.stacksController->getActiveStack()->waitedThisTurn : false;
-
-	setShortcutBlocked(EShortcut::GLOBAL_OPTIONS, on);
-	setShortcutBlocked(EShortcut::BATTLE_RETREAT, on || !owner.getBattle()->battleCanFlee());
-	setShortcutBlocked(EShortcut::BATTLE_SURRENDER, on || owner.getBattle()->battleGetSurrenderCost() < 0);
-	setShortcutBlocked(EShortcut::BATTLE_CAST_SPELL, on || owner.tacticsMode || !canCastSpells);
-	setShortcutBlocked(EShortcut::BATTLE_WAIT, on || owner.tacticsMode || !canWait);
-	setShortcutBlocked(EShortcut::BATTLE_DEFEND, on || owner.tacticsMode);
-	setShortcutBlocked(EShortcut::BATTLE_SELECT_ACTION, on || owner.tacticsMode);
-	setShortcutBlocked(EShortcut::BATTLE_AUTOCOMBAT, owner.actionsController->spellcastingModeActive());
-	setShortcutBlocked(EShortcut::BATTLE_TACTICS_END, on && owner.tacticsMode);
-	setShortcutBlocked(EShortcut::BATTLE_TACTICS_NEXT, on && owner.tacticsMode);
-	setShortcutBlocked(EShortcut::BATTLE_CONSOLE_DOWN, on && !owner.tacticsMode);
-	setShortcutBlocked(EShortcut::BATTLE_CONSOLE_UP, on && !owner.tacticsMode);
-}
-
-std::optional<uint32_t> BattleWindow::getQueueHoveredUnitId()
-{
-	return queue->getHoveredUnitIdIfAny();
-}
-
-void BattleWindow::showAll(Canvas & to)
-{
-	CIntObject::showAll(to);
-
-	if (GH.screenDimensions().x != 800 || GH.screenDimensions().y !=600)
-		CMessage::drawBorder(owner.curInt->playerID, to.getInternalSurface(), pos.w+28, pos.h+29, pos.x-14, pos.y-15);
-}
-
-void BattleWindow::show(Canvas & to)
-{
-	CIntObject::show(to);
-	LOCPLINT->cingconsole->show(to);
-}
-
-void BattleWindow::close()
-{
-	if(!GH.windows().isTopWindow(this))
-		logGlobal->error("Only top interface must be closed");
-	GH.windows().popWindows(1);
-}
+/*
+ * BattleWindow.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 "BattleWindow.h"
+
+#include "BattleInterface.h"
+#include "BattleInterfaceClasses.h"
+#include "BattleFieldController.h"
+#include "BattleStacksController.h"
+#include "BattleActionsController.h"
+
+#include "../CGameInfo.h"
+#include "../CPlayerInterface.h"
+#include "../CMusicHandler.h"
+#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"
+#include "../windows/CMessage.h"
+#include "../render/CAnimation.h"
+#include "../render/Canvas.h"
+#include "../render/IRenderHandler.h"
+#include "../adventureMap/CInGameConsole.h"
+
+#include "../../CCallback.h"
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/gameState/InfoAboutArmy.h"
+#include "../../lib/mapObjects/CGHeroInstance.h"
+#include "../../lib/CStack.h"
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/filesystem/ResourcePath.h"
+#include "../windows/settings/SettingsMainWindow.h"
+
+BattleWindow::BattleWindow(BattleInterface & owner):
+	owner(owner),
+	defaultAction(PossiblePlayerBattleAction::INVALID)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	pos.w = 800;
+	pos.h = 600;
+	pos = center();
+
+	REGISTER_BUILDER("battleConsole", &BattleWindow::buildBattleConsole);
+	
+	const JsonNode config(JsonPath::builtin("config/widgets/BattleWindow2.json"));
+	
+	addShortcut(EShortcut::GLOBAL_OPTIONS, std::bind(&BattleWindow::bOptionsf, this));
+	addShortcut(EShortcut::BATTLE_SURRENDER, std::bind(&BattleWindow::bSurrenderf, this));
+	addShortcut(EShortcut::BATTLE_RETREAT, std::bind(&BattleWindow::bFleef, this));
+	addShortcut(EShortcut::BATTLE_AUTOCOMBAT, std::bind(&BattleWindow::bAutofightf, this));
+	addShortcut(EShortcut::BATTLE_CAST_SPELL, std::bind(&BattleWindow::bSpellf, this));
+	addShortcut(EShortcut::BATTLE_WAIT, std::bind(&BattleWindow::bWaitf, this));
+	addShortcut(EShortcut::BATTLE_DEFEND, std::bind(&BattleWindow::bDefencef, this));
+	addShortcut(EShortcut::BATTLE_CONSOLE_UP, std::bind(&BattleWindow::bConsoleUpf, this));
+	addShortcut(EShortcut::BATTLE_CONSOLE_DOWN, std::bind(&BattleWindow::bConsoleDownf, this));
+	addShortcut(EShortcut::BATTLE_TACTICS_NEXT, std::bind(&BattleWindow::bTacticNextStack, this));
+	addShortcut(EShortcut::BATTLE_TACTICS_END, std::bind(&BattleWindow::bTacticPhaseEnd, this));
+	addShortcut(EShortcut::BATTLE_SELECT_ACTION, std::bind(&BattleWindow::bSwitchActionf, this));
+
+	addShortcut(EShortcut::BATTLE_TOGGLE_QUEUE, [this](){ this->toggleQueueVisibility();});
+	addShortcut(EShortcut::BATTLE_TOGGLE_HEROES_STATS, [this](){ this->toggleStickyHeroWindowsVisibility();});
+	addShortcut(EShortcut::BATTLE_USE_CREATURE_SPELL, [this](){ this->owner.actionsController->enterCreatureCastingMode(); });
+	addShortcut(EShortcut::GLOBAL_CANCEL, [this](){ this->owner.actionsController->endCastingSpell(); });
+
+	build(config);
+	
+	console = widget<BattleConsole>("console");
+
+	owner.console = console;
+
+	owner.fieldController.reset( new BattleFieldController(owner));
+	owner.fieldController->createHeroes();
+
+	createQueue();
+	createStickyHeroInfoWindows();
+
+	if ( owner.tacticsMode )
+		tacticPhaseStarted();
+	else
+		tacticPhaseEnded();
+
+	addUsedEvents(LCLICK | KEYBOARD);
+}
+
+void BattleWindow::createQueue()
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
+	//create stack queue and adjust our own position
+	bool embedQueue;
+	bool showQueue = settings["battle"]["showQueue"].Bool();
+	std::string queueSize = settings["battle"]["queueSize"].String();
+
+	if(queueSize == "auto")
+		embedQueue = GH.screenDimensions().y < 700;
+	else
+		embedQueue = GH.screenDimensions().y < 700 || queueSize == "small";
+
+	queue = std::make_shared<StackQueue>(embedQueue, owner);
+	if(!embedQueue && showQueue)
+	{
+		//re-center, taking into account stack queue position
+		pos.y -= queue->pos.h;
+		pos.h += queue->pos.h;
+		pos = center();
+	}
+
+	if (!showQueue)
+		queue->disable();
+}
+
+void BattleWindow::createStickyHeroInfoWindows()
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
+	if(owner.defendingHeroInstance)
+	{
+		InfoAboutHero info;
+		info.initFromHero(owner.defendingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE);
+		Point position = (GH.screenDimensions().x >= 1000)
+				? Point(pos.x + pos.w + 15, pos.y)
+				: Point(pos.x + pos.w -79, pos.y + 135);
+		defenderHeroWindow = std::make_shared<HeroInfoBasicPanel>(info, &position);
+	}
+	if(owner.attackingHeroInstance)
+	{
+		InfoAboutHero info;
+		info.initFromHero(owner.attackingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE);
+		Point position = (GH.screenDimensions().x >= 1000)
+				? Point(pos.x - 93, pos.y)
+				: Point(pos.x + 1, pos.y + 135);
+		attackerHeroWindow = std::make_shared<HeroInfoBasicPanel>(info, &position);
+	}
+
+	bool showInfoWindows = settings["battle"]["stickyHeroInfoWindows"].Bool();
+
+	if(!showInfoWindows)
+	{
+		if(attackerHeroWindow)
+			attackerHeroWindow->disable();
+
+		if(defenderHeroWindow)
+			defenderHeroWindow->disable();
+	}
+}
+
+BattleWindow::~BattleWindow()
+{
+	CPlayerInterface::battleInt = nullptr;
+}
+
+std::shared_ptr<BattleConsole> BattleWindow::buildBattleConsole(const JsonNode & config) const
+{
+	auto rect = readRect(config["rect"]);
+	auto offset = readPosition(config["imagePosition"]);
+	auto background = widget<CPicture>("menuBattle");
+	return std::make_shared<BattleConsole>(background, rect.topLeft(), offset, rect.dimensions() );
+}
+
+void BattleWindow::toggleQueueVisibility()
+{
+	if(settings["battle"]["showQueue"].Bool())
+		hideQueue();
+	else
+		showQueue();
+}
+
+void BattleWindow::hideQueue()
+{
+	if(settings["battle"]["showQueue"].Bool() == false)
+		return;
+
+	Settings showQueue = settings.write["battle"]["showQueue"];
+	showQueue->Bool() = false;
+
+	queue->disable();
+
+	if (!queue->embedded)
+	{
+		//re-center, taking into account stack queue position
+		pos.y += queue->pos.h;
+		pos.h -= queue->pos.h;
+		pos = center();
+	}
+	GH.windows().totalRedraw();
+}
+
+void BattleWindow::showQueue()
+{
+	if(settings["battle"]["showQueue"].Bool() == true)
+		return;
+
+	Settings showQueue = settings.write["battle"]["showQueue"];
+	showQueue->Bool() = true;
+
+	createQueue();
+	updateQueue();
+	GH.windows().totalRedraw();
+}
+
+void BattleWindow::toggleStickyHeroWindowsVisibility()
+{
+	if(settings["battle"]["stickyHeroInfoWindows"].Bool())
+		hideStickyHeroWindows();
+	else
+		showStickyHeroWindows();
+}
+
+void BattleWindow::hideStickyHeroWindows()
+{
+	if(settings["battle"]["stickyHeroInfoWindows"].Bool() == false)
+		return;
+
+	Settings showStickyHeroInfoWindows = settings.write["battle"]["stickyHeroInfoWindows"];
+	showStickyHeroInfoWindows->Bool() = false;
+
+	if(attackerHeroWindow)
+		attackerHeroWindow->disable();
+
+	if(defenderHeroWindow)
+		defenderHeroWindow->disable();
+
+	GH.windows().totalRedraw();
+}
+
+void BattleWindow::showStickyHeroWindows()
+{
+	if(settings["battle"]["stickyHeroInfoWindows"].Bool() == true)
+		return;
+
+	Settings showStickyHeroInfoWindows = settings.write["battle"]["stickyHeroInfoWindows"];
+	showStickyHeroInfoWindows->Bool() = true;
+
+
+	createStickyHeroInfoWindows();
+	GH.windows().totalRedraw();
+}
+
+void BattleWindow::updateQueue()
+{
+	queue->update();
+}
+
+void BattleWindow::updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero)
+{
+	std::shared_ptr<HeroInfoBasicPanel> panelToUpdate = side == 0 ? attackerHeroWindow : defenderHeroWindow;
+	panelToUpdate->update(hero);
+}
+
+void BattleWindow::heroManaPointsChanged(const CGHeroInstance * hero)
+{
+	if(hero == owner.attackingHeroInstance || hero == owner.defendingHeroInstance)
+	{
+		InfoAboutHero heroInfo = InfoAboutHero();
+		heroInfo.initFromHero(hero, InfoAboutHero::INBATTLE);
+
+		updateHeroInfoWindow(hero == owner.attackingHeroInstance ? 0 : 1, heroInfo);
+	}
+	else
+	{
+		logGlobal->error("BattleWindow::heroManaPointsChanged: 'Mana points changed' called for hero not belonging to current battle window");
+	}
+}
+
+void BattleWindow::activate()
+{
+	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())
+	{
+		owner.openingEnd();
+		return;
+	}
+	InterfaceObjectConfigurable::keyPressed(key);
+}
+
+void BattleWindow::clickPressed(const Point & cursorPosition)
+{
+	if (owner.openingPlaying())
+	{
+		owner.openingEnd();
+		return;
+	}
+	InterfaceObjectConfigurable::clickPressed(cursorPosition);
+}
+
+void BattleWindow::tacticPhaseStarted()
+{
+	auto menuBattle = widget<CIntObject>("menuBattle");
+	auto console = widget<CIntObject>("console");
+	auto menuTactics = widget<CIntObject>("menuTactics");
+	auto tacticNext = widget<CIntObject>("tacticNext");
+	auto tacticEnd = widget<CIntObject>("tacticEnd");
+	auto alternativeAction = widget<CIntObject>("alternativeAction");
+
+	menuBattle->disable();
+	console->disable();
+	if (alternativeAction)
+		alternativeAction->disable();
+
+	menuTactics->enable();
+	tacticNext->enable();
+	tacticEnd->enable();
+
+	redraw();
+}
+
+void BattleWindow::tacticPhaseEnded()
+{
+	auto menuBattle = widget<CIntObject>("menuBattle");
+	auto console = widget<CIntObject>("console");
+	auto menuTactics = widget<CIntObject>("menuTactics");
+	auto tacticNext = widget<CIntObject>("tacticNext");
+	auto tacticEnd = widget<CIntObject>("tacticEnd");
+	auto alternativeAction = widget<CIntObject>("alternativeAction");
+
+	menuBattle->enable();
+	console->enable();
+	if (alternativeAction)
+		alternativeAction->enable();
+
+	menuTactics->disable();
+	tacticNext->disable();
+	tacticEnd->disable();
+
+	redraw();
+}
+
+void BattleWindow::bOptionsf()
+{
+	if (owner.actionsController->spellcastingModeActive())
+		return;
+
+	CCS->curh->set(Cursor::Map::POINTER);
+
+	GH.windows().createAndPushWindow<SettingsMainWindow>(&owner);
+}
+
+void BattleWindow::bSurrenderf()
+{
+	if (owner.actionsController->spellcastingModeActive())
+		return;
+
+	int cost = owner.getBattle()->battleGetSurrenderCost();
+	if(cost >= 0)
+	{
+		std::string enemyHeroName = owner.getBattle()->battleGetEnemyHero().name;
+		if(enemyHeroName.empty())
+		{
+			logGlobal->warn("Surrender performed without enemy hero, should not happen!");
+			enemyHeroName = "#ENEMY#";
+		}
+
+		std::string surrenderMessage = boost::str(boost::format(CGI->generaltexth->allTexts[32]) % enemyHeroName % cost); //%s states: "I will accept your surrender and grant you and your troops safe passage for the price of %d gold."
+		owner.curInt->showYesNoDialog(surrenderMessage, [this](){ reallySurrender(); }, nullptr);
+	}
+}
+
+void BattleWindow::bFleef()
+{
+	if (owner.actionsController->spellcastingModeActive())
+		return;
+
+	if ( owner.getBattle()->battleCanFlee() )
+	{
+		CFunctionList<void()> ony = std::bind(&BattleWindow::reallyFlee,this);
+		owner.curInt->showYesNoDialog(CGI->generaltexth->allTexts[28], ony, nullptr); //Are you sure you want to retreat?
+	}
+	else
+	{
+		std::vector<std::shared_ptr<CComponent>> comps;
+		std::string heroName;
+		//calculating fleeing hero's name
+		if (owner.attackingHeroInstance)
+			if (owner.attackingHeroInstance->tempOwner == owner.curInt->cb->getPlayerID())
+				heroName = owner.attackingHeroInstance->getNameTranslated();
+		if (owner.defendingHeroInstance)
+			if (owner.defendingHeroInstance->tempOwner == owner.curInt->cb->getPlayerID())
+				heroName = owner.defendingHeroInstance->getNameTranslated();
+		//calculating text
+		auto txt = boost::format(CGI->generaltexth->allTexts[340]) % heroName; //The Shackles of War are present.  %s can not retreat!
+
+		//printing message
+		owner.curInt->showInfoDialog(boost::str(txt), comps);
+	}
+}
+
+void BattleWindow::reallyFlee()
+{
+	owner.giveCommand(EActionType::RETREAT);
+	CCS->curh->set(Cursor::Map::POINTER);
+}
+
+void BattleWindow::reallySurrender()
+{
+	if (owner.curInt->cb->getResourceAmount(EGameResID::GOLD) < owner.getBattle()->battleGetSurrenderCost())
+	{
+		owner.curInt->showInfoDialog(CGI->generaltexth->allTexts[29]); //You don't have enough gold!
+	}
+	else
+	{
+		owner.giveCommand(EActionType::SURRENDER);
+		CCS->curh->set(Cursor::Map::POINTER);
+	}
+}
+
+void BattleWindow::showAlternativeActionIcon(PossiblePlayerBattleAction action)
+{
+	auto w = widget<CButton>("alternativeAction");
+	if(!w)
+		return;
+	
+	AnimationPath iconName = AnimationPath::fromJson(variables["actionIconDefault"]);
+	switch(action.get())
+	{
+		case PossiblePlayerBattleAction::ATTACK:
+			iconName = AnimationPath::fromJson(variables["actionIconAttack"]);
+			break;
+			
+		case PossiblePlayerBattleAction::SHOOT:
+			iconName = AnimationPath::fromJson(variables["actionIconShoot"]);
+			break;
+			
+		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
+			iconName = AnimationPath::fromJson(variables["actionIconSpell"]);
+			break;
+
+		case PossiblePlayerBattleAction::ANY_LOCATION:
+			iconName = AnimationPath::fromJson(variables["actionIconSpell"]);
+			break;
+			
+		//TODO: figure out purpose of this icon
+		//case PossiblePlayerBattleAction::???:
+			//iconName = variables["actionIconWalk"].String();
+			//break;
+			
+		case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
+			iconName = AnimationPath::fromJson(variables["actionIconReturn"]);
+			break;
+			
+		case PossiblePlayerBattleAction::WALK_AND_ATTACK:
+			iconName = AnimationPath::fromJson(variables["actionIconNoReturn"]);
+			break;
+	}
+		
+	auto anim = GH.renderHandler().loadAnimation(iconName);
+	w->setImage(anim);
+	w->redraw();
+}
+
+void BattleWindow::setAlternativeActions(const std::list<PossiblePlayerBattleAction> & actions)
+{
+	alternativeActions = actions;
+	defaultAction = PossiblePlayerBattleAction::INVALID;
+	if(alternativeActions.size() > 1)
+		defaultAction = alternativeActions.back();
+	if(!alternativeActions.empty())
+		showAlternativeActionIcon(alternativeActions.front());
+	else
+		showAlternativeActionIcon(defaultAction);
+}
+
+void BattleWindow::bAutofightf()
+{
+	if (owner.actionsController->spellcastingModeActive())
+		return;
+
+	//Stop auto-fight mode
+	if(owner.curInt->isAutoFightOn)
+	{
+		assert(owner.curInt->autofightingAI);
+		owner.curInt->isAutoFightOn = false;
+		logGlobal->trace("Stopping the autofight...");
+	}
+	else if(!owner.curInt->autofightingAI)
+	{
+		owner.curInt->isAutoFightOn = true;
+		blockUI(true);
+
+		auto ai = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String());
+
+		AutocombatPreferences autocombatPreferences = AutocombatPreferences();
+		autocombatPreferences.enableSpellsUsage = settings["battle"]["enableAutocombatSpells"].Bool();
+
+		ai->initBattleInterface(owner.curInt->env, owner.curInt->cb, autocombatPreferences);
+		ai->battleStart(owner.getBattleID(), owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.getBattle()->battleGetMySide(), false);
+		owner.curInt->autofightingAI = ai;
+		owner.curInt->cb->registerBattleInterface(ai);
+
+		owner.requestAutofightingAIToTakeAction();
+	}
+}
+
+void BattleWindow::bSpellf()
+{
+	if (owner.actionsController->spellcastingModeActive())
+		return;
+
+	if (!owner.makingTurn())
+		return;
+
+	auto myHero = owner.currentHero();
+	if(!myHero)
+		return;
+
+	CCS->curh->set(Cursor::Map::POINTER);
+
+	ESpellCastProblem spellCastProblem = owner.getBattle()->battleCanCastSpell(myHero, spells::Mode::HERO);
+
+	if(spellCastProblem == ESpellCastProblem::OK)
+	{
+		GH.windows().createAndPushWindow<CSpellWindow>(myHero, owner.curInt.get());
+	}
+	else if (spellCastProblem == ESpellCastProblem::MAGIC_IS_BLOCKED)
+	{
+		//TODO: move to spell mechanics, add more information to spell cast problem
+		//Handle Orb of Inhibition-like effects -> we want to display dialog with info, why casting is impossible
+		auto blockingBonus = owner.currentHero()->getBonusLocalFirst(Selector::type()(BonusType::BLOCK_ALL_MAGIC));
+		if (!blockingBonus)
+			return;
+
+		if (blockingBonus->source == BonusSource::ARTIFACT)
+		{
+			const auto artID = blockingBonus->sid.as<ArtifactID>();
+			//If we have artifact, put name of our hero. Otherwise assume it's the enemy.
+			//TODO check who *really* is source of bonus
+			std::string heroName = myHero->hasArt(artID) ? myHero->getNameTranslated() : owner.enemyHero().name;
+
+			//%s wields the %s, an ancient artifact which creates a p dead to all magic.
+			LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[683])
+										% heroName % CGI->artifacts()->getByIndex(artID)->getNameTranslated()));
+		}
+	}
+}
+
+void BattleWindow::bSwitchActionf()
+{
+	if(alternativeActions.empty())
+		return;
+	
+	if(alternativeActions.front() == defaultAction)
+	{
+		alternativeActions.push_back(alternativeActions.front());
+		alternativeActions.pop_front();
+	}
+	
+	auto actions = owner.actionsController->getPossibleActions();
+	if(!actions.empty() && actions.front() == alternativeActions.front())
+	{
+		owner.actionsController->removePossibleAction(alternativeActions.front());
+		showAlternativeActionIcon(defaultAction);
+	}
+	else
+	{
+		owner.actionsController->pushFrontPossibleAction(alternativeActions.front());
+		showAlternativeActionIcon(alternativeActions.front());
+	}
+	
+	alternativeActions.push_back(alternativeActions.front());
+	alternativeActions.pop_front();
+}
+
+void BattleWindow::bWaitf()
+{
+	if (owner.actionsController->spellcastingModeActive())
+		return;
+
+	if (owner.stacksController->getActiveStack() != nullptr)
+		owner.giveCommand(EActionType::WAIT);
+}
+
+void BattleWindow::bDefencef()
+{
+	if (owner.actionsController->spellcastingModeActive())
+		return;
+
+	if (owner.stacksController->getActiveStack() != nullptr)
+		owner.giveCommand(EActionType::DEFEND);
+}
+
+void BattleWindow::bConsoleUpf()
+{
+	if (owner.actionsController->spellcastingModeActive())
+		return;
+
+	console->scrollUp();
+}
+
+void BattleWindow::bConsoleDownf()
+{
+	if (owner.actionsController->spellcastingModeActive())
+		return;
+
+	console->scrollDown();
+}
+
+void BattleWindow::bTacticNextStack()
+{
+	owner.tacticNextStack(nullptr);
+}
+
+void BattleWindow::bTacticPhaseEnd()
+{
+	owner.tacticPhaseEnd();
+}
+
+void BattleWindow::blockUI(bool on)
+{
+	bool canCastSpells = false;
+	auto hero = owner.getBattle()->battleGetMyHero();
+
+	if(hero)
+	{
+		ESpellCastProblem spellcastingProblem = owner.getBattle()->battleCanCastSpell(hero, spells::Mode::HERO);
+
+		//if magic is blocked, we leave button active, so the message can be displayed after button click
+		canCastSpells = spellcastingProblem == ESpellCastProblem::OK || spellcastingProblem == ESpellCastProblem::MAGIC_IS_BLOCKED;
+	}
+
+	bool canWait = owner.stacksController->getActiveStack() ? !owner.stacksController->getActiveStack()->waitedThisTurn : false;
+
+	setShortcutBlocked(EShortcut::GLOBAL_OPTIONS, on);
+	setShortcutBlocked(EShortcut::BATTLE_RETREAT, on || !owner.getBattle()->battleCanFlee());
+	setShortcutBlocked(EShortcut::BATTLE_SURRENDER, on || owner.getBattle()->battleGetSurrenderCost() < 0);
+	setShortcutBlocked(EShortcut::BATTLE_CAST_SPELL, on || owner.tacticsMode || !canCastSpells);
+	setShortcutBlocked(EShortcut::BATTLE_WAIT, on || owner.tacticsMode || !canWait);
+	setShortcutBlocked(EShortcut::BATTLE_DEFEND, on || owner.tacticsMode);
+	setShortcutBlocked(EShortcut::BATTLE_SELECT_ACTION, on || owner.tacticsMode);
+	setShortcutBlocked(EShortcut::BATTLE_AUTOCOMBAT, owner.actionsController->spellcastingModeActive());
+	setShortcutBlocked(EShortcut::BATTLE_TACTICS_END, on && owner.tacticsMode);
+	setShortcutBlocked(EShortcut::BATTLE_TACTICS_NEXT, on && owner.tacticsMode);
+	setShortcutBlocked(EShortcut::BATTLE_CONSOLE_DOWN, on && !owner.tacticsMode);
+	setShortcutBlocked(EShortcut::BATTLE_CONSOLE_UP, on && !owner.tacticsMode);
+}
+
+std::optional<uint32_t> BattleWindow::getQueueHoveredUnitId()
+{
+	return queue->getHoveredUnitIdIfAny();
+}
+
+void BattleWindow::showAll(Canvas & to)
+{
+	CIntObject::showAll(to);
+
+	if (GH.screenDimensions().x != 800 || GH.screenDimensions().y !=600)
+		CMessage::drawBorder(owner.curInt->playerID, to.getInternalSurface(), pos.w+28, pos.h+29, pos.x-14, pos.y-15);
+}
+
+void BattleWindow::show(Canvas & to)
+{
+	CIntObject::show(to);
+	LOCPLINT->cingconsole->show(to);
+}
+
+void BattleWindow::close()
+{
+	if(!GH.windows().isTopWindow(this))
+		logGlobal->error("Only top interface must be closed");
+	GH.windows().popWindows(1);
+}

+ 118 - 118
client/battle/BattleWindow.h

@@ -1,118 +1,118 @@
-/*
- * BattleWindow.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"
-#include "../gui/InterfaceObjectConfigurable.h"
-#include "../../lib/battle/CBattleInfoCallback.h"
-#include "../../lib/battle/PossiblePlayerBattleAction.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-class CStack;
-
-VCMI_LIB_NAMESPACE_END
-
-class CButton;
-class BattleInterface;
-class BattleConsole;
-class BattleRenderer;
-class StackQueue;
-class HeroInfoBasicPanel;
-
-/// GUI object that handles functionality of panel at the bottom of combat screen
-class BattleWindow : public InterfaceObjectConfigurable
-{
-	BattleInterface & owner;
-
-	std::shared_ptr<StackQueue> queue;
-	std::shared_ptr<BattleConsole> console;
-	std::shared_ptr<HeroInfoBasicPanel> attackerHeroWindow;
-	std::shared_ptr<HeroInfoBasicPanel> defenderHeroWindow;
-
-	/// button press handling functions
-	void bOptionsf();
-	void bSurrenderf();
-	void bFleef();
-	void bAutofightf();
-	void bSpellf();
-	void bWaitf();
-	void bSwitchActionf();
-	void bDefencef();
-	void bConsoleUpf();
-	void bConsoleDownf();
-	void bTacticNextStack();
-	void bTacticPhaseEnd();
-
-	/// functions for handling actions after they were confirmed by popup window
-	void reallyFlee();
-	void reallySurrender();
-	
-	/// management of alternative actions
-	std::list<PossiblePlayerBattleAction> alternativeActions;
-	PossiblePlayerBattleAction defaultAction;
-	void showAlternativeActionIcon(PossiblePlayerBattleAction);
-
-	/// flip battle queue visibility to opposite
-	void toggleQueueVisibility();
-	void createQueue();
-
-	void toggleStickyHeroWindowsVisibility();
-	void createStickyHeroInfoWindows();
-
-	std::shared_ptr<BattleConsole> buildBattleConsole(const JsonNode &) const;
-
-public:
-	BattleWindow(BattleInterface & owner );
-	~BattleWindow();
-
-	/// Closes window once battle finished
-	void close();
-
-	/// Toggle StackQueue visibility
-	void hideQueue();
-	void showQueue();
-
-	/// Toggle permanent hero info windows visibility (HD mod feature)
-	void hideStickyHeroWindows();
-	void showStickyHeroWindows();
-
-	/// Event handler for netpack changing hero mana points
-	void heroManaPointsChanged(const CGHeroInstance * hero);
-
-	/// block all UI elements when player is not allowed to act, e.g. during enemy turn
-	void blockUI(bool on);
-
-	/// Refresh queue after turn order changes
-	void updateQueue();
-
-	/// Refresh sticky variant of hero info window after spellcast, side same as in BattleSpellCast::side
-	void updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero);
-
-	/// Get mouse-hovered battle queue unit ID if any found
-	std::optional<uint32_t> getQueueHoveredUnitId();
-
-	void activate() override;
-	void deactivate() override;
-	void keyPressed(EShortcut key) override;
-	bool captureThisKey(EShortcut key) override;
-	void clickPressed(const Point & cursorPosition) override;
-	void show(Canvas & to) override;
-	void showAll(Canvas & to) override;
-
-	/// Toggle UI to displaying tactics phase
-	void tacticPhaseStarted();
-
-	/// Toggle UI to displaying battle log in place of tactics UI
-	void tacticPhaseEnded();
-
-	/// Set possible alternative options. If more than 1 - the last will be considered as default option
-	void setAlternativeActions(const std::list<PossiblePlayerBattleAction> &);
-};
-
+/*
+ * BattleWindow.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"
+#include "../gui/InterfaceObjectConfigurable.h"
+#include "../../lib/battle/CBattleInfoCallback.h"
+#include "../../lib/battle/PossiblePlayerBattleAction.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+class CStack;
+
+VCMI_LIB_NAMESPACE_END
+
+class CButton;
+class BattleInterface;
+class BattleConsole;
+class BattleRenderer;
+class StackQueue;
+class HeroInfoBasicPanel;
+
+/// GUI object that handles functionality of panel at the bottom of combat screen
+class BattleWindow : public InterfaceObjectConfigurable
+{
+	BattleInterface & owner;
+
+	std::shared_ptr<StackQueue> queue;
+	std::shared_ptr<BattleConsole> console;
+	std::shared_ptr<HeroInfoBasicPanel> attackerHeroWindow;
+	std::shared_ptr<HeroInfoBasicPanel> defenderHeroWindow;
+
+	/// button press handling functions
+	void bOptionsf();
+	void bSurrenderf();
+	void bFleef();
+	void bAutofightf();
+	void bSpellf();
+	void bWaitf();
+	void bSwitchActionf();
+	void bDefencef();
+	void bConsoleUpf();
+	void bConsoleDownf();
+	void bTacticNextStack();
+	void bTacticPhaseEnd();
+
+	/// functions for handling actions after they were confirmed by popup window
+	void reallyFlee();
+	void reallySurrender();
+	
+	/// management of alternative actions
+	std::list<PossiblePlayerBattleAction> alternativeActions;
+	PossiblePlayerBattleAction defaultAction;
+	void showAlternativeActionIcon(PossiblePlayerBattleAction);
+
+	/// flip battle queue visibility to opposite
+	void toggleQueueVisibility();
+	void createQueue();
+
+	void toggleStickyHeroWindowsVisibility();
+	void createStickyHeroInfoWindows();
+
+	std::shared_ptr<BattleConsole> buildBattleConsole(const JsonNode &) const;
+
+public:
+	BattleWindow(BattleInterface & owner );
+	~BattleWindow();
+
+	/// Closes window once battle finished
+	void close();
+
+	/// Toggle StackQueue visibility
+	void hideQueue();
+	void showQueue();
+
+	/// Toggle permanent hero info windows visibility (HD mod feature)
+	void hideStickyHeroWindows();
+	void showStickyHeroWindows();
+
+	/// Event handler for netpack changing hero mana points
+	void heroManaPointsChanged(const CGHeroInstance * hero);
+
+	/// block all UI elements when player is not allowed to act, e.g. during enemy turn
+	void blockUI(bool on);
+
+	/// Refresh queue after turn order changes
+	void updateQueue();
+
+	/// Refresh sticky variant of hero info window after spellcast, side same as in BattleSpellCast::side
+	void updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero);
+
+	/// Get mouse-hovered battle queue unit ID if any found
+	std::optional<uint32_t> getQueueHoveredUnitId();
+
+	void activate() override;
+	void deactivate() override;
+	void keyPressed(EShortcut key) override;
+	bool captureThisKey(EShortcut key) override;
+	void clickPressed(const Point & cursorPosition) override;
+	void show(Canvas & to) override;
+	void showAll(Canvas & to) override;
+
+	/// Toggle UI to displaying tactics phase
+	void tacticPhaseStarted();
+
+	/// Toggle UI to displaying battle log in place of tactics UI
+	void tacticPhaseEnded();
+
+	/// Set possible alternative options. If more than 1 - the last will be considered as default option
+	void setAlternativeActions(const std::list<PossiblePlayerBattleAction> &);
+};
+

+ 433 - 433
client/battle/CreatureAnimation.cpp

@@ -1,433 +1,433 @@
-/*
- * CCreatureAnimation.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 "CreatureAnimation.h"
-
-#include "../../lib/CConfigHandler.h"
-#include "../../lib/CCreatureHandler.h"
-
-#include "../gui/CGuiHandler.h"
-#include "../render/Canvas.h"
-#include "../render/ColorFilter.h"
-#include "../render/IRenderHandler.h"
-
-static const ColorRGBA creatureBlueBorder = { 0, 255, 255, 255 };
-static const ColorRGBA creatureGoldBorder = { 255, 255, 0, 255 };
-static const ColorRGBA creatureNoBorder  =  { 0, 0, 0, 0 };
-
-static ColorRGBA genShadow(ui8 alpha)
-{
-	return ColorRGBA(0, 0, 0, alpha);
-}
-
-ColorRGBA AnimationControls::getBlueBorder()
-{
-	return creatureBlueBorder;
-}
-
-ColorRGBA AnimationControls::getGoldBorder()
-{
-	return creatureGoldBorder;
-}
-
-ColorRGBA AnimationControls::getNoBorder()
-{
-	return creatureNoBorder;
-}
-
-std::shared_ptr<CreatureAnimation> AnimationControls::getAnimation(const CCreature * creature)
-{
-	auto func = std::bind(&AnimationControls::getCreatureAnimationSpeed, creature, _1, _2);
-	return std::make_shared<CreatureAnimation>(creature->animDefName, func);
-}
-
-float AnimationControls::getAnimationSpeedFactor()
-{
-	// according to testing, H3 ratios between slow/medium/fast might actually be 36/60/100 (x1.666)
-	// exact value is hard to tell due to large rounding errors
-	// however we will assume them to be 33/66/100 since these values are better for standard 60 fps displays:
-	// with these numbers, base frame display duration will be 100/66/33 ms - exactly 6/4/2 frames
-	return settings["battle"]["speedFactor"].Float();
-}
-
-float AnimationControls::getCreatureAnimationSpeed(const CCreature * creature, const CreatureAnimation * anim, ECreatureAnimType type)
-{
-	assert(creature->animation.walkAnimationTime != 0);
-	assert(creature->animation.attackAnimationTime != 0);
-	assert(anim->framesInGroup(type) != 0);
-
-	// possible new fields for creature format:
-	//split "Attack time" into "Shoot Time" and "Cast Time"
-
-	// base speed for all H3 animations on slow speed is 10 frames per second (or 100ms per frame)
-	const float baseSpeed = 10.f;
-	const float speed = baseSpeed * getAnimationSpeedFactor();
-
-	switch (type)
-	{
-	case ECreatureAnimType::MOVING:
-		return speed / creature->animation.walkAnimationTime;
-
-	case ECreatureAnimType::MOUSEON:
-		return baseSpeed;
-
-	case ECreatureAnimType::HOLDING:
-			return creature->animation.idleAnimationTime;
-
-	case ECreatureAnimType::SHOOT_UP:
-	case ECreatureAnimType::SHOOT_FRONT:
-	case ECreatureAnimType::SHOOT_DOWN:
-	case ECreatureAnimType::SPECIAL_UP:
-	case ECreatureAnimType::SPECIAL_FRONT:
-	case ECreatureAnimType::SPECIAL_DOWN:
-	case ECreatureAnimType::CAST_DOWN:
-	case ECreatureAnimType::CAST_FRONT:
-	case ECreatureAnimType::CAST_UP:
-		return speed / creature->animation.attackAnimationTime;
-
-	// as strange as it looks like "attackAnimationTime" does not affects melee attacks
-	// necessary because length of these animations must be same for all creatures for synchronization
-	case ECreatureAnimType::ATTACK_UP:
-	case ECreatureAnimType::ATTACK_FRONT:
-	case ECreatureAnimType::ATTACK_DOWN:
-	case ECreatureAnimType::HITTED:
-	case ECreatureAnimType::DEFENCE:
-	case ECreatureAnimType::DEATH:
-	case ECreatureAnimType::DEATH_RANGED:
-	case ECreatureAnimType::RESURRECTION:
-	case ECreatureAnimType::GROUP_ATTACK_DOWN:
-	case ECreatureAnimType::GROUP_ATTACK_FRONT:
-	case ECreatureAnimType::GROUP_ATTACK_UP:
-		return speed;
-
-	case ECreatureAnimType::TURN_L:
-	case ECreatureAnimType::TURN_R:
-		return speed;
-
-	case ECreatureAnimType::MOVE_START:
-	case ECreatureAnimType::MOVE_END:
-		return speed;
-
-	case ECreatureAnimType::DEAD:
-	case ECreatureAnimType::DEAD_RANGED:
-		return speed;
-
-	default:
-		return speed;
-	}
-}
-
-float AnimationControls::getProjectileSpeed()
-{
-	// H3 speed: 1250/2500/3750 pixels per second
-	return static_cast<float>(getAnimationSpeedFactor() * 1250);
-}
-
-float AnimationControls::getRayProjectileSpeed()
-{
-	// H3 speed: 4000/8000/12000 pixels per second
-	return static_cast<float>(getAnimationSpeedFactor() * 4000);
-}
-
-float AnimationControls::getCatapultSpeed()
-{
-	// H3 speed: 200/400/600 pixels per second
-	return static_cast<float>(getAnimationSpeedFactor() * 200);
-}
-
-float AnimationControls::getSpellEffectSpeed()
-{
-	// H3 speed: 10/20/30 frames per second
-	return static_cast<float>(getAnimationSpeedFactor() * 10);
-}
-
-float AnimationControls::getMovementDistance(const CCreature * creature)
-{
-	// H3 speed: 2/4/6 tiles per second
-	return static_cast<float>( 2.0 * getAnimationSpeedFactor() / creature->animation.walkAnimationTime);
-}
-
-float AnimationControls::getFlightDistance(const CCreature * creature)
-{
-	// Note: for whatever reason, H3 uses "Walk Animation Time" here, even though "Flight Animation Distance" also exists
-	// H3 speed: 250/500/750 pixels per second
-	return static_cast<float>( 250.0 * getAnimationSpeedFactor() / creature->animation.walkAnimationTime);
-}
-
-float AnimationControls::getFadeInDuration()
-{
-	// H3 speed: 500/250/166 ms
-	return 0.5f / getAnimationSpeedFactor();
-}
-
-float AnimationControls::getObstaclesSpeed()
-{
-	// H3 speed: 20 frames per second, irregardless of speed setting.
-	return 20.f;
-}
-
-ECreatureAnimType CreatureAnimation::getType() const
-{
-	return type;
-}
-
-void CreatureAnimation::setType(ECreatureAnimType type)
-{
-	this->type = type;
-	currentFrame = 0;
-	once = false;
-
-	speed = speedController(this, type);
-}
-
-CreatureAnimation::CreatureAnimation(const AnimationPath & name_, TSpeedController controller)
-	: name(name_),
-	  speed(0.1f),
-	  shadowAlpha(128),
-	  currentFrame(0),
-	  animationEnd(-1),
-	  elapsedTime(0),
-	  type(ECreatureAnimType::HOLDING),
-	  speedController(controller),
-	  once(false)
-{
-	forward = GH.renderHandler().loadAnimation(name_);
-	reverse = GH.renderHandler().loadAnimation(name_);
-
-	//todo: optimize
-	forward->preload();
-	reverse->preload();
-
-	// if necessary, add one frame into vcmi-only group DEAD
-	if(forward->size(size_t(ECreatureAnimType::DEAD)) == 0)
-	{
-		forward->duplicateImage(size_t(ECreatureAnimType::DEATH), forward->size(size_t(ECreatureAnimType::DEATH))-1, size_t(ECreatureAnimType::DEAD));
-		reverse->duplicateImage(size_t(ECreatureAnimType::DEATH), reverse->size(size_t(ECreatureAnimType::DEATH))-1, size_t(ECreatureAnimType::DEAD));
-	}
-
-	if(forward->size(size_t(ECreatureAnimType::DEAD_RANGED)) == 0 && forward->size(size_t(ECreatureAnimType::DEATH_RANGED)) != 0)
-	{
-		forward->duplicateImage(size_t(ECreatureAnimType::DEATH_RANGED), forward->size(size_t(ECreatureAnimType::DEATH_RANGED))-1, size_t(ECreatureAnimType::DEAD_RANGED));
-		reverse->duplicateImage(size_t(ECreatureAnimType::DEATH_RANGED), reverse->size(size_t(ECreatureAnimType::DEATH_RANGED))-1, size_t(ECreatureAnimType::DEAD_RANGED));
-	}
-
-	if(forward->size(size_t(ECreatureAnimType::FROZEN)) == 0)
-	{
-		forward->duplicateImage(size_t(ECreatureAnimType::HOLDING), 0, size_t(ECreatureAnimType::FROZEN));
-		reverse->duplicateImage(size_t(ECreatureAnimType::HOLDING), 0, size_t(ECreatureAnimType::FROZEN));
-	}
-
-	if(forward->size(size_t(ECreatureAnimType::RESURRECTION)) == 0)
-	{
-		for (size_t i = 0; i < forward->size(size_t(ECreatureAnimType::DEATH)); ++i)
-		{
-			size_t current = forward->size(size_t(ECreatureAnimType::DEATH)) - 1 - i;
-
-			forward->duplicateImage(size_t(ECreatureAnimType::DEATH), current, size_t(ECreatureAnimType::RESURRECTION));
-			reverse->duplicateImage(size_t(ECreatureAnimType::DEATH), current, size_t(ECreatureAnimType::RESURRECTION));
-		}
-	}
-
-	//TODO: get dimensions form CAnimation
-	auto first = forward->getImage(0, size_t(type), true);
-
-	if(!first)
-	{
-		fullWidth = 0;
-		fullHeight = 0;
-		return;
-	}
-	fullWidth = first->width();
-	fullHeight = first->height();
-
-	reverse->verticalFlip();
-
-	speed = speedController(this, type);
-}
-
-void CreatureAnimation::endAnimation()
-{
-	once = false;
-	auto copy = onAnimationReset;
-	onAnimationReset.clear();
-	copy();
-}
-
-bool CreatureAnimation::incrementFrame(float timePassed)
-{
-	elapsedTime += timePassed;
-	currentFrame += timePassed * speed;
-	if (animationEnd >= 0)
-		currentFrame = std::min(currentFrame, animationEnd);
-
-	const auto framesNumber = framesInGroup(type);
-
-	if(framesNumber <= 0)
-	{
-		endAnimation();
-	}
-	else if(currentFrame >= float(framesNumber))
-	{
-		// just in case of extremely low fps (or insanely high speed)
-		while(currentFrame >= float(framesNumber))
-			currentFrame -= framesNumber;
-
-		if(once)
-			setType(ECreatureAnimType::HOLDING);
-
-		endAnimation();
-		return true;
-	}
-	return false;
-}
-
-void CreatureAnimation::setBorderColor(ColorRGBA palette)
-{
-	border = palette;
-}
-
-int CreatureAnimation::getWidth() const
-{
-	return fullWidth;
-}
-
-int CreatureAnimation::getHeight() const
-{
-	return fullHeight;
-}
-
-float CreatureAnimation::getCurrentFrame() const
-{
-	return currentFrame;
-}
-
-void CreatureAnimation::playOnce( ECreatureAnimType type )
-{
-	setType(type);
-	once = true;
-}
-
-inline int getBorderStrength(float time)
-{
-	float borderStrength = fabs(std::round(time) - time) * 2; // generate value in range 0-1
-
-	return static_cast<int>(borderStrength * 155 + 100); // scale to 0-255
-}
-
-static ColorRGBA genBorderColor(ui8 alpha, const ColorRGBA & base)
-{
-	return ColorRGBA(base.r, base.g, base.b, ui8(base.a * alpha / 256));
-}
-
-static ui8 mixChannels(ui8 c1, ui8 c2, ui8 a1, ui8 a2)
-{
-	return c1*a1 / 256 + c2*a2*(255 - a1) / 256 / 256;
-}
-
-static ColorRGBA addColors(const ColorRGBA & base, const ColorRGBA & over)
-{
-	return ColorRGBA(
-			mixChannels(over.r, base.r, over.a, base.a),
-			mixChannels(over.g, base.g, over.a, base.a),
-			mixChannels(over.b, base.b, over.a, base.a),
-			ui8(over.a + base.a * (255 - over.a) / 256)
-			);
-}
-
-void CreatureAnimation::genSpecialPalette(IImage::SpecialPalette & target)
-{
-	target.resize(8);
-	target[0] = genShadow(0);
-	target[1] = genShadow(shadowAlpha / 2);
-	// colors 2 & 3 are not used in creatures
-	target[4] = genShadow(shadowAlpha);
-	target[5] = genBorderColor(getBorderStrength(elapsedTime), border);
-	target[6] = addColors(genShadow(shadowAlpha),     genBorderColor(getBorderStrength(elapsedTime), border));
-	target[7] = addColors(genShadow(shadowAlpha / 2), genBorderColor(getBorderStrength(elapsedTime), border));
-}
-
-void CreatureAnimation::nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight)
-{
-	ColorRGBA shadowTest = shifter.shiftColor(genShadow(128));
-	shadowAlpha = shadowTest.a;
-
-	size_t frame = static_cast<size_t>(floor(currentFrame));
-
-	std::shared_ptr<IImage> image;
-
-	if(facingRight)
-		image = forward->getImage(frame, size_t(type));
-	else
-		image = reverse->getImage(frame, size_t(type));
-
-	if(image)
-	{
-		IImage::SpecialPalette SpecialPalette;
-		genSpecialPalette(SpecialPalette);
-
-		image->setSpecialPallete(SpecialPalette, IImage::SPECIAL_PALETTE_MASK_CREATURES);
-		image->adjustPalette(shifter, IImage::SPECIAL_PALETTE_MASK_CREATURES);
-
-		canvas.draw(image, pos.topLeft(), Rect(0, 0, pos.w, pos.h));
-
-	}
-}
-
-void CreatureAnimation::playUntil(size_t frameIndex)
-{
-	animationEnd = frameIndex;
-}
-
-int CreatureAnimation::framesInGroup(ECreatureAnimType group) const
-{
-	return static_cast<int>(forward->size(size_t(group)));
-}
-
-bool CreatureAnimation::isDead() const
-{
-	return getType() == ECreatureAnimType::DEAD
-		|| getType() == ECreatureAnimType::DEAD_RANGED;
-}
-
-bool CreatureAnimation::isDying() const
-{
-	return getType() == ECreatureAnimType::DEATH
-		|| getType() == ECreatureAnimType::DEATH_RANGED;
-}
-
-bool CreatureAnimation::isDeadOrDying() const
-{
-	return getType() == ECreatureAnimType::DEAD
-		|| getType() == ECreatureAnimType::DEATH
-		|| getType() == ECreatureAnimType::DEAD_RANGED
-		|| getType() == ECreatureAnimType::DEATH_RANGED;
-}
-
-bool CreatureAnimation::isIdle() const
-{
-	return getType() == ECreatureAnimType::HOLDING
-		|| getType() == ECreatureAnimType::MOUSEON;
-}
-
-bool CreatureAnimation::isMoving() const
-{
-	return getType() == ECreatureAnimType::MOVE_START
-		|| getType() == ECreatureAnimType::MOVING
-		|| getType() == ECreatureAnimType::MOVE_END
-		|| getType() == ECreatureAnimType::TURN_L
-		|| getType() == ECreatureAnimType::TURN_R;
-}
-
-bool CreatureAnimation::isShooting() const
-{
-	return getType() == ECreatureAnimType::SHOOT_UP
-		|| getType() == ECreatureAnimType::SHOOT_FRONT
-		|| getType() == ECreatureAnimType::SHOOT_DOWN;
-}
+/*
+ * CCreatureAnimation.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 "CreatureAnimation.h"
+
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/CCreatureHandler.h"
+
+#include "../gui/CGuiHandler.h"
+#include "../render/Canvas.h"
+#include "../render/ColorFilter.h"
+#include "../render/IRenderHandler.h"
+
+static const ColorRGBA creatureBlueBorder = { 0, 255, 255, 255 };
+static const ColorRGBA creatureGoldBorder = { 255, 255, 0, 255 };
+static const ColorRGBA creatureNoBorder  =  { 0, 0, 0, 0 };
+
+static ColorRGBA genShadow(ui8 alpha)
+{
+	return ColorRGBA(0, 0, 0, alpha);
+}
+
+ColorRGBA AnimationControls::getBlueBorder()
+{
+	return creatureBlueBorder;
+}
+
+ColorRGBA AnimationControls::getGoldBorder()
+{
+	return creatureGoldBorder;
+}
+
+ColorRGBA AnimationControls::getNoBorder()
+{
+	return creatureNoBorder;
+}
+
+std::shared_ptr<CreatureAnimation> AnimationControls::getAnimation(const CCreature * creature)
+{
+	auto func = std::bind(&AnimationControls::getCreatureAnimationSpeed, creature, _1, _2);
+	return std::make_shared<CreatureAnimation>(creature->animDefName, func);
+}
+
+float AnimationControls::getAnimationSpeedFactor()
+{
+	// according to testing, H3 ratios between slow/medium/fast might actually be 36/60/100 (x1.666)
+	// exact value is hard to tell due to large rounding errors
+	// however we will assume them to be 33/66/100 since these values are better for standard 60 fps displays:
+	// with these numbers, base frame display duration will be 100/66/33 ms - exactly 6/4/2 frames
+	return settings["battle"]["speedFactor"].Float();
+}
+
+float AnimationControls::getCreatureAnimationSpeed(const CCreature * creature, const CreatureAnimation * anim, ECreatureAnimType type)
+{
+	assert(creature->animation.walkAnimationTime != 0);
+	assert(creature->animation.attackAnimationTime != 0);
+	assert(anim->framesInGroup(type) != 0);
+
+	// possible new fields for creature format:
+	//split "Attack time" into "Shoot Time" and "Cast Time"
+
+	// base speed for all H3 animations on slow speed is 10 frames per second (or 100ms per frame)
+	const float baseSpeed = 10.f;
+	const float speed = baseSpeed * getAnimationSpeedFactor();
+
+	switch (type)
+	{
+	case ECreatureAnimType::MOVING:
+		return speed / creature->animation.walkAnimationTime;
+
+	case ECreatureAnimType::MOUSEON:
+		return baseSpeed;
+
+	case ECreatureAnimType::HOLDING:
+			return creature->animation.idleAnimationTime;
+
+	case ECreatureAnimType::SHOOT_UP:
+	case ECreatureAnimType::SHOOT_FRONT:
+	case ECreatureAnimType::SHOOT_DOWN:
+	case ECreatureAnimType::SPECIAL_UP:
+	case ECreatureAnimType::SPECIAL_FRONT:
+	case ECreatureAnimType::SPECIAL_DOWN:
+	case ECreatureAnimType::CAST_DOWN:
+	case ECreatureAnimType::CAST_FRONT:
+	case ECreatureAnimType::CAST_UP:
+		return speed / creature->animation.attackAnimationTime;
+
+	// as strange as it looks like "attackAnimationTime" does not affects melee attacks
+	// necessary because length of these animations must be same for all creatures for synchronization
+	case ECreatureAnimType::ATTACK_UP:
+	case ECreatureAnimType::ATTACK_FRONT:
+	case ECreatureAnimType::ATTACK_DOWN:
+	case ECreatureAnimType::HITTED:
+	case ECreatureAnimType::DEFENCE:
+	case ECreatureAnimType::DEATH:
+	case ECreatureAnimType::DEATH_RANGED:
+	case ECreatureAnimType::RESURRECTION:
+	case ECreatureAnimType::GROUP_ATTACK_DOWN:
+	case ECreatureAnimType::GROUP_ATTACK_FRONT:
+	case ECreatureAnimType::GROUP_ATTACK_UP:
+		return speed;
+
+	case ECreatureAnimType::TURN_L:
+	case ECreatureAnimType::TURN_R:
+		return speed;
+
+	case ECreatureAnimType::MOVE_START:
+	case ECreatureAnimType::MOVE_END:
+		return speed;
+
+	case ECreatureAnimType::DEAD:
+	case ECreatureAnimType::DEAD_RANGED:
+		return speed;
+
+	default:
+		return speed;
+	}
+}
+
+float AnimationControls::getProjectileSpeed()
+{
+	// H3 speed: 1250/2500/3750 pixels per second
+	return static_cast<float>(getAnimationSpeedFactor() * 1250);
+}
+
+float AnimationControls::getRayProjectileSpeed()
+{
+	// H3 speed: 4000/8000/12000 pixels per second
+	return static_cast<float>(getAnimationSpeedFactor() * 4000);
+}
+
+float AnimationControls::getCatapultSpeed()
+{
+	// H3 speed: 200/400/600 pixels per second
+	return static_cast<float>(getAnimationSpeedFactor() * 200);
+}
+
+float AnimationControls::getSpellEffectSpeed()
+{
+	// H3 speed: 10/20/30 frames per second
+	return static_cast<float>(getAnimationSpeedFactor() * 10);
+}
+
+float AnimationControls::getMovementDistance(const CCreature * creature)
+{
+	// H3 speed: 2/4/6 tiles per second
+	return static_cast<float>( 2.0 * getAnimationSpeedFactor() / creature->animation.walkAnimationTime);
+}
+
+float AnimationControls::getFlightDistance(const CCreature * creature)
+{
+	// Note: for whatever reason, H3 uses "Walk Animation Time" here, even though "Flight Animation Distance" also exists
+	// H3 speed: 250/500/750 pixels per second
+	return static_cast<float>( 250.0 * getAnimationSpeedFactor() / creature->animation.walkAnimationTime);
+}
+
+float AnimationControls::getFadeInDuration()
+{
+	// H3 speed: 500/250/166 ms
+	return 0.5f / getAnimationSpeedFactor();
+}
+
+float AnimationControls::getObstaclesSpeed()
+{
+	// H3 speed: 20 frames per second, irregardless of speed setting.
+	return 20.f;
+}
+
+ECreatureAnimType CreatureAnimation::getType() const
+{
+	return type;
+}
+
+void CreatureAnimation::setType(ECreatureAnimType type)
+{
+	this->type = type;
+	currentFrame = 0;
+	once = false;
+
+	speed = speedController(this, type);
+}
+
+CreatureAnimation::CreatureAnimation(const AnimationPath & name_, TSpeedController controller)
+	: name(name_),
+	  speed(0.1f),
+	  shadowAlpha(128),
+	  currentFrame(0),
+	  animationEnd(-1),
+	  elapsedTime(0),
+	  type(ECreatureAnimType::HOLDING),
+	  speedController(controller),
+	  once(false)
+{
+	forward = GH.renderHandler().loadAnimation(name_);
+	reverse = GH.renderHandler().loadAnimation(name_);
+
+	//todo: optimize
+	forward->preload();
+	reverse->preload();
+
+	// if necessary, add one frame into vcmi-only group DEAD
+	if(forward->size(size_t(ECreatureAnimType::DEAD)) == 0)
+	{
+		forward->duplicateImage(size_t(ECreatureAnimType::DEATH), forward->size(size_t(ECreatureAnimType::DEATH))-1, size_t(ECreatureAnimType::DEAD));
+		reverse->duplicateImage(size_t(ECreatureAnimType::DEATH), reverse->size(size_t(ECreatureAnimType::DEATH))-1, size_t(ECreatureAnimType::DEAD));
+	}
+
+	if(forward->size(size_t(ECreatureAnimType::DEAD_RANGED)) == 0 && forward->size(size_t(ECreatureAnimType::DEATH_RANGED)) != 0)
+	{
+		forward->duplicateImage(size_t(ECreatureAnimType::DEATH_RANGED), forward->size(size_t(ECreatureAnimType::DEATH_RANGED))-1, size_t(ECreatureAnimType::DEAD_RANGED));
+		reverse->duplicateImage(size_t(ECreatureAnimType::DEATH_RANGED), reverse->size(size_t(ECreatureAnimType::DEATH_RANGED))-1, size_t(ECreatureAnimType::DEAD_RANGED));
+	}
+
+	if(forward->size(size_t(ECreatureAnimType::FROZEN)) == 0)
+	{
+		forward->duplicateImage(size_t(ECreatureAnimType::HOLDING), 0, size_t(ECreatureAnimType::FROZEN));
+		reverse->duplicateImage(size_t(ECreatureAnimType::HOLDING), 0, size_t(ECreatureAnimType::FROZEN));
+	}
+
+	if(forward->size(size_t(ECreatureAnimType::RESURRECTION)) == 0)
+	{
+		for (size_t i = 0; i < forward->size(size_t(ECreatureAnimType::DEATH)); ++i)
+		{
+			size_t current = forward->size(size_t(ECreatureAnimType::DEATH)) - 1 - i;
+
+			forward->duplicateImage(size_t(ECreatureAnimType::DEATH), current, size_t(ECreatureAnimType::RESURRECTION));
+			reverse->duplicateImage(size_t(ECreatureAnimType::DEATH), current, size_t(ECreatureAnimType::RESURRECTION));
+		}
+	}
+
+	//TODO: get dimensions form CAnimation
+	auto first = forward->getImage(0, size_t(type), true);
+
+	if(!first)
+	{
+		fullWidth = 0;
+		fullHeight = 0;
+		return;
+	}
+	fullWidth = first->width();
+	fullHeight = first->height();
+
+	reverse->verticalFlip();
+
+	speed = speedController(this, type);
+}
+
+void CreatureAnimation::endAnimation()
+{
+	once = false;
+	auto copy = onAnimationReset;
+	onAnimationReset.clear();
+	copy();
+}
+
+bool CreatureAnimation::incrementFrame(float timePassed)
+{
+	elapsedTime += timePassed;
+	currentFrame += timePassed * speed;
+	if (animationEnd >= 0)
+		currentFrame = std::min(currentFrame, animationEnd);
+
+	const auto framesNumber = framesInGroup(type);
+
+	if(framesNumber <= 0)
+	{
+		endAnimation();
+	}
+	else if(currentFrame >= float(framesNumber))
+	{
+		// just in case of extremely low fps (or insanely high speed)
+		while(currentFrame >= float(framesNumber))
+			currentFrame -= framesNumber;
+
+		if(once)
+			setType(ECreatureAnimType::HOLDING);
+
+		endAnimation();
+		return true;
+	}
+	return false;
+}
+
+void CreatureAnimation::setBorderColor(ColorRGBA palette)
+{
+	border = palette;
+}
+
+int CreatureAnimation::getWidth() const
+{
+	return fullWidth;
+}
+
+int CreatureAnimation::getHeight() const
+{
+	return fullHeight;
+}
+
+float CreatureAnimation::getCurrentFrame() const
+{
+	return currentFrame;
+}
+
+void CreatureAnimation::playOnce( ECreatureAnimType type )
+{
+	setType(type);
+	once = true;
+}
+
+inline int getBorderStrength(float time)
+{
+	float borderStrength = fabs(std::round(time) - time) * 2; // generate value in range 0-1
+
+	return static_cast<int>(borderStrength * 155 + 100); // scale to 0-255
+}
+
+static ColorRGBA genBorderColor(ui8 alpha, const ColorRGBA & base)
+{
+	return ColorRGBA(base.r, base.g, base.b, ui8(base.a * alpha / 256));
+}
+
+static ui8 mixChannels(ui8 c1, ui8 c2, ui8 a1, ui8 a2)
+{
+	return c1*a1 / 256 + c2*a2*(255 - a1) / 256 / 256;
+}
+
+static ColorRGBA addColors(const ColorRGBA & base, const ColorRGBA & over)
+{
+	return ColorRGBA(
+			mixChannels(over.r, base.r, over.a, base.a),
+			mixChannels(over.g, base.g, over.a, base.a),
+			mixChannels(over.b, base.b, over.a, base.a),
+			ui8(over.a + base.a * (255 - over.a) / 256)
+			);
+}
+
+void CreatureAnimation::genSpecialPalette(IImage::SpecialPalette & target)
+{
+	target.resize(8);
+	target[0] = genShadow(0);
+	target[1] = genShadow(shadowAlpha / 2);
+	// colors 2 & 3 are not used in creatures
+	target[4] = genShadow(shadowAlpha);
+	target[5] = genBorderColor(getBorderStrength(elapsedTime), border);
+	target[6] = addColors(genShadow(shadowAlpha),     genBorderColor(getBorderStrength(elapsedTime), border));
+	target[7] = addColors(genShadow(shadowAlpha / 2), genBorderColor(getBorderStrength(elapsedTime), border));
+}
+
+void CreatureAnimation::nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight)
+{
+	ColorRGBA shadowTest = shifter.shiftColor(genShadow(128));
+	shadowAlpha = shadowTest.a;
+
+	size_t frame = static_cast<size_t>(floor(currentFrame));
+
+	std::shared_ptr<IImage> image;
+
+	if(facingRight)
+		image = forward->getImage(frame, size_t(type));
+	else
+		image = reverse->getImage(frame, size_t(type));
+
+	if(image)
+	{
+		IImage::SpecialPalette SpecialPalette;
+		genSpecialPalette(SpecialPalette);
+
+		image->setSpecialPallete(SpecialPalette, IImage::SPECIAL_PALETTE_MASK_CREATURES);
+		image->adjustPalette(shifter, IImage::SPECIAL_PALETTE_MASK_CREATURES);
+
+		canvas.draw(image, pos.topLeft(), Rect(0, 0, pos.w, pos.h));
+
+	}
+}
+
+void CreatureAnimation::playUntil(size_t frameIndex)
+{
+	animationEnd = frameIndex;
+}
+
+int CreatureAnimation::framesInGroup(ECreatureAnimType group) const
+{
+	return static_cast<int>(forward->size(size_t(group)));
+}
+
+bool CreatureAnimation::isDead() const
+{
+	return getType() == ECreatureAnimType::DEAD
+		|| getType() == ECreatureAnimType::DEAD_RANGED;
+}
+
+bool CreatureAnimation::isDying() const
+{
+	return getType() == ECreatureAnimType::DEATH
+		|| getType() == ECreatureAnimType::DEATH_RANGED;
+}
+
+bool CreatureAnimation::isDeadOrDying() const
+{
+	return getType() == ECreatureAnimType::DEAD
+		|| getType() == ECreatureAnimType::DEATH
+		|| getType() == ECreatureAnimType::DEAD_RANGED
+		|| getType() == ECreatureAnimType::DEATH_RANGED;
+}
+
+bool CreatureAnimation::isIdle() const
+{
+	return getType() == ECreatureAnimType::HOLDING
+		|| getType() == ECreatureAnimType::MOUSEON;
+}
+
+bool CreatureAnimation::isMoving() const
+{
+	return getType() == ECreatureAnimType::MOVE_START
+		|| getType() == ECreatureAnimType::MOVING
+		|| getType() == ECreatureAnimType::MOVE_END
+		|| getType() == ECreatureAnimType::TURN_L
+		|| getType() == ECreatureAnimType::TURN_R;
+}
+
+bool CreatureAnimation::isShooting() const
+{
+	return getType() == ECreatureAnimType::SHOOT_UP
+		|| getType() == ECreatureAnimType::SHOOT_FRONT
+		|| getType() == ECreatureAnimType::SHOOT_DOWN;
+}

+ 158 - 158
client/battle/CreatureAnimation.h

@@ -1,158 +1,158 @@
-/*
- * CCreatureAnimation.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/FunctionList.h"
-#include "../../lib/Color.h"
-#include "../widgets/Images.h"
-#include "../render/CAnimation.h"
-#include "../render/IImage.h"
-
-class CIntObject;
-class CreatureAnimation;
-class Canvas;
-
-/// Namespace for some common controls of animations
-namespace AnimationControls
-{
-	/// get color for creature selection borders
-	ColorRGBA getBlueBorder();
-	ColorRGBA getGoldBorder();
-	ColorRGBA getNoBorder();
-
-	/// returns animation speed factor according to game settings,
-	/// slow speed is considered to be "base speed" and will return 1.0
-	float getAnimationSpeedFactor();
-
-	/// creates animation object with preset speed control
-	std::shared_ptr<CreatureAnimation> getAnimation(const CCreature * creature);
-
-	/// returns animation speed of specific group, taking in mind game setting (in frames per second)
-	float getCreatureAnimationSpeed(const CCreature * creature, const CreatureAnimation * anim, ECreatureAnimType groupID);
-
-	/// returns how far projectile should move per second, in pixels per second
-	float getProjectileSpeed();
-
-	/// returns how far projectile should move per second, in pixels per second
-	float getRayProjectileSpeed();
-
-	/// returns speed of catapult projectile, in pixels per second, on a straight line, without parabola correction
-	float getCatapultSpeed();
-
-	/// returns speed of any spell effects, including any special effects like morale (in frames per second)
-	float getSpellEffectSpeed();
-
-	/// returns speed of movement animation across the screen, in tiles per second
-	float getMovementDistance(const CCreature * creature);
-
-	/// returns speed of movement animation across the screen, in pixels per seconds
-	float getFlightDistance(const CCreature * creature);
-
-	/// Returns total time for full fade-in effect on newly summoned creatures, in seconds
-	float getFadeInDuration();
-
-	/// Returns animation speed for obstacles, in frames per second
-	float getObstaclesSpeed();
-}
-
-/// Class which manages animations of creatures/units inside battles
-/// TODO: split into constant image container and class that does *control* of animation
-class CreatureAnimation : public CIntObject
-{
-public:
-	using TSpeedController = std::function<float(CreatureAnimation *, ECreatureAnimType)>;
-
-private:
-	AnimationPath name;
-
-	/// animation for rendering stack in default orientation - facing right
-	std::shared_ptr<CAnimation> forward;
-
-	/// animation that has all its frames flipped for rendering stack facing left
-	std::shared_ptr<CAnimation> reverse;
-
-	int fullWidth;
-	int fullHeight;
-
-	/// speed of animation, measure in frames per second
-	float speed;
-
-	/// currently displayed frame. Float to allow H3-style animations where frames
-	/// don't display for integer number of frames
-	float currentFrame;
-	float animationEnd;
-
-	/// cumulative, real-time duration of animation. Used for effects like selection border
-	float elapsedTime;
-
-	///type of animation being displayed
-	ECreatureAnimType type;
-
-	/// current value of shadow transparency
-	uint8_t shadowAlpha;
-
-	/// border color, disabled if alpha = 0
-	ColorRGBA border;
-
-	TSpeedController speedController;
-
-	/// animation will be played once and the reset to idling
-	bool once;
-
-	void endAnimation();
-
-	void genSpecialPalette(IImage::SpecialPalette & target);
-public:
-
-	/// function(s) that will be called when animation ends, after reset to 1st frame
-	/// NOTE that these functions will be fired only once
-	CFunctionList<void()> onAnimationReset;
-
-	int getWidth() const;
-	int getHeight() const;
-
-	/// Constructor
-	/// name - path to .def file, relative to SPRITES/ directory
-	/// controller - function that will return for how long *each* frame
-	/// in specified group of animation should be played, measured in seconds
-	CreatureAnimation(const AnimationPath & name_, TSpeedController speedController);
-
-	/// sets type of animation and resets framecount
-	void setType(ECreatureAnimType type);
-
-	/// returns currently rendered type of animation
-	ECreatureAnimType getType() const;
-
-	void nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight);
-
-	/// should be called every frame, return true when animation was reset to beginning
-	bool incrementFrame(float timePassed);
-
-	void setBorderColor(ColorRGBA palette);
-
-	/// Gets the current frame ID within current group.
-	float getCurrentFrame() const;
-
-	/// plays once given type of animation, then resets to idle
-	void playOnce(ECreatureAnimType type);
-
-	/// returns number of frames in selected animation type
-	int framesInGroup(ECreatureAnimType group) const;
-
-	void playUntil(size_t frameIndex);
-
-	/// helpers to classify current type of animation
-	bool isDead() const;
-	bool isDying() const;
-	bool isDeadOrDying() const;
-	bool isIdle() const;
-	bool isMoving() const;
-	bool isShooting() const;
-};
+/*
+ * CCreatureAnimation.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/FunctionList.h"
+#include "../../lib/Color.h"
+#include "../widgets/Images.h"
+#include "../render/CAnimation.h"
+#include "../render/IImage.h"
+
+class CIntObject;
+class CreatureAnimation;
+class Canvas;
+
+/// Namespace for some common controls of animations
+namespace AnimationControls
+{
+	/// get color for creature selection borders
+	ColorRGBA getBlueBorder();
+	ColorRGBA getGoldBorder();
+	ColorRGBA getNoBorder();
+
+	/// returns animation speed factor according to game settings,
+	/// slow speed is considered to be "base speed" and will return 1.0
+	float getAnimationSpeedFactor();
+
+	/// creates animation object with preset speed control
+	std::shared_ptr<CreatureAnimation> getAnimation(const CCreature * creature);
+
+	/// returns animation speed of specific group, taking in mind game setting (in frames per second)
+	float getCreatureAnimationSpeed(const CCreature * creature, const CreatureAnimation * anim, ECreatureAnimType groupID);
+
+	/// returns how far projectile should move per second, in pixels per second
+	float getProjectileSpeed();
+
+	/// returns how far projectile should move per second, in pixels per second
+	float getRayProjectileSpeed();
+
+	/// returns speed of catapult projectile, in pixels per second, on a straight line, without parabola correction
+	float getCatapultSpeed();
+
+	/// returns speed of any spell effects, including any special effects like morale (in frames per second)
+	float getSpellEffectSpeed();
+
+	/// returns speed of movement animation across the screen, in tiles per second
+	float getMovementDistance(const CCreature * creature);
+
+	/// returns speed of movement animation across the screen, in pixels per seconds
+	float getFlightDistance(const CCreature * creature);
+
+	/// Returns total time for full fade-in effect on newly summoned creatures, in seconds
+	float getFadeInDuration();
+
+	/// Returns animation speed for obstacles, in frames per second
+	float getObstaclesSpeed();
+}
+
+/// Class which manages animations of creatures/units inside battles
+/// TODO: split into constant image container and class that does *control* of animation
+class CreatureAnimation : public CIntObject
+{
+public:
+	using TSpeedController = std::function<float(CreatureAnimation *, ECreatureAnimType)>;
+
+private:
+	AnimationPath name;
+
+	/// animation for rendering stack in default orientation - facing right
+	std::shared_ptr<CAnimation> forward;
+
+	/// animation that has all its frames flipped for rendering stack facing left
+	std::shared_ptr<CAnimation> reverse;
+
+	int fullWidth;
+	int fullHeight;
+
+	/// speed of animation, measure in frames per second
+	float speed;
+
+	/// currently displayed frame. Float to allow H3-style animations where frames
+	/// don't display for integer number of frames
+	float currentFrame;
+	float animationEnd;
+
+	/// cumulative, real-time duration of animation. Used for effects like selection border
+	float elapsedTime;
+
+	///type of animation being displayed
+	ECreatureAnimType type;
+
+	/// current value of shadow transparency
+	uint8_t shadowAlpha;
+
+	/// border color, disabled if alpha = 0
+	ColorRGBA border;
+
+	TSpeedController speedController;
+
+	/// animation will be played once and the reset to idling
+	bool once;
+
+	void endAnimation();
+
+	void genSpecialPalette(IImage::SpecialPalette & target);
+public:
+
+	/// function(s) that will be called when animation ends, after reset to 1st frame
+	/// NOTE that these functions will be fired only once
+	CFunctionList<void()> onAnimationReset;
+
+	int getWidth() const;
+	int getHeight() const;
+
+	/// Constructor
+	/// name - path to .def file, relative to SPRITES/ directory
+	/// controller - function that will return for how long *each* frame
+	/// in specified group of animation should be played, measured in seconds
+	CreatureAnimation(const AnimationPath & name_, TSpeedController speedController);
+
+	/// sets type of animation and resets framecount
+	void setType(ECreatureAnimType type);
+
+	/// returns currently rendered type of animation
+	ECreatureAnimType getType() const;
+
+	void nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight);
+
+	/// should be called every frame, return true when animation was reset to beginning
+	bool incrementFrame(float timePassed);
+
+	void setBorderColor(ColorRGBA palette);
+
+	/// Gets the current frame ID within current group.
+	float getCurrentFrame() const;
+
+	/// plays once given type of animation, then resets to idle
+	void playOnce(ECreatureAnimType type);
+
+	/// returns number of frames in selected animation type
+	int framesInGroup(ECreatureAnimType group) const;
+
+	void playUntil(size_t frameIndex);
+
+	/// helpers to classify current type of animation
+	bool isDead() const;
+	bool isDying() const;
+	bool isDeadOrDying() const;
+	bool isIdle() const;
+	bool isMoving() const;
+	bool isShooting() const;
+};

+ 248 - 248
client/gui/CGuiHandler.cpp

@@ -1,248 +1,248 @@
-/*
- * CGuiHandler.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 "CGuiHandler.h"
-#include "../lib/CondSh.h"
-
-#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 "../render/Graphics.h"
-#include "../render/IFont.h"
-#include "../render/EFont.h"
-#include "../renderSDL/ScreenHandler.h"
-#include "../renderSDL/RenderHandler.h"
-#include "../CMT.h"
-#include "../CPlayerInterface.h"
-#include "../battle/BattleInterface.h"
-
-#include "../../lib/CThreadHelper.h"
-#include "../../lib/CConfigHandler.h"
-
-#include <SDL_render.h>
-
-CGuiHandler GH;
-
-static thread_local bool inGuiThread = false;
-
-SObjectConstruction::SObjectConstruction(CIntObject *obj)
-:myObj(obj)
-{
-	GH.createdObj.push_front(obj);
-	GH.captureChildren = true;
-}
-
-SObjectConstruction::~SObjectConstruction()
-{
-	assert(GH.createdObj.size());
-	assert(GH.createdObj.front() == myObj);
-	GH.createdObj.pop_front();
-	GH.captureChildren = GH.createdObj.size();
-}
-
-SSetCaptureState::SSetCaptureState(bool allow, ui8 actions)
-{
-	previousCapture = GH.captureChildren;
-	GH.captureChildren = false;
-	prevActions = GH.defActionsDef;
-	GH.defActionsDef = actions;
-}
-
-SSetCaptureState::~SSetCaptureState()
-{
-	GH.captureChildren = previousCapture;
-	GH.defActionsDef = prevActions;
-}
-
-void CGuiHandler::init()
-{
-	inGuiThread = true;
-
-	inputHandlerInstance = std::make_unique<InputHandler>();
-	eventDispatcherInstance = std::make_unique<EventDispatcher>();
-	windowHandlerInstance = std::make_unique<WindowHandler>();
-	screenHandlerInstance = std::make_unique<ScreenHandler>();
-	renderHandlerInstance = std::make_unique<RenderHandler>();
-	shortcutsHandlerInstance = std::make_unique<ShortcutHandler>();
-	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;
-
-	input().processEvents();
-}
-
-void CGuiHandler::fakeMouseMove()
-{
-	dispatchMainThread([](){
-		GH.events().dispatchMouseMoved(Point(0, 0), GH.getCursorPosition());
-	});
-}
-
-void CGuiHandler::startTextInput(const Rect & whereInput)
-{
-	input().startTextInput(whereInput);
-}
-
-void CGuiHandler::stopTextInput()
-{
-	input().stopTextInput();
-}
-
-void CGuiHandler::renderFrame()
-{
-	{
-		boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
-
-		if(nullptr != curInt)
-			curInt->update();
-
-		if(settings["video"]["showfps"].Bool())
-			drawFPSCounter();
-
-		SDL_UpdateTexture(screenTexture, nullptr, screen->pixels, screen->pitch);
-
-		SDL_RenderClear(mainRenderer);
-		SDL_RenderCopy(mainRenderer, screenTexture, nullptr, nullptr);
-
-		CCS->curh->render();
-
-		windows().onFrameRendered();
-	}
-
-	SDL_RenderPresent(mainRenderer);
-	framerate().framerateDelay(); // holds a constant FPS
-}
-
-CGuiHandler::CGuiHandler()
-	: defActionsDef(0)
-	, captureChildren(false)
-	, curInt(nullptr)
-	, fakeStatusBar(std::make_shared<EmptyStatusBar>())
-{
-}
-
-CGuiHandler::~CGuiHandler() = default;
-
-ShortcutHandler & CGuiHandler::shortcuts()
-{
-	assert(shortcutsHandlerInstance);
-	return *shortcutsHandlerInstance;
-}
-
-FramerateManager & CGuiHandler::framerate()
-{
-	assert(framerateManagerInstance);
-	return *framerateManagerInstance;
-}
-
-bool CGuiHandler::isKeyboardCtrlDown() const
-{
-	return inputHandlerInstance->isKeyboardCtrlDown();
-}
-
-bool CGuiHandler::isKeyboardAltDown() const
-{
-	return inputHandlerInstance->isKeyboardAltDown();
-}
-
-bool CGuiHandler::isKeyboardShiftDown() const
-{
-	return inputHandlerInstance->isKeyboardShiftDown();
-}
-
-const Point & CGuiHandler::getCursorPosition() const
-{
-	return inputHandlerInstance->getCursorPosition();
-}
-
-Point CGuiHandler::screenDimensions() const
-{
-	return Point(screen->w, screen->h);
-}
-
-void CGuiHandler::drawFPSCounter()
-{
-	static SDL_Rect overlay = { 0, 0, 24, 24};
-	uint32_t black = SDL_MapRGB(screen->format, 10, 10, 10);
-	SDL_FillRect(screen, &overlay, black);
-	std::string fps = std::to_string(framerate().getFramerate());
-	graphics->fonts[FONT_BIG]->renderTextLeft(screen, fps, Colors::YELLOW, Point(4, 2));
-}
-
-bool CGuiHandler::amIGuiThread()
-{
-	return inGuiThread;
-}
-
-void CGuiHandler::dispatchMainThread(const std::function<void()> & functor)
-{
-	inputHandlerInstance->dispatchMainThread(functor);
-}
-
-IScreenHandler & CGuiHandler::screenHandler()
-{
-	return *screenHandlerInstance;
-}
-
-IRenderHandler & CGuiHandler::renderHandler()
-{
-	return *renderHandlerInstance;
-}
-
-EventDispatcher & CGuiHandler::events()
-{
-	return *eventDispatcherInstance;
-}
-
-InputHandler & CGuiHandler::input()
-{
-	return *inputHandlerInstance;
-}
-
-WindowHandler & CGuiHandler::windows()
-{
-	assert(windowHandlerInstance);
-	return *windowHandlerInstance;
-}
-
-std::shared_ptr<IStatusBar> CGuiHandler::statusbar()
-{
-	auto locked = currentStatusBar.lock();
-
-	if (!locked)
-		return fakeStatusBar;
-
-	return locked;
-}
-
-void CGuiHandler::setStatusbar(std::shared_ptr<IStatusBar> newStatusBar)
-{
-	currentStatusBar = newStatusBar;
-}
-
-void CGuiHandler::onScreenResize()
-{
-	screenHandler().onScreenResize();
-	windows().onScreenResize();
-}
+/*
+ * CGuiHandler.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 "CGuiHandler.h"
+#include "../lib/CondSh.h"
+
+#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 "../render/Graphics.h"
+#include "../render/IFont.h"
+#include "../render/EFont.h"
+#include "../renderSDL/ScreenHandler.h"
+#include "../renderSDL/RenderHandler.h"
+#include "../CMT.h"
+#include "../CPlayerInterface.h"
+#include "../battle/BattleInterface.h"
+
+#include "../../lib/CThreadHelper.h"
+#include "../../lib/CConfigHandler.h"
+
+#include <SDL_render.h>
+
+CGuiHandler GH;
+
+static thread_local bool inGuiThread = false;
+
+SObjectConstruction::SObjectConstruction(CIntObject *obj)
+:myObj(obj)
+{
+	GH.createdObj.push_front(obj);
+	GH.captureChildren = true;
+}
+
+SObjectConstruction::~SObjectConstruction()
+{
+	assert(GH.createdObj.size());
+	assert(GH.createdObj.front() == myObj);
+	GH.createdObj.pop_front();
+	GH.captureChildren = GH.createdObj.size();
+}
+
+SSetCaptureState::SSetCaptureState(bool allow, ui8 actions)
+{
+	previousCapture = GH.captureChildren;
+	GH.captureChildren = false;
+	prevActions = GH.defActionsDef;
+	GH.defActionsDef = actions;
+}
+
+SSetCaptureState::~SSetCaptureState()
+{
+	GH.captureChildren = previousCapture;
+	GH.defActionsDef = prevActions;
+}
+
+void CGuiHandler::init()
+{
+	inGuiThread = true;
+
+	inputHandlerInstance = std::make_unique<InputHandler>();
+	eventDispatcherInstance = std::make_unique<EventDispatcher>();
+	windowHandlerInstance = std::make_unique<WindowHandler>();
+	screenHandlerInstance = std::make_unique<ScreenHandler>();
+	renderHandlerInstance = std::make_unique<RenderHandler>();
+	shortcutsHandlerInstance = std::make_unique<ShortcutHandler>();
+	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;
+
+	input().processEvents();
+}
+
+void CGuiHandler::fakeMouseMove()
+{
+	dispatchMainThread([](){
+		GH.events().dispatchMouseMoved(Point(0, 0), GH.getCursorPosition());
+	});
+}
+
+void CGuiHandler::startTextInput(const Rect & whereInput)
+{
+	input().startTextInput(whereInput);
+}
+
+void CGuiHandler::stopTextInput()
+{
+	input().stopTextInput();
+}
+
+void CGuiHandler::renderFrame()
+{
+	{
+		boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
+
+		if(nullptr != curInt)
+			curInt->update();
+
+		if(settings["video"]["showfps"].Bool())
+			drawFPSCounter();
+
+		SDL_UpdateTexture(screenTexture, nullptr, screen->pixels, screen->pitch);
+
+		SDL_RenderClear(mainRenderer);
+		SDL_RenderCopy(mainRenderer, screenTexture, nullptr, nullptr);
+
+		CCS->curh->render();
+
+		windows().onFrameRendered();
+	}
+
+	SDL_RenderPresent(mainRenderer);
+	framerate().framerateDelay(); // holds a constant FPS
+}
+
+CGuiHandler::CGuiHandler()
+	: defActionsDef(0)
+	, captureChildren(false)
+	, curInt(nullptr)
+	, fakeStatusBar(std::make_shared<EmptyStatusBar>())
+{
+}
+
+CGuiHandler::~CGuiHandler() = default;
+
+ShortcutHandler & CGuiHandler::shortcuts()
+{
+	assert(shortcutsHandlerInstance);
+	return *shortcutsHandlerInstance;
+}
+
+FramerateManager & CGuiHandler::framerate()
+{
+	assert(framerateManagerInstance);
+	return *framerateManagerInstance;
+}
+
+bool CGuiHandler::isKeyboardCtrlDown() const
+{
+	return inputHandlerInstance->isKeyboardCtrlDown();
+}
+
+bool CGuiHandler::isKeyboardAltDown() const
+{
+	return inputHandlerInstance->isKeyboardAltDown();
+}
+
+bool CGuiHandler::isKeyboardShiftDown() const
+{
+	return inputHandlerInstance->isKeyboardShiftDown();
+}
+
+const Point & CGuiHandler::getCursorPosition() const
+{
+	return inputHandlerInstance->getCursorPosition();
+}
+
+Point CGuiHandler::screenDimensions() const
+{
+	return Point(screen->w, screen->h);
+}
+
+void CGuiHandler::drawFPSCounter()
+{
+	static SDL_Rect overlay = { 0, 0, 24, 24};
+	uint32_t black = SDL_MapRGB(screen->format, 10, 10, 10);
+	SDL_FillRect(screen, &overlay, black);
+	std::string fps = std::to_string(framerate().getFramerate());
+	graphics->fonts[FONT_BIG]->renderTextLeft(screen, fps, Colors::YELLOW, Point(4, 2));
+}
+
+bool CGuiHandler::amIGuiThread()
+{
+	return inGuiThread;
+}
+
+void CGuiHandler::dispatchMainThread(const std::function<void()> & functor)
+{
+	inputHandlerInstance->dispatchMainThread(functor);
+}
+
+IScreenHandler & CGuiHandler::screenHandler()
+{
+	return *screenHandlerInstance;
+}
+
+IRenderHandler & CGuiHandler::renderHandler()
+{
+	return *renderHandlerInstance;
+}
+
+EventDispatcher & CGuiHandler::events()
+{
+	return *eventDispatcherInstance;
+}
+
+InputHandler & CGuiHandler::input()
+{
+	return *inputHandlerInstance;
+}
+
+WindowHandler & CGuiHandler::windows()
+{
+	assert(windowHandlerInstance);
+	return *windowHandlerInstance;
+}
+
+std::shared_ptr<IStatusBar> CGuiHandler::statusbar()
+{
+	auto locked = currentStatusBar.lock();
+
+	if (!locked)
+		return fakeStatusBar;
+
+	return locked;
+}
+
+void CGuiHandler::setStatusbar(std::shared_ptr<IStatusBar> newStatusBar)
+{
+	currentStatusBar = newStatusBar;
+}
+
+void CGuiHandler::onScreenResize()
+{
+	screenHandler().onScreenResize();
+	windows().onScreenResize();
+}

+ 130 - 130
client/gui/CGuiHandler.h

@@ -1,130 +1,130 @@
-/*
- * CGuiHandler.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
-template <typename T> struct CondSh;
-class Point;
-class Rect;
-VCMI_LIB_NAMESPACE_END
-
-enum class MouseButton;
-class ShortcutHandler;
-class FramerateManager;
-class IStatusBar;
-class CIntObject;
-class IUpdateable;
-class IShowActivatable;
-class IRenderHandler;
-class IScreenHandler;
-class WindowHandler;
-class EventDispatcher;
-class InputHandler;
-
-// Handles GUI logic and drawing
-class CGuiHandler
-{
-private:
-	/// Fake no-op version status bar, for use in windows that have no status bar
-	std::shared_ptr<IStatusBar> fakeStatusBar;
-
-	/// 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::unique_ptr<IScreenHandler> screenHandlerInstance;
-	std::unique_ptr<IRenderHandler> renderHandlerInstance;
-	std::unique_ptr<FramerateManager> framerateManagerInstance;
-	std::unique_ptr<EventDispatcher> eventDispatcherInstance;
-	std::unique_ptr<InputHandler> inputHandlerInstance;
-
-public:
-	boost::mutex interfaceMutex;
-
-	/// returns current position of mouse cursor, relative to vcmi window
-	const Point & getCursorPosition() const;
-
-	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 chosen keyboard key is currently pressed down
-	bool isKeyboardAltDown() const;
-	bool isKeyboardCtrlDown() const;
-	bool isKeyboardShiftDown() const;
-
-	void startTextInput(const Rect & where);
-	void stopTextInput();
-
-	IScreenHandler & screenHandler();
-	IRenderHandler & renderHandler();
-	WindowHandler & windows();
-
-	/// Returns currently active status bar. Guaranteed to be non-null
-	std::shared_ptr<IStatusBar> statusbar();
-
-	/// 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
-	std::list<CIntObject *> createdObj; //stack of objs being created
-
-	CGuiHandler();
-	~CGuiHandler();
-
-	void init();
-	void renderFrame();
-
-	/// called whenever user selects different resolution, requiring to center/resize all windows
-	void onScreenResize();
-
-	void handleEvents(); //takes events from queue and calls interested objects
-	void fakeMouseMove();
-	void drawFPSCounter(); // draws the FPS to the upper left corner of the screen
-
-	bool amIGuiThread();
-
-	/// Calls provided functor in main thread on next execution frame
-	void dispatchMainThread(const std::function<void()> & functor);
-};
-
-extern CGuiHandler GH; //global gui handler
-
-struct SObjectConstruction
-{
-	CIntObject *myObj;
-	SObjectConstruction(CIntObject *obj);
-	~SObjectConstruction();
-};
-
-struct SSetCaptureState
-{
-	bool previousCapture;
-	ui8 prevActions;
-	SSetCaptureState(bool allow, ui8 actions);
-	~SSetCaptureState();
-};
-
-#define OBJ_CONSTRUCTION SObjectConstruction obj__i(this)
-#define OBJ_CONSTRUCTION_TARGETED(obj) SObjectConstruction obj__i(obj)
-#define OBJECT_CONSTRUCTION_CAPTURING(actions) defActions = actions; SSetCaptureState obj__i1(true, actions); SObjectConstruction obj__i(this)
-#define OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(actions) SSetCaptureState obj__i1(true, actions); SObjectConstruction obj__i(this)
-
-#define OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE defActions = 255 - DISPOSE; SSetCaptureState obj__i1(true, 255 - DISPOSE); SObjectConstruction obj__i(this)
+/*
+ * CGuiHandler.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
+template <typename T> struct CondSh;
+class Point;
+class Rect;
+VCMI_LIB_NAMESPACE_END
+
+enum class MouseButton;
+class ShortcutHandler;
+class FramerateManager;
+class IStatusBar;
+class CIntObject;
+class IUpdateable;
+class IShowActivatable;
+class IRenderHandler;
+class IScreenHandler;
+class WindowHandler;
+class EventDispatcher;
+class InputHandler;
+
+// Handles GUI logic and drawing
+class CGuiHandler
+{
+private:
+	/// Fake no-op version status bar, for use in windows that have no status bar
+	std::shared_ptr<IStatusBar> fakeStatusBar;
+
+	/// 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::unique_ptr<IScreenHandler> screenHandlerInstance;
+	std::unique_ptr<IRenderHandler> renderHandlerInstance;
+	std::unique_ptr<FramerateManager> framerateManagerInstance;
+	std::unique_ptr<EventDispatcher> eventDispatcherInstance;
+	std::unique_ptr<InputHandler> inputHandlerInstance;
+
+public:
+	boost::mutex interfaceMutex;
+
+	/// returns current position of mouse cursor, relative to vcmi window
+	const Point & getCursorPosition() const;
+
+	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 chosen keyboard key is currently pressed down
+	bool isKeyboardAltDown() const;
+	bool isKeyboardCtrlDown() const;
+	bool isKeyboardShiftDown() const;
+
+	void startTextInput(const Rect & where);
+	void stopTextInput();
+
+	IScreenHandler & screenHandler();
+	IRenderHandler & renderHandler();
+	WindowHandler & windows();
+
+	/// Returns currently active status bar. Guaranteed to be non-null
+	std::shared_ptr<IStatusBar> statusbar();
+
+	/// 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
+	std::list<CIntObject *> createdObj; //stack of objs being created
+
+	CGuiHandler();
+	~CGuiHandler();
+
+	void init();
+	void renderFrame();
+
+	/// called whenever user selects different resolution, requiring to center/resize all windows
+	void onScreenResize();
+
+	void handleEvents(); //takes events from queue and calls interested objects
+	void fakeMouseMove();
+	void drawFPSCounter(); // draws the FPS to the upper left corner of the screen
+
+	bool amIGuiThread();
+
+	/// Calls provided functor in main thread on next execution frame
+	void dispatchMainThread(const std::function<void()> & functor);
+};
+
+extern CGuiHandler GH; //global gui handler
+
+struct SObjectConstruction
+{
+	CIntObject *myObj;
+	SObjectConstruction(CIntObject *obj);
+	~SObjectConstruction();
+};
+
+struct SSetCaptureState
+{
+	bool previousCapture;
+	ui8 prevActions;
+	SSetCaptureState(bool allow, ui8 actions);
+	~SSetCaptureState();
+};
+
+#define OBJ_CONSTRUCTION SObjectConstruction obj__i(this)
+#define OBJ_CONSTRUCTION_TARGETED(obj) SObjectConstruction obj__i(obj)
+#define OBJECT_CONSTRUCTION_CAPTURING(actions) defActions = actions; SSetCaptureState obj__i1(true, actions); SObjectConstruction obj__i(this)
+#define OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(actions) SSetCaptureState obj__i1(true, actions); SObjectConstruction obj__i(this)
+
+#define OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE defActions = 255 - DISPOSE; SSetCaptureState obj__i1(true, 255 - DISPOSE); SObjectConstruction obj__i(this)

+ 346 - 346
client/gui/CIntObject.cpp

@@ -1,346 +1,346 @@
-/*
- * CIntObject.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 "CIntObject.h"
-
-#include "CGuiHandler.h"
-#include "WindowHandler.h"
-#include "EventDispatcher.h"
-#include "Shortcut.h"
-#include "../render/Canvas.h"
-#include "../windows/CMessage.h"
-#include "../CMT.h"
-
-CIntObject::CIntObject(int used_, Point pos_):
-	parent_m(nullptr),
-	parent(parent_m),
-	redrawParent(false),
-	inputEnabled(true),
-	used(used_),
-	recActions(GH.defActionsDef),
-	defActions(GH.defActionsDef),
-	pos(pos_, Point())
-{
-	if(GH.captureChildren)
-		GH.createdObj.front()->addChild(this, true);
-}
-
-CIntObject::~CIntObject()
-{
-	if(isActive())
-		deactivate();
-
-	while(!children.empty())
-	{
-		if((defActions & DISPOSE) && (children.front()->recActions & DISPOSE))
-			delete children.front();
-		else
-			removeChild(children.front());
-	}
-
-	if(parent_m)
-		parent_m->removeChild(this);
-}
-
-void CIntObject::show(Canvas & to)
-{
-	if(defActions & UPDATE)
-		for(auto & elem : children)
-			if(elem->recActions & UPDATE)
-				elem->show(to);
-}
-
-void CIntObject::showAll(Canvas & to)
-{
-	if(defActions & SHOWALL)
-	{
-		for(auto & elem : children)
-			if(elem->recActions & SHOWALL)
-				elem->showAll(to);
-	}
-}
-
-void CIntObject::activate()
-{
-	if (isActive())
-		return;
-
-	if (inputEnabled)
-		activateEvents(used | GENERAL);
-	else
-		activateEvents(GENERAL);
-
-	assert(isActive());
-
-	if(defActions & ACTIVATE)
-		for(auto & elem : children)
-			if(elem->recActions & ACTIVATE)
-				elem->activate();
-}
-
-void CIntObject::deactivate()
-{
-	if (!isActive())
-		return;
-
-	deactivateEvents(used | GENERAL);
-
-	assert(!isActive());
-
-	if(defActions & DEACTIVATE)
-		for(auto & elem : children)
-			if(elem->recActions & DEACTIVATE)
-				elem->deactivate();
-}
-
-void CIntObject::addUsedEvents(ui16 newActions)
-{
-	if (isActive() && inputEnabled)
-		activateEvents(~used & newActions);
-	used |= newActions;
-}
-
-void CIntObject::removeUsedEvents(ui16 newActions)
-{
-	if (isActive())
-		deactivateEvents(used & newActions);
-	used &= ~newActions;
-}
-
-void CIntObject::disable()
-{
-	if(isActive())
-		deactivate();
-
-	recActions = DISPOSE;
-}
-
-void CIntObject::enable()
-{
-	if(!isActive() && (!parent_m || parent_m->isActive()))
-	{
-		activate();
-		redraw();
-	}
-
-	recActions = 255;
-}
-
-void CIntObject::setEnabled(bool on)
-{
-	if (on)
-		enable();
-	else
-		disable();
-}
-
-void CIntObject::setInputEnabled(bool on)
-{
-	if (inputEnabled == on)
-		return;
-
-	inputEnabled = on;
-
-	if (isActive())
-	{
-		assert((used & GENERAL) == 0);
-
-		if (on)
-			activateEvents(used);
-		else
-			deactivateEvents(used);
-	}
-
-	for(auto & elem : children)
-		elem->setInputEnabled(on);
-}
-
-void CIntObject::setRedrawParent(bool on)
-{
-	redrawParent = on;
-}
-
-void CIntObject::fitToScreen(int borderWidth, bool propagate)
-{
-	Point newPos = pos.topLeft();
-	vstd::amax(newPos.x, borderWidth);
-	vstd::amax(newPos.y, borderWidth);
-	vstd::amin(newPos.x, GH.screenDimensions().x - borderWidth - pos.w);
-	vstd::amin(newPos.y, GH.screenDimensions().y - borderWidth - pos.h);
-	if (newPos != pos.topLeft())
-		moveTo(newPos, propagate);
-}
-
-void CIntObject::moveBy(const Point & p, bool propagate)
-{
-	pos.x += p.x;
-	pos.y += p.y;
-	if(propagate)
-		for(auto & elem : children)
-			elem->moveBy(p, propagate);
-}
-
-void CIntObject::moveTo(const Point & p, bool propagate)
-{
-	moveBy(Point(p.x - pos.x, p.y - pos.y), propagate);
-}
-
-void CIntObject::addChild(CIntObject * child, bool adjustPosition)
-{
-	if (vstd::contains(children, child))
-	{
-		return;
-	}
-	if (child->parent_m)
-	{
-		child->parent_m->removeChild(child, adjustPosition);
-	}
-	children.push_back(child);
-	child->parent_m = this;
-	if(adjustPosition)
-		child->moveBy(pos.topLeft(), adjustPosition);
-
-	if (inputEnabled != child->inputEnabled)
-		child->setInputEnabled(inputEnabled);
-
-	if (!isActive() && child->isActive())
-		child->deactivate();
-	if (isActive()&& !child->isActive())
-		child->activate();
-}
-
-void CIntObject::removeChild(CIntObject * child, bool adjustPosition)
-{
-	if (!child)
-		return;
-
-	if(!vstd::contains(children, child))
-		throw std::runtime_error("Wrong child object");
-
-	if(child->parent_m != this)
-		throw std::runtime_error("Wrong child object");
-
-	children -= child;
-	child->parent_m = nullptr;
-	if(adjustPosition)
-		child->pos -= pos.topLeft();
-}
-
-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 (isActive())
-	{
-		if (parent_m && redrawParent)
-		{
-			parent_m->redraw();
-		}
-		else
-		{
-			Canvas buffer = Canvas::createFromSurface(screenBuf);
-
-			showAll(buffer);
-			if(screenBuf != screen)
-			{
-				Canvas screenBuffer = Canvas::createFromSurface(screen);
-
-				showAll(screenBuffer);
-			}
-		}
-	}
-}
-
-bool CIntObject::receiveEvent(const Point & position, int eventType) const
-{
-	return pos.isInside(position);
-}
-
-const Rect & CIntObject::getPosition() const
-{
-	return pos;
-}
-
-void CIntObject::onScreenResize()
-{
-	center(pos, true);
-}
-
-bool CIntObject::isPopupWindow() const
-{
-	return false;
-}
-
-const Rect & CIntObject::center( const Rect &r, bool propagate )
-{
-	pos.w = r.w;
-	pos.h = r.h;
-	return center(Point(GH.screenDimensions().x/2, GH.screenDimensions().y/2), propagate);
-}
-
-const Rect & CIntObject::center( bool propagate )
-{
-	return center(pos, propagate);
-}
-
-const Rect & CIntObject::center(const Point & p, bool propagate)
-{
-	moveBy(Point(p.x - pos.w/2 - pos.x,
-		p.y - pos.h/2 - pos.y),
-		propagate);
-	return pos;
-}
-
-bool CIntObject::captureThisKey(EShortcut key)
-{
-	return false;
-}
-
-CKeyShortcut::CKeyShortcut()
-	: assignedKey(EShortcut::NONE)
-	, shortcutPressed(false)
-{}
-
-CKeyShortcut::CKeyShortcut(EShortcut key)
-	: assignedKey(key)
-	, shortcutPressed(false)
-{
-}
-
-void CKeyShortcut::keyPressed(EShortcut key)
-{
-	if( assignedKey == key && assignedKey != EShortcut::NONE && !shortcutPressed)
-	{
-		shortcutPressed = true;
-		clickPressed(GH.getCursorPosition());
-	}
-}
-
-void CKeyShortcut::keyReleased(EShortcut key)
-{
-	if( assignedKey == key && assignedKey != EShortcut::NONE && shortcutPressed)
-	{
-		shortcutPressed = false;
-		clickReleased(GH.getCursorPosition());
-	}
-}
-
-WindowBase::WindowBase(int used_, Point pos_)
-	: CIntObject(used_, pos_)
-{
-
-}
-
-void WindowBase::close()
-{
-	if(!GH.windows().isTopWindow(this))
-		logGlobal->error("Only top interface must be closed");
-	GH.windows().popWindows(1);
-}
+/*
+ * CIntObject.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 "CIntObject.h"
+
+#include "CGuiHandler.h"
+#include "WindowHandler.h"
+#include "EventDispatcher.h"
+#include "Shortcut.h"
+#include "../render/Canvas.h"
+#include "../windows/CMessage.h"
+#include "../CMT.h"
+
+CIntObject::CIntObject(int used_, Point pos_):
+	parent_m(nullptr),
+	parent(parent_m),
+	redrawParent(false),
+	inputEnabled(true),
+	used(used_),
+	recActions(GH.defActionsDef),
+	defActions(GH.defActionsDef),
+	pos(pos_, Point())
+{
+	if(GH.captureChildren)
+		GH.createdObj.front()->addChild(this, true);
+}
+
+CIntObject::~CIntObject()
+{
+	if(isActive())
+		deactivate();
+
+	while(!children.empty())
+	{
+		if((defActions & DISPOSE) && (children.front()->recActions & DISPOSE))
+			delete children.front();
+		else
+			removeChild(children.front());
+	}
+
+	if(parent_m)
+		parent_m->removeChild(this);
+}
+
+void CIntObject::show(Canvas & to)
+{
+	if(defActions & UPDATE)
+		for(auto & elem : children)
+			if(elem->recActions & UPDATE)
+				elem->show(to);
+}
+
+void CIntObject::showAll(Canvas & to)
+{
+	if(defActions & SHOWALL)
+	{
+		for(auto & elem : children)
+			if(elem->recActions & SHOWALL)
+				elem->showAll(to);
+	}
+}
+
+void CIntObject::activate()
+{
+	if (isActive())
+		return;
+
+	if (inputEnabled)
+		activateEvents(used | GENERAL);
+	else
+		activateEvents(GENERAL);
+
+	assert(isActive());
+
+	if(defActions & ACTIVATE)
+		for(auto & elem : children)
+			if(elem->recActions & ACTIVATE)
+				elem->activate();
+}
+
+void CIntObject::deactivate()
+{
+	if (!isActive())
+		return;
+
+	deactivateEvents(used | GENERAL);
+
+	assert(!isActive());
+
+	if(defActions & DEACTIVATE)
+		for(auto & elem : children)
+			if(elem->recActions & DEACTIVATE)
+				elem->deactivate();
+}
+
+void CIntObject::addUsedEvents(ui16 newActions)
+{
+	if (isActive() && inputEnabled)
+		activateEvents(~used & newActions);
+	used |= newActions;
+}
+
+void CIntObject::removeUsedEvents(ui16 newActions)
+{
+	if (isActive())
+		deactivateEvents(used & newActions);
+	used &= ~newActions;
+}
+
+void CIntObject::disable()
+{
+	if(isActive())
+		deactivate();
+
+	recActions = DISPOSE;
+}
+
+void CIntObject::enable()
+{
+	if(!isActive() && (!parent_m || parent_m->isActive()))
+	{
+		activate();
+		redraw();
+	}
+
+	recActions = 255;
+}
+
+void CIntObject::setEnabled(bool on)
+{
+	if (on)
+		enable();
+	else
+		disable();
+}
+
+void CIntObject::setInputEnabled(bool on)
+{
+	if (inputEnabled == on)
+		return;
+
+	inputEnabled = on;
+
+	if (isActive())
+	{
+		assert((used & GENERAL) == 0);
+
+		if (on)
+			activateEvents(used);
+		else
+			deactivateEvents(used);
+	}
+
+	for(auto & elem : children)
+		elem->setInputEnabled(on);
+}
+
+void CIntObject::setRedrawParent(bool on)
+{
+	redrawParent = on;
+}
+
+void CIntObject::fitToScreen(int borderWidth, bool propagate)
+{
+	Point newPos = pos.topLeft();
+	vstd::amax(newPos.x, borderWidth);
+	vstd::amax(newPos.y, borderWidth);
+	vstd::amin(newPos.x, GH.screenDimensions().x - borderWidth - pos.w);
+	vstd::amin(newPos.y, GH.screenDimensions().y - borderWidth - pos.h);
+	if (newPos != pos.topLeft())
+		moveTo(newPos, propagate);
+}
+
+void CIntObject::moveBy(const Point & p, bool propagate)
+{
+	pos.x += p.x;
+	pos.y += p.y;
+	if(propagate)
+		for(auto & elem : children)
+			elem->moveBy(p, propagate);
+}
+
+void CIntObject::moveTo(const Point & p, bool propagate)
+{
+	moveBy(Point(p.x - pos.x, p.y - pos.y), propagate);
+}
+
+void CIntObject::addChild(CIntObject * child, bool adjustPosition)
+{
+	if (vstd::contains(children, child))
+	{
+		return;
+	}
+	if (child->parent_m)
+	{
+		child->parent_m->removeChild(child, adjustPosition);
+	}
+	children.push_back(child);
+	child->parent_m = this;
+	if(adjustPosition)
+		child->moveBy(pos.topLeft(), adjustPosition);
+
+	if (inputEnabled != child->inputEnabled)
+		child->setInputEnabled(inputEnabled);
+
+	if (!isActive() && child->isActive())
+		child->deactivate();
+	if (isActive()&& !child->isActive())
+		child->activate();
+}
+
+void CIntObject::removeChild(CIntObject * child, bool adjustPosition)
+{
+	if (!child)
+		return;
+
+	if(!vstd::contains(children, child))
+		throw std::runtime_error("Wrong child object");
+
+	if(child->parent_m != this)
+		throw std::runtime_error("Wrong child object");
+
+	children -= child;
+	child->parent_m = nullptr;
+	if(adjustPosition)
+		child->pos -= pos.topLeft();
+}
+
+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 (isActive())
+	{
+		if (parent_m && redrawParent)
+		{
+			parent_m->redraw();
+		}
+		else
+		{
+			Canvas buffer = Canvas::createFromSurface(screenBuf);
+
+			showAll(buffer);
+			if(screenBuf != screen)
+			{
+				Canvas screenBuffer = Canvas::createFromSurface(screen);
+
+				showAll(screenBuffer);
+			}
+		}
+	}
+}
+
+bool CIntObject::receiveEvent(const Point & position, int eventType) const
+{
+	return pos.isInside(position);
+}
+
+const Rect & CIntObject::getPosition() const
+{
+	return pos;
+}
+
+void CIntObject::onScreenResize()
+{
+	center(pos, true);
+}
+
+bool CIntObject::isPopupWindow() const
+{
+	return false;
+}
+
+const Rect & CIntObject::center( const Rect &r, bool propagate )
+{
+	pos.w = r.w;
+	pos.h = r.h;
+	return center(Point(GH.screenDimensions().x/2, GH.screenDimensions().y/2), propagate);
+}
+
+const Rect & CIntObject::center( bool propagate )
+{
+	return center(pos, propagate);
+}
+
+const Rect & CIntObject::center(const Point & p, bool propagate)
+{
+	moveBy(Point(p.x - pos.w/2 - pos.x,
+		p.y - pos.h/2 - pos.y),
+		propagate);
+	return pos;
+}
+
+bool CIntObject::captureThisKey(EShortcut key)
+{
+	return false;
+}
+
+CKeyShortcut::CKeyShortcut()
+	: assignedKey(EShortcut::NONE)
+	, shortcutPressed(false)
+{}
+
+CKeyShortcut::CKeyShortcut(EShortcut key)
+	: assignedKey(key)
+	, shortcutPressed(false)
+{
+}
+
+void CKeyShortcut::keyPressed(EShortcut key)
+{
+	if( assignedKey == key && assignedKey != EShortcut::NONE && !shortcutPressed)
+	{
+		shortcutPressed = true;
+		clickPressed(GH.getCursorPosition());
+	}
+}
+
+void CKeyShortcut::keyReleased(EShortcut key)
+{
+	if( assignedKey == key && assignedKey != EShortcut::NONE && shortcutPressed)
+	{
+		shortcutPressed = false;
+		clickReleased(GH.getCursorPosition());
+	}
+}
+
+WindowBase::WindowBase(int used_, Point pos_)
+	: CIntObject(used_, pos_)
+{
+
+}
+
+void WindowBase::close()
+{
+	if(!GH.windows().isTopWindow(this))
+		logGlobal->error("Only top interface must be closed");
+	GH.windows().popWindows(1);
+}

+ 191 - 191
client/gui/CIntObject.h

@@ -1,191 +1,191 @@
-/*
- * CIntObject.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 "EventsReceiver.h"
-
-#include "../../lib/Rect.h"
-#include "../../lib/Color.h"
-#include "../../lib/GameConstants.h"
-
-class CGuiHandler;
-class CPicture;
-class Canvas;
-
-class IUpdateable
-{
-public:
-	virtual void update()=0;
-	virtual ~IUpdateable() = default;
-};
-
-class IShowActivatable
-{
-public:
-	virtual void activate()=0;
-	virtual void deactivate()=0;
-
-	virtual void redraw()=0;
-	virtual void show(Canvas & to) = 0;
-	virtual void showAll(Canvas & to) = 0;
-
-	virtual bool isPopupWindow() const = 0;
-	virtual void onScreenResize() = 0;
-	virtual ~IShowActivatable() = default;
-};
-
-// Base UI element
-class CIntObject : public IShowActivatable, public AEventsReceiver //interface object
-{
-	ui16 used;
-
-	//non-const versions of fields to allow changing them in CIntObject
-	CIntObject *parent_m; //parent object
-
-	bool inputEnabled;
-	bool redrawParent;
-
-public:
-	std::vector<CIntObject *> children;
-
-	/// read-only parent access. May not be a "clean" solution but allows some compatibility
-	CIntObject * const & parent;
-
-	/// position of object on the screen. Please do not modify this anywhere but in constructor - use moveBy\moveTo instead
-	/*const*/ Rect pos;
-
-	CIntObject(int used=0, Point offset=Point());
-	virtual ~CIntObject();
-
-	bool captureThisKey(EShortcut key) override;
-
-	void addUsedEvents(ui16 newActions);
-	void removeUsedEvents(ui16 newActions);
-
-	enum {ACTIVATE=1, DEACTIVATE=2, UPDATE=4, SHOWALL=8, DISPOSE=16, SHARE_POS=32};
-	ui8 defActions; //which calls will be tried to be redirected to children
-	ui8 recActions; //which calls we allow to receive from parent
-
-	/// 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);
-
-	/// Block (or allow) all user input, e.g. mouse/keyboard/touch without hiding element
-	void setInputEnabled(bool on);
-
-	/// Mark this input as one that requires parent redraw on update,
-	/// for example if current control might have semi-transparent elements and requires redrawing of background
-	void setRedrawParent(bool on);
-
-	// activate or deactivate object. Inactive object won't receive any input events (keyboard\mouse)
-	// usually used automatically by parent
-	void activate() override;
-	void deactivate() override;
-
-	//called each frame to update screen
-	void show(Canvas & to) override;
-	//called on complete redraw only
-	void showAll(Canvas & to) override;
-	//request complete redraw of this object
-	void redraw() override;
-
-	/// returns true if this element is a popup window
-	/// called only for windows
-	bool isPopupWindow() const override;
-
-	/// called only for windows whenever screen size changes
-	/// default behavior is to re-center, can be overriden
-	void onScreenResize() override;
-
-	/// returns true if UI elements wants to handle event of specific type (LCLICK, SHOW_POPUP ...)
-	/// by default, usedEvents inside UI elements are always handled
-	bool receiveEvent(const Point & position, int eventType) const override;
-
-	const Rect & getPosition() const 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
-	void fitToScreen(int borderWidth, bool propagate = true); //moves window to fit into screen
-	void moveBy(const Point &p, bool propagate = true);
-	void moveTo(const Point &p, bool propagate = true);//move this to new position, coordinates are absolute (0,0 is topleft screen corner)
-
-	void addChild(CIntObject *child, bool adjustPosition = false);
-	void removeChild(CIntObject *child, bool adjustPosition = false);
-
-};
-
-/// 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();
-	CKeyShortcut(EShortcut key);
-	void keyPressed(EShortcut key) override;
-	void keyReleased(EShortcut key) override;
-
-};
-
-class WindowBase : public CIntObject
-{
-public:
-	WindowBase(int used_ = 0, Point pos_ = Point());
-protected:
-	virtual void close();
-};
-
-class IGarrisonHolder
-{
-public:
-	virtual void updateGarrisons() = 0;
-};
-
-class ITownHolder
-{
-public:
-	virtual void buildChanged() = 0;
-};
-
-class IStatusBar
-{
-public:
-	virtual ~IStatusBar() = default;
-
-	/// set current text for the status bar
-	virtual void write(const std::string & text) = 0;
-
-	/// remove any current text from the status bar
-	virtual void clear() = 0;
-
-	/// remove text from status bar if current text matches tested text
-	virtual void clearIfMatching(const std::string & testedText) = 0;
-
-	/// enables mode for entering text instead of showing hover text
-	virtual void setEnteringMode(bool on) = 0;
-
-	/// overrides hover text from controls with text entered into in-game console (for chat/cheats)
-	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){};
-};
+/*
+ * CIntObject.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 "EventsReceiver.h"
+
+#include "../../lib/Rect.h"
+#include "../../lib/Color.h"
+#include "../../lib/GameConstants.h"
+
+class CGuiHandler;
+class CPicture;
+class Canvas;
+
+class IUpdateable
+{
+public:
+	virtual void update()=0;
+	virtual ~IUpdateable() = default;
+};
+
+class IShowActivatable
+{
+public:
+	virtual void activate()=0;
+	virtual void deactivate()=0;
+
+	virtual void redraw()=0;
+	virtual void show(Canvas & to) = 0;
+	virtual void showAll(Canvas & to) = 0;
+
+	virtual bool isPopupWindow() const = 0;
+	virtual void onScreenResize() = 0;
+	virtual ~IShowActivatable() = default;
+};
+
+// Base UI element
+class CIntObject : public IShowActivatable, public AEventsReceiver //interface object
+{
+	ui16 used;
+
+	//non-const versions of fields to allow changing them in CIntObject
+	CIntObject *parent_m; //parent object
+
+	bool inputEnabled;
+	bool redrawParent;
+
+public:
+	std::vector<CIntObject *> children;
+
+	/// read-only parent access. May not be a "clean" solution but allows some compatibility
+	CIntObject * const & parent;
+
+	/// position of object on the screen. Please do not modify this anywhere but in constructor - use moveBy\moveTo instead
+	/*const*/ Rect pos;
+
+	CIntObject(int used=0, Point offset=Point());
+	virtual ~CIntObject();
+
+	bool captureThisKey(EShortcut key) override;
+
+	void addUsedEvents(ui16 newActions);
+	void removeUsedEvents(ui16 newActions);
+
+	enum {ACTIVATE=1, DEACTIVATE=2, UPDATE=4, SHOWALL=8, DISPOSE=16, SHARE_POS=32};
+	ui8 defActions; //which calls will be tried to be redirected to children
+	ui8 recActions; //which calls we allow to receive from parent
+
+	/// 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);
+
+	/// Block (or allow) all user input, e.g. mouse/keyboard/touch without hiding element
+	void setInputEnabled(bool on);
+
+	/// Mark this input as one that requires parent redraw on update,
+	/// for example if current control might have semi-transparent elements and requires redrawing of background
+	void setRedrawParent(bool on);
+
+	// activate or deactivate object. Inactive object won't receive any input events (keyboard\mouse)
+	// usually used automatically by parent
+	void activate() override;
+	void deactivate() override;
+
+	//called each frame to update screen
+	void show(Canvas & to) override;
+	//called on complete redraw only
+	void showAll(Canvas & to) override;
+	//request complete redraw of this object
+	void redraw() override;
+
+	/// returns true if this element is a popup window
+	/// called only for windows
+	bool isPopupWindow() const override;
+
+	/// called only for windows whenever screen size changes
+	/// default behavior is to re-center, can be overriden
+	void onScreenResize() override;
+
+	/// returns true if UI elements wants to handle event of specific type (LCLICK, SHOW_POPUP ...)
+	/// by default, usedEvents inside UI elements are always handled
+	bool receiveEvent(const Point & position, int eventType) const override;
+
+	const Rect & getPosition() const 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
+	void fitToScreen(int borderWidth, bool propagate = true); //moves window to fit into screen
+	void moveBy(const Point &p, bool propagate = true);
+	void moveTo(const Point &p, bool propagate = true);//move this to new position, coordinates are absolute (0,0 is topleft screen corner)
+
+	void addChild(CIntObject *child, bool adjustPosition = false);
+	void removeChild(CIntObject *child, bool adjustPosition = false);
+
+};
+
+/// 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();
+	CKeyShortcut(EShortcut key);
+	void keyPressed(EShortcut key) override;
+	void keyReleased(EShortcut key) override;
+
+};
+
+class WindowBase : public CIntObject
+{
+public:
+	WindowBase(int used_ = 0, Point pos_ = Point());
+protected:
+	virtual void close();
+};
+
+class IGarrisonHolder
+{
+public:
+	virtual void updateGarrisons() = 0;
+};
+
+class ITownHolder
+{
+public:
+	virtual void buildChanged() = 0;
+};
+
+class IStatusBar
+{
+public:
+	virtual ~IStatusBar() = default;
+
+	/// set current text for the status bar
+	virtual void write(const std::string & text) = 0;
+
+	/// remove any current text from the status bar
+	virtual void clear() = 0;
+
+	/// remove text from status bar if current text matches tested text
+	virtual void clearIfMatching(const std::string & testedText) = 0;
+
+	/// enables mode for entering text instead of showing hover text
+	virtual void setEnteringMode(bool on) = 0;
+
+	/// overrides hover text from controls with text entered into in-game console (for chat/cheats)
+	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){};
+};

+ 292 - 292
client/gui/CursorHandler.cpp

@@ -1,292 +1,292 @@
-/*
- * CCursorHandler.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 "CursorHandler.h"
-
-#include "CGuiHandler.h"
-#include "FramerateManager.h"
-#include "../renderSDL/CursorSoftware.h"
-#include "../renderSDL/CursorHardware.h"
-#include "../render/CAnimation.h"
-#include "../render/IImage.h"
-#include "../render/IRenderHandler.h"
-
-#include "../../lib/CConfigHandler.h"
-
-std::unique_ptr<ICursor> CursorHandler::createCursor()
-{
-#if defined(VCMI_MOBILE)
-	if (settings["general"]["userRelativePointer"].Bool())
-		return std::make_unique<CursorSoftware>();
-#endif
-
-	if (settings["video"]["cursor"].String() == "hardware")
-		return std::make_unique<CursorHardware>();
-
-	assert(settings["video"]["cursor"].String() == "software");
-	return std::make_unique<CursorSoftware>();
-}
-
-CursorHandler::CursorHandler()
-	: cursor(createCursor())
-	, frameTime(0.f)
-	, showing(false)
-	, pos(0,0)
-{
-
-	type = Cursor::Type::DEFAULT;
-	dndObject = nullptr;
-
-	cursors =
-	{
-		GH.renderHandler().loadAnimation(AnimationPath::builtin("CRADVNTR")),
-		GH.renderHandler().loadAnimation(AnimationPath::builtin("CRCOMBAT")),
-		GH.renderHandler().loadAnimation(AnimationPath::builtin("CRDEFLT")),
-		GH.renderHandler().loadAnimation(AnimationPath::builtin("CRSPELL"))
-	};
-
-	for (auto & cursor : cursors)
-		cursor->preload();
-
-	set(Cursor::Map::POINTER);
-}
-
-CursorHandler::~CursorHandler() = default;
-
-void CursorHandler::changeGraphic(Cursor::Type type, size_t index)
-{
-	assert(dndObject == nullptr);
-
-	if (type == this->type && index == this->frame)
-		return;
-
-	this->type = type;
-	this->frame = index;
-
-	cursor->setImage(getCurrentImage(), getPivotOffset());
-}
-
-void CursorHandler::set(Cursor::Default index)
-{
-	changeGraphic(Cursor::Type::DEFAULT, static_cast<size_t>(index));
-}
-
-void CursorHandler::set(Cursor::Map index)
-{
-	changeGraphic(Cursor::Type::ADVENTURE, static_cast<size_t>(index));
-}
-
-void CursorHandler::set(Cursor::Combat index)
-{
-	changeGraphic(Cursor::Type::COMBAT, static_cast<size_t>(index));
-}
-
-void CursorHandler::set(Cursor::Spellcast index)
-{
-	//Note: this is animated cursor, ignore specified frame and only change type
-	changeGraphic(Cursor::Type::SPELLBOOK, frame);
-}
-
-void CursorHandler::dragAndDropCursor(std::shared_ptr<IImage> image)
-{
-	dndObject = image;
-	cursor->setImage(getCurrentImage(), getPivotOffset());
-}
-
-void CursorHandler::dragAndDropCursor (const AnimationPath & path, size_t index)
-{
-	auto anim = GH.renderHandler().loadAnimation(path);
-	anim->load(index);
-	dragAndDropCursor(anim->getImage(index));
-}
-
-void CursorHandler::cursorMove(const int & x, const int & y)
-{
-	pos.x = x;
-	pos.y = y;
-
-	cursor->setCursorPosition(pos);
-}
-
-Point CursorHandler::getPivotOffsetDefault(size_t index)
-{
-	return {0, 0};
-}
-
-Point CursorHandler::getPivotOffsetMap(size_t index)
-{
-	static const std::array<Point, 43> offsets = {{
-		{  0,  0}, // POINTER          =  0,
-		{  0,  0}, // HOURGLASS        =  1,
-		{ 12, 10}, // HERO             =  2,
-		{ 12, 12}, // TOWN             =  3,
-
-		{ 15, 13}, // T1_MOVE          =  4,
-		{ 13, 13}, // T1_ATTACK        =  5,
-		{ 16, 32}, // T1_SAIL          =  6,
-		{ 13, 20}, // T1_DISEMBARK     =  7,
-		{  8,  9}, // T1_EXCHANGE      =  8,
-		{ 14, 16}, // T1_VISIT         =  9,
-
-		{ 15, 13}, // T2_MOVE          = 10,
-		{ 13, 13}, // T2_ATTACK        = 11,
-		{ 16, 32}, // T2_SAIL          = 12,
-		{ 13, 20}, // T2_DISEMBARK     = 13,
-		{  8,  9}, // T2_EXCHANGE      = 14,
-		{ 14, 16}, // T2_VISIT         = 15,
-
-		{ 15, 13}, // T3_MOVE          = 16,
-		{ 13, 13}, // T3_ATTACK        = 17,
-		{ 16, 32}, // T3_SAIL          = 18,
-		{ 13, 20}, // T3_DISEMBARK     = 19,
-		{  8,  9}, // T3_EXCHANGE      = 20,
-		{ 14, 16}, // T3_VISIT         = 21,
-
-		{ 15, 13}, // T4_MOVE          = 22,
-		{ 13, 13}, // T4_ATTACK        = 23,
-		{ 16, 32}, // T4_SAIL          = 24,
-		{ 13, 20}, // T4_DISEMBARK     = 25,
-		{  8,  9}, // T4_EXCHANGE      = 26,
-		{ 14, 16}, // T4_VISIT         = 27,
-
-		{ 16, 32}, // T1_SAIL_VISIT    = 28,
-		{ 16, 32}, // T2_SAIL_VISIT    = 29,
-		{ 16, 32}, // T3_SAIL_VISIT    = 30,
-		{ 16, 32}, // T4_SAIL_VISIT    = 31,
-
-		{  6,  1}, // SCROLL_NORTH     = 32,
-		{ 16,  2}, // SCROLL_NORTHEAST = 33,
-		{ 21,  6}, // SCROLL_EAST      = 34,
-		{ 16, 16}, // SCROLL_SOUTHEAST = 35,
-		{  6, 21}, // SCROLL_SOUTH     = 36,
-		{  1, 16}, // SCROLL_SOUTHWEST = 37,
-		{  1,  5}, // SCROLL_WEST      = 38,
-		{  2,  1}, // SCROLL_NORTHWEST = 39,
-
-		{  0,  0}, // POINTER_COPY     = 40,
-		{ 14, 16}, // TELEPORT         = 41,
-		{ 20, 20}, // SCUTTLE_BOAT     = 42
-	}};
-
-	assert(offsets.size() == size_t(Cursor::Map::COUNT)); //Invalid number of pivot offsets for cursor
-	assert(index < offsets.size());
-	return offsets[index];
-}
-
-Point CursorHandler::getPivotOffsetCombat(size_t index)
-{
-	static const std::array<Point, 20> offsets = {{
-		{ 12, 12 }, // BLOCKED        = 0,
-		{ 10, 14 }, // MOVE           = 1,
-		{ 14, 14 }, // FLY            = 2,
-		{ 12, 12 }, // SHOOT          = 3,
-		{ 12, 12 }, // HERO           = 4,
-		{  8, 12 }, // QUERY          = 5,
-		{  0,  0 }, // POINTER        = 6,
-		{ 21,  0 }, // HIT_NORTHEAST  = 7,
-		{ 31,  5 }, // HIT_EAST       = 8,
-		{ 21, 21 }, // HIT_SOUTHEAST  = 9,
-		{  0, 21 }, // HIT_SOUTHWEST  = 10,
-		{  0,  5 }, // HIT_WEST       = 11,
-		{  0,  0 }, // HIT_NORTHWEST  = 12,
-		{  6,  0 }, // HIT_NORTH      = 13,
-		{  6, 31 }, // HIT_SOUTH      = 14,
-		{ 14,  0 }, // SHOOT_PENALTY  = 15,
-		{ 12, 12 }, // SHOOT_CATAPULT = 16,
-		{ 12, 12 }, // HEAL           = 17,
-		{ 12, 12 }, // SACRIFICE      = 18,
-		{ 14, 20 }, // TELEPORT       = 19
-	}};
-
-	assert(offsets.size() == size_t(Cursor::Combat::COUNT)); //Invalid number of pivot offsets for cursor
-	assert(index < offsets.size());
-	return offsets[index];
-}
-
-Point CursorHandler::getPivotOffsetSpellcast()
-{
-	return { 18, 28};
-}
-
-Point CursorHandler::getPivotOffset()
-{
-	if (dndObject)
-		return dndObject->dimensions() / 2;
-
-	switch (type) {
-	case Cursor::Type::ADVENTURE: return getPivotOffsetMap(frame);
-	case Cursor::Type::COMBAT:    return getPivotOffsetCombat(frame);
-	case Cursor::Type::DEFAULT:   return getPivotOffsetDefault(frame);
-	case Cursor::Type::SPELLBOOK: return getPivotOffsetSpellcast();
-	};
-
-	assert(0);
-	return {0, 0};
-}
-
-std::shared_ptr<IImage> CursorHandler::getCurrentImage()
-{
-	if (dndObject)
-		return dndObject;
-
-	return cursors[static_cast<size_t>(type)]->getImage(frame);
-}
-
-void CursorHandler::updateSpellcastCursor()
-{
-	static const float frameDisplayDuration = 0.1f; // H3 uses 100 ms per frame
-
-	frameTime += GH.framerate().getElapsedMilliseconds() / 1000.f;
-	size_t newFrame = frame;
-
-	while (frameTime >= frameDisplayDuration)
-	{
-		frameTime -= frameDisplayDuration;
-		newFrame++;
-	}
-
-	auto & animation = cursors.at(static_cast<size_t>(type));
-
-	while (newFrame >= animation->size())
-		newFrame -= animation->size();
-
-	changeGraphic(Cursor::Type::SPELLBOOK, newFrame);
-}
-
-void CursorHandler::render()
-{
-	if(!showing)
-		return;
-
-	if (type == Cursor::Type::SPELLBOOK)
-		updateSpellcastCursor();
-
-	cursor->render();
-}
-
-void CursorHandler::hide()
-{
-	if (!showing)
-		return;
-
-	showing = false;
-	cursor->setVisible(false);
-}
-
-void CursorHandler::show()
-{
-	if (showing)
-		return;
-
-	showing = true;
-	cursor->setVisible(true);
-}
-
+/*
+ * CCursorHandler.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 "CursorHandler.h"
+
+#include "CGuiHandler.h"
+#include "FramerateManager.h"
+#include "../renderSDL/CursorSoftware.h"
+#include "../renderSDL/CursorHardware.h"
+#include "../render/CAnimation.h"
+#include "../render/IImage.h"
+#include "../render/IRenderHandler.h"
+
+#include "../../lib/CConfigHandler.h"
+
+std::unique_ptr<ICursor> CursorHandler::createCursor()
+{
+#if defined(VCMI_MOBILE)
+	if (settings["general"]["userRelativePointer"].Bool())
+		return std::make_unique<CursorSoftware>();
+#endif
+
+	if (settings["video"]["cursor"].String() == "hardware")
+		return std::make_unique<CursorHardware>();
+
+	assert(settings["video"]["cursor"].String() == "software");
+	return std::make_unique<CursorSoftware>();
+}
+
+CursorHandler::CursorHandler()
+	: cursor(createCursor())
+	, frameTime(0.f)
+	, showing(false)
+	, pos(0,0)
+{
+
+	type = Cursor::Type::DEFAULT;
+	dndObject = nullptr;
+
+	cursors =
+	{
+		GH.renderHandler().loadAnimation(AnimationPath::builtin("CRADVNTR")),
+		GH.renderHandler().loadAnimation(AnimationPath::builtin("CRCOMBAT")),
+		GH.renderHandler().loadAnimation(AnimationPath::builtin("CRDEFLT")),
+		GH.renderHandler().loadAnimation(AnimationPath::builtin("CRSPELL"))
+	};
+
+	for (auto & cursor : cursors)
+		cursor->preload();
+
+	set(Cursor::Map::POINTER);
+}
+
+CursorHandler::~CursorHandler() = default;
+
+void CursorHandler::changeGraphic(Cursor::Type type, size_t index)
+{
+	assert(dndObject == nullptr);
+
+	if (type == this->type && index == this->frame)
+		return;
+
+	this->type = type;
+	this->frame = index;
+
+	cursor->setImage(getCurrentImage(), getPivotOffset());
+}
+
+void CursorHandler::set(Cursor::Default index)
+{
+	changeGraphic(Cursor::Type::DEFAULT, static_cast<size_t>(index));
+}
+
+void CursorHandler::set(Cursor::Map index)
+{
+	changeGraphic(Cursor::Type::ADVENTURE, static_cast<size_t>(index));
+}
+
+void CursorHandler::set(Cursor::Combat index)
+{
+	changeGraphic(Cursor::Type::COMBAT, static_cast<size_t>(index));
+}
+
+void CursorHandler::set(Cursor::Spellcast index)
+{
+	//Note: this is animated cursor, ignore specified frame and only change type
+	changeGraphic(Cursor::Type::SPELLBOOK, frame);
+}
+
+void CursorHandler::dragAndDropCursor(std::shared_ptr<IImage> image)
+{
+	dndObject = image;
+	cursor->setImage(getCurrentImage(), getPivotOffset());
+}
+
+void CursorHandler::dragAndDropCursor (const AnimationPath & path, size_t index)
+{
+	auto anim = GH.renderHandler().loadAnimation(path);
+	anim->load(index);
+	dragAndDropCursor(anim->getImage(index));
+}
+
+void CursorHandler::cursorMove(const int & x, const int & y)
+{
+	pos.x = x;
+	pos.y = y;
+
+	cursor->setCursorPosition(pos);
+}
+
+Point CursorHandler::getPivotOffsetDefault(size_t index)
+{
+	return {0, 0};
+}
+
+Point CursorHandler::getPivotOffsetMap(size_t index)
+{
+	static const std::array<Point, 43> offsets = {{
+		{  0,  0}, // POINTER          =  0,
+		{  0,  0}, // HOURGLASS        =  1,
+		{ 12, 10}, // HERO             =  2,
+		{ 12, 12}, // TOWN             =  3,
+
+		{ 15, 13}, // T1_MOVE          =  4,
+		{ 13, 13}, // T1_ATTACK        =  5,
+		{ 16, 32}, // T1_SAIL          =  6,
+		{ 13, 20}, // T1_DISEMBARK     =  7,
+		{  8,  9}, // T1_EXCHANGE      =  8,
+		{ 14, 16}, // T1_VISIT         =  9,
+
+		{ 15, 13}, // T2_MOVE          = 10,
+		{ 13, 13}, // T2_ATTACK        = 11,
+		{ 16, 32}, // T2_SAIL          = 12,
+		{ 13, 20}, // T2_DISEMBARK     = 13,
+		{  8,  9}, // T2_EXCHANGE      = 14,
+		{ 14, 16}, // T2_VISIT         = 15,
+
+		{ 15, 13}, // T3_MOVE          = 16,
+		{ 13, 13}, // T3_ATTACK        = 17,
+		{ 16, 32}, // T3_SAIL          = 18,
+		{ 13, 20}, // T3_DISEMBARK     = 19,
+		{  8,  9}, // T3_EXCHANGE      = 20,
+		{ 14, 16}, // T3_VISIT         = 21,
+
+		{ 15, 13}, // T4_MOVE          = 22,
+		{ 13, 13}, // T4_ATTACK        = 23,
+		{ 16, 32}, // T4_SAIL          = 24,
+		{ 13, 20}, // T4_DISEMBARK     = 25,
+		{  8,  9}, // T4_EXCHANGE      = 26,
+		{ 14, 16}, // T4_VISIT         = 27,
+
+		{ 16, 32}, // T1_SAIL_VISIT    = 28,
+		{ 16, 32}, // T2_SAIL_VISIT    = 29,
+		{ 16, 32}, // T3_SAIL_VISIT    = 30,
+		{ 16, 32}, // T4_SAIL_VISIT    = 31,
+
+		{  6,  1}, // SCROLL_NORTH     = 32,
+		{ 16,  2}, // SCROLL_NORTHEAST = 33,
+		{ 21,  6}, // SCROLL_EAST      = 34,
+		{ 16, 16}, // SCROLL_SOUTHEAST = 35,
+		{  6, 21}, // SCROLL_SOUTH     = 36,
+		{  1, 16}, // SCROLL_SOUTHWEST = 37,
+		{  1,  5}, // SCROLL_WEST      = 38,
+		{  2,  1}, // SCROLL_NORTHWEST = 39,
+
+		{  0,  0}, // POINTER_COPY     = 40,
+		{ 14, 16}, // TELEPORT         = 41,
+		{ 20, 20}, // SCUTTLE_BOAT     = 42
+	}};
+
+	assert(offsets.size() == size_t(Cursor::Map::COUNT)); //Invalid number of pivot offsets for cursor
+	assert(index < offsets.size());
+	return offsets[index];
+}
+
+Point CursorHandler::getPivotOffsetCombat(size_t index)
+{
+	static const std::array<Point, 20> offsets = {{
+		{ 12, 12 }, // BLOCKED        = 0,
+		{ 10, 14 }, // MOVE           = 1,
+		{ 14, 14 }, // FLY            = 2,
+		{ 12, 12 }, // SHOOT          = 3,
+		{ 12, 12 }, // HERO           = 4,
+		{  8, 12 }, // QUERY          = 5,
+		{  0,  0 }, // POINTER        = 6,
+		{ 21,  0 }, // HIT_NORTHEAST  = 7,
+		{ 31,  5 }, // HIT_EAST       = 8,
+		{ 21, 21 }, // HIT_SOUTHEAST  = 9,
+		{  0, 21 }, // HIT_SOUTHWEST  = 10,
+		{  0,  5 }, // HIT_WEST       = 11,
+		{  0,  0 }, // HIT_NORTHWEST  = 12,
+		{  6,  0 }, // HIT_NORTH      = 13,
+		{  6, 31 }, // HIT_SOUTH      = 14,
+		{ 14,  0 }, // SHOOT_PENALTY  = 15,
+		{ 12, 12 }, // SHOOT_CATAPULT = 16,
+		{ 12, 12 }, // HEAL           = 17,
+		{ 12, 12 }, // SACRIFICE      = 18,
+		{ 14, 20 }, // TELEPORT       = 19
+	}};
+
+	assert(offsets.size() == size_t(Cursor::Combat::COUNT)); //Invalid number of pivot offsets for cursor
+	assert(index < offsets.size());
+	return offsets[index];
+}
+
+Point CursorHandler::getPivotOffsetSpellcast()
+{
+	return { 18, 28};
+}
+
+Point CursorHandler::getPivotOffset()
+{
+	if (dndObject)
+		return dndObject->dimensions() / 2;
+
+	switch (type) {
+	case Cursor::Type::ADVENTURE: return getPivotOffsetMap(frame);
+	case Cursor::Type::COMBAT:    return getPivotOffsetCombat(frame);
+	case Cursor::Type::DEFAULT:   return getPivotOffsetDefault(frame);
+	case Cursor::Type::SPELLBOOK: return getPivotOffsetSpellcast();
+	};
+
+	assert(0);
+	return {0, 0};
+}
+
+std::shared_ptr<IImage> CursorHandler::getCurrentImage()
+{
+	if (dndObject)
+		return dndObject;
+
+	return cursors[static_cast<size_t>(type)]->getImage(frame);
+}
+
+void CursorHandler::updateSpellcastCursor()
+{
+	static const float frameDisplayDuration = 0.1f; // H3 uses 100 ms per frame
+
+	frameTime += GH.framerate().getElapsedMilliseconds() / 1000.f;
+	size_t newFrame = frame;
+
+	while (frameTime >= frameDisplayDuration)
+	{
+		frameTime -= frameDisplayDuration;
+		newFrame++;
+	}
+
+	auto & animation = cursors.at(static_cast<size_t>(type));
+
+	while (newFrame >= animation->size())
+		newFrame -= animation->size();
+
+	changeGraphic(Cursor::Type::SPELLBOOK, newFrame);
+}
+
+void CursorHandler::render()
+{
+	if(!showing)
+		return;
+
+	if (type == Cursor::Type::SPELLBOOK)
+		updateSpellcastCursor();
+
+	cursor->render();
+}
+
+void CursorHandler::hide()
+{
+	if (!showing)
+		return;
+
+	showing = false;
+	cursor->setVisible(false);
+}
+
+void CursorHandler::show()
+{
+	if (showing)
+		return;
+
+	showing = true;
+	cursor->setVisible(true);
+}
+

+ 182 - 182
client/gui/CursorHandler.h

@@ -1,182 +1,182 @@
-/*
- * CCursorHandler.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/Point.h"
-#include "../../lib/filesystem/ResourcePath.h"
-
-class ICursor;
-class IImage;
-class CAnimation;
-
-namespace Cursor
-{
-	enum class Type {
-		ADVENTURE, // set of various cursors for adventure map
-		COMBAT,    // set of various cursors for combat
-		DEFAULT,   // default arrow and hourglass cursors
-		SPELLBOOK  // animated cursor for spellcasting
-	};
-
-	enum class Default {
-		POINTER      = 0,
-		//ARROW_COPY = 1, // probably unused
-		HOURGLASS  = 2,
-	};
-
-	enum class Combat {
-		BLOCKED        = 0,
-		MOVE           = 1,
-		FLY            = 2,
-		SHOOT          = 3,
-		HERO           = 4,
-		QUERY          = 5,
-		POINTER        = 6,
-		HIT_NORTHEAST  = 7,
-		HIT_EAST       = 8,
-		HIT_SOUTHEAST  = 9,
-		HIT_SOUTHWEST  = 10,
-		HIT_WEST       = 11,
-		HIT_NORTHWEST  = 12,
-		HIT_NORTH      = 13,
-		HIT_SOUTH      = 14,
-		SHOOT_PENALTY  = 15,
-		SHOOT_CATAPULT = 16,
-		HEAL           = 17,
-		SACRIFICE      = 18,
-		TELEPORT       = 19,
-
-		COUNT
-	};
-
-	enum class Map {
-		POINTER          =  0,
-		HOURGLASS        =  1,
-		HERO             =  2,
-		TOWN             =  3,
-		T1_MOVE          =  4,
-		T1_ATTACK        =  5,
-		T1_SAIL          =  6,
-		T1_DISEMBARK     =  7,
-		T1_EXCHANGE      =  8,
-		T1_VISIT         =  9,
-		T2_MOVE          = 10,
-		T2_ATTACK        = 11,
-		T2_SAIL          = 12,
-		T2_DISEMBARK     = 13,
-		T2_EXCHANGE      = 14,
-		T2_VISIT         = 15,
-		T3_MOVE          = 16,
-		T3_ATTACK        = 17,
-		T3_SAIL          = 18,
-		T3_DISEMBARK     = 19,
-		T3_EXCHANGE      = 20,
-		T3_VISIT         = 21,
-		T4_MOVE          = 22,
-		T4_ATTACK        = 23,
-		T4_SAIL          = 24,
-		T4_DISEMBARK     = 25,
-		T4_EXCHANGE      = 26,
-		T4_VISIT         = 27,
-		T1_SAIL_VISIT    = 28,
-		T2_SAIL_VISIT    = 29,
-		T3_SAIL_VISIT    = 30,
-		T4_SAIL_VISIT    = 31,
-		SCROLL_NORTH     = 32,
-		SCROLL_NORTHEAST = 33,
-		SCROLL_EAST      = 34,
-		SCROLL_SOUTHEAST = 35,
-		SCROLL_SOUTH     = 36,
-		SCROLL_SOUTHWEST = 37,
-		SCROLL_WEST      = 38,
-		SCROLL_NORTHWEST = 39,
-		//POINTER_COPY       = 40, // probably unused
-		TELEPORT         = 41,
-		SCUTTLE_BOAT     = 42,
-
-		COUNT
-	};
-
-	enum class Spellcast {
-		SPELL = 0,
-	};
-}
-
-/// handles mouse cursor
-class CursorHandler final
-{
-	std::shared_ptr<IImage> dndObject; //if set, overrides currentCursor
-
-	std::array<std::shared_ptr<CAnimation>, 4> cursors;
-
-	bool showing;
-
-	/// Current cursor
-	Cursor::Type type;
-	size_t frame;
-	float frameTime;
-	Point pos;
-
-	void changeGraphic(Cursor::Type type, size_t index);
-
-	Point getPivotOffset();
-
-	void updateSpellcastCursor();
-
-	std::shared_ptr<IImage> getCurrentImage();
-
-	std::unique_ptr<ICursor> cursor;
-
-	static std::unique_ptr<ICursor> createCursor();
-public:
-	CursorHandler();
-	~CursorHandler();
-
-	/// Replaces the cursor with a custom image.
-	/// @param image Image to replace cursor with or nullptr to use the normal cursor.
-	void dragAndDropCursor(std::shared_ptr<IImage> image);
-
-	void dragAndDropCursor(const AnimationPath & path, size_t index);
-
-	/// Changes cursor to specified index
-	void set(Cursor::Default index);
-	void set(Cursor::Map index);
-	void set(Cursor::Combat index);
-	void set(Cursor::Spellcast index);
-
-	/// Returns current index of cursor
-	template<typename Index>
-	std::optional<Index> get()
-	{
-		bool typeValid = true;
-
-		typeValid &= (std::is_same<Index, Cursor::Default>::value   )|| type != Cursor::Type::DEFAULT;
-		typeValid &= (std::is_same<Index, Cursor::Map>::value       )|| type != Cursor::Type::ADVENTURE;
-		typeValid &= (std::is_same<Index, Cursor::Combat>::value    )|| type != Cursor::Type::COMBAT;
-		typeValid &= (std::is_same<Index, Cursor::Spellcast>::value )|| type != Cursor::Type::SPELLBOOK;
-
-		if (typeValid)
-			return static_cast<Index>(frame);
-		return std::nullopt;
-	}
-
-	Point getPivotOffsetSpellcast();
-	Point getPivotOffsetDefault(size_t index);
-	Point getPivotOffsetMap(size_t index);
-	Point getPivotOffsetCombat(size_t index);
-
-	void render();
-
-	void hide();
-	void show();
-
-	/// change cursor's positions to (x, y)
-	void cursorMove(const int & x, const int & y);
-};
+/*
+ * CCursorHandler.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/Point.h"
+#include "../../lib/filesystem/ResourcePath.h"
+
+class ICursor;
+class IImage;
+class CAnimation;
+
+namespace Cursor
+{
+	enum class Type {
+		ADVENTURE, // set of various cursors for adventure map
+		COMBAT,    // set of various cursors for combat
+		DEFAULT,   // default arrow and hourglass cursors
+		SPELLBOOK  // animated cursor for spellcasting
+	};
+
+	enum class Default {
+		POINTER      = 0,
+		//ARROW_COPY = 1, // probably unused
+		HOURGLASS  = 2,
+	};
+
+	enum class Combat {
+		BLOCKED        = 0,
+		MOVE           = 1,
+		FLY            = 2,
+		SHOOT          = 3,
+		HERO           = 4,
+		QUERY          = 5,
+		POINTER        = 6,
+		HIT_NORTHEAST  = 7,
+		HIT_EAST       = 8,
+		HIT_SOUTHEAST  = 9,
+		HIT_SOUTHWEST  = 10,
+		HIT_WEST       = 11,
+		HIT_NORTHWEST  = 12,
+		HIT_NORTH      = 13,
+		HIT_SOUTH      = 14,
+		SHOOT_PENALTY  = 15,
+		SHOOT_CATAPULT = 16,
+		HEAL           = 17,
+		SACRIFICE      = 18,
+		TELEPORT       = 19,
+
+		COUNT
+	};
+
+	enum class Map {
+		POINTER          =  0,
+		HOURGLASS        =  1,
+		HERO             =  2,
+		TOWN             =  3,
+		T1_MOVE          =  4,
+		T1_ATTACK        =  5,
+		T1_SAIL          =  6,
+		T1_DISEMBARK     =  7,
+		T1_EXCHANGE      =  8,
+		T1_VISIT         =  9,
+		T2_MOVE          = 10,
+		T2_ATTACK        = 11,
+		T2_SAIL          = 12,
+		T2_DISEMBARK     = 13,
+		T2_EXCHANGE      = 14,
+		T2_VISIT         = 15,
+		T3_MOVE          = 16,
+		T3_ATTACK        = 17,
+		T3_SAIL          = 18,
+		T3_DISEMBARK     = 19,
+		T3_EXCHANGE      = 20,
+		T3_VISIT         = 21,
+		T4_MOVE          = 22,
+		T4_ATTACK        = 23,
+		T4_SAIL          = 24,
+		T4_DISEMBARK     = 25,
+		T4_EXCHANGE      = 26,
+		T4_VISIT         = 27,
+		T1_SAIL_VISIT    = 28,
+		T2_SAIL_VISIT    = 29,
+		T3_SAIL_VISIT    = 30,
+		T4_SAIL_VISIT    = 31,
+		SCROLL_NORTH     = 32,
+		SCROLL_NORTHEAST = 33,
+		SCROLL_EAST      = 34,
+		SCROLL_SOUTHEAST = 35,
+		SCROLL_SOUTH     = 36,
+		SCROLL_SOUTHWEST = 37,
+		SCROLL_WEST      = 38,
+		SCROLL_NORTHWEST = 39,
+		//POINTER_COPY       = 40, // probably unused
+		TELEPORT         = 41,
+		SCUTTLE_BOAT     = 42,
+
+		COUNT
+	};
+
+	enum class Spellcast {
+		SPELL = 0,
+	};
+}
+
+/// handles mouse cursor
+class CursorHandler final
+{
+	std::shared_ptr<IImage> dndObject; //if set, overrides currentCursor
+
+	std::array<std::shared_ptr<CAnimation>, 4> cursors;
+
+	bool showing;
+
+	/// Current cursor
+	Cursor::Type type;
+	size_t frame;
+	float frameTime;
+	Point pos;
+
+	void changeGraphic(Cursor::Type type, size_t index);
+
+	Point getPivotOffset();
+
+	void updateSpellcastCursor();
+
+	std::shared_ptr<IImage> getCurrentImage();
+
+	std::unique_ptr<ICursor> cursor;
+
+	static std::unique_ptr<ICursor> createCursor();
+public:
+	CursorHandler();
+	~CursorHandler();
+
+	/// Replaces the cursor with a custom image.
+	/// @param image Image to replace cursor with or nullptr to use the normal cursor.
+	void dragAndDropCursor(std::shared_ptr<IImage> image);
+
+	void dragAndDropCursor(const AnimationPath & path, size_t index);
+
+	/// Changes cursor to specified index
+	void set(Cursor::Default index);
+	void set(Cursor::Map index);
+	void set(Cursor::Combat index);
+	void set(Cursor::Spellcast index);
+
+	/// Returns current index of cursor
+	template<typename Index>
+	std::optional<Index> get()
+	{
+		bool typeValid = true;
+
+		typeValid &= (std::is_same<Index, Cursor::Default>::value   )|| type != Cursor::Type::DEFAULT;
+		typeValid &= (std::is_same<Index, Cursor::Map>::value       )|| type != Cursor::Type::ADVENTURE;
+		typeValid &= (std::is_same<Index, Cursor::Combat>::value    )|| type != Cursor::Type::COMBAT;
+		typeValid &= (std::is_same<Index, Cursor::Spellcast>::value )|| type != Cursor::Type::SPELLBOOK;
+
+		if (typeValid)
+			return static_cast<Index>(frame);
+		return std::nullopt;
+	}
+
+	Point getPivotOffsetSpellcast();
+	Point getPivotOffsetDefault(size_t index);
+	Point getPivotOffsetMap(size_t index);
+	Point getPivotOffsetCombat(size_t index);
+
+	void render();
+
+	void hide();
+	void show();
+
+	/// change cursor's positions to (x, y)
+	void cursorMove(const int & x, const int & y);
+};

+ 19 - 19
client/gui/MouseButton.h

@@ -1,19 +1,19 @@
-/*
- * MouseButton.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 MouseButton
-{
-	LEFT   = 1,
-	MIDDLE = 2,
-	RIGHT  = 3,
-	EXTRA1 = 4,
-	EXTRA2 = 5
-};
+/*
+ * MouseButton.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 MouseButton
+{
+	LEFT   = 1,
+	MIDDLE = 2,
+	RIGHT  = 3,
+	EXTRA1 = 4,
+	EXTRA2 = 5
+};

+ 12 - 12
client/gui/TextAlignment.h

@@ -1,12 +1,12 @@
-/*
- * TextAlignment.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 ETextAlignment {TOPLEFT, TOPCENTER, CENTER, BOTTOMRIGHT};
+/*
+ * TextAlignment.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 ETextAlignment {TOPLEFT, TOPCENTER, CENTER, BOTTOMRIGHT};

+ 93 - 93
client/mapView/IMapRendererContext.h

@@ -1,93 +1,93 @@
-/*
- * IMapRendererContext.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 int3;
-class Point;
-class CGObjectInstance;
-class ObjectInstanceID;
-struct TerrainTile;
-struct CGPath;
-
-VCMI_LIB_NAMESPACE_END
-
-class IMapRendererContext
-{
-public:
-	using MapObject = ObjectInstanceID;
-	using MapObjectsList = std::vector<MapObject>;
-
-	virtual ~IMapRendererContext() = default;
-
-	/// returns dimensions of current map
-	virtual int3 getMapSize() const = 0;
-
-	/// returns true if chosen coordinates exist on map
-	virtual bool isInMap(const int3 & coordinates) const = 0;
-
-	/// returns true if selected tile has animation and should not be cached
-	virtual bool tileAnimated(const int3 & coordinates) const = 0;
-
-	/// returns tile by selected coordinates. Coordinates MUST be valid
-	virtual const TerrainTile & getMapTile(const int3 & coordinates) const = 0;
-
-	/// returns all objects visible on specified tile
-	virtual const MapObjectsList & getObjects(const int3 & coordinates) const = 0;
-
-	/// returns specific object by ID, or nullptr if not found
-	virtual const CGObjectInstance * getObject(ObjectInstanceID objectID) const = 0;
-
-	/// returns path of currently active hero, or nullptr if none
-	virtual const CGPath * currentPath() const = 0;
-
-	/// returns true if specified tile is visible in current context
-	virtual bool isVisible(const int3 & coordinates) const = 0;
-
-	/// returns true if specified object is the currently active hero
-	virtual bool isActiveHero(const CGObjectInstance* obj) const = 0;
-
-	virtual size_t objectGroupIndex(ObjectInstanceID objectID) const = 0;
-	virtual Point objectImageOffset(ObjectInstanceID objectID, const int3 & coordinates) const = 0;
-
-	/// returns object animation transparency. IF set to 0, object will not be visible
-	virtual double objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const = 0;
-
-	/// returns animation frame for selected object
-	virtual size_t objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const = 0;
-
-	/// returns index of image for overlay on specific tile, or numeric_limits::max if none
-	virtual size_t overlayImageIndex(const int3 & coordinates) const = 0;
-
-	/// returns animation frame for terrain
-	virtual size_t terrainImageIndex(size_t groupSize) const = 0;
-
-	virtual double viewTransitionProgress() const = 0;
-
-	/// if true, rendered images will be converted to grayscale
-	virtual bool filterGrayscale() const = 0;
-
-	virtual bool showRoads() const = 0;
-	virtual bool showRivers() const = 0;
-	virtual bool showBorder() const = 0;
-
-	/// if true, world view overlay will be shown
-	virtual bool showOverlay() const = 0;
-
-	/// if true, map grid should be visible on map
-	virtual bool showGrid() const = 0;
-	virtual bool showVisitable() const = 0;
-	virtual bool showBlocked() const = 0;
-
-	/// if true, spell range for teleport / scuttle boat will be visible
-	virtual bool showSpellRange(const int3 & position) const = 0;
-
-};
+/*
+ * IMapRendererContext.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 int3;
+class Point;
+class CGObjectInstance;
+class ObjectInstanceID;
+struct TerrainTile;
+struct CGPath;
+
+VCMI_LIB_NAMESPACE_END
+
+class IMapRendererContext
+{
+public:
+	using MapObject = ObjectInstanceID;
+	using MapObjectsList = std::vector<MapObject>;
+
+	virtual ~IMapRendererContext() = default;
+
+	/// returns dimensions of current map
+	virtual int3 getMapSize() const = 0;
+
+	/// returns true if chosen coordinates exist on map
+	virtual bool isInMap(const int3 & coordinates) const = 0;
+
+	/// returns true if selected tile has animation and should not be cached
+	virtual bool tileAnimated(const int3 & coordinates) const = 0;
+
+	/// returns tile by selected coordinates. Coordinates MUST be valid
+	virtual const TerrainTile & getMapTile(const int3 & coordinates) const = 0;
+
+	/// returns all objects visible on specified tile
+	virtual const MapObjectsList & getObjects(const int3 & coordinates) const = 0;
+
+	/// returns specific object by ID, or nullptr if not found
+	virtual const CGObjectInstance * getObject(ObjectInstanceID objectID) const = 0;
+
+	/// returns path of currently active hero, or nullptr if none
+	virtual const CGPath * currentPath() const = 0;
+
+	/// returns true if specified tile is visible in current context
+	virtual bool isVisible(const int3 & coordinates) const = 0;
+
+	/// returns true if specified object is the currently active hero
+	virtual bool isActiveHero(const CGObjectInstance* obj) const = 0;
+
+	virtual size_t objectGroupIndex(ObjectInstanceID objectID) const = 0;
+	virtual Point objectImageOffset(ObjectInstanceID objectID, const int3 & coordinates) const = 0;
+
+	/// returns object animation transparency. IF set to 0, object will not be visible
+	virtual double objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const = 0;
+
+	/// returns animation frame for selected object
+	virtual size_t objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const = 0;
+
+	/// returns index of image for overlay on specific tile, or numeric_limits::max if none
+	virtual size_t overlayImageIndex(const int3 & coordinates) const = 0;
+
+	/// returns animation frame for terrain
+	virtual size_t terrainImageIndex(size_t groupSize) const = 0;
+
+	virtual double viewTransitionProgress() const = 0;
+
+	/// if true, rendered images will be converted to grayscale
+	virtual bool filterGrayscale() const = 0;
+
+	virtual bool showRoads() const = 0;
+	virtual bool showRivers() const = 0;
+	virtual bool showBorder() const = 0;
+
+	/// if true, world view overlay will be shown
+	virtual bool showOverlay() const = 0;
+
+	/// if true, map grid should be visible on map
+	virtual bool showGrid() const = 0;
+	virtual bool showVisitable() const = 0;
+	virtual bool showBlocked() const = 0;
+
+	/// if true, spell range for teleport / scuttle boat will be visible
+	virtual bool showSpellRange(const int3 & position) const = 0;
+
+};

Some files were not shown because too many files changed in this diff