Bläddra i källkod

Merge branch 'beta' into 'develop'

Ivan Savenko 8 månader sedan
förälder
incheckning
d3131ea365
100 ändrade filer med 2244 tillägg och 669 borttagningar
  1. 18 16
      AI/BattleAI/AttackPossibility.cpp
  2. 2 2
      AI/BattleAI/AttackPossibility.h
  3. 2 2
      AI/BattleAI/BattleAI.cpp
  4. 1 1
      AI/BattleAI/BattleAI.h
  5. 36 39
      AI/BattleAI/BattleEvaluator.cpp
  6. 2 2
      AI/BattleAI/BattleEvaluator.h
  7. 97 73
      AI/BattleAI/BattleExchangeVariant.cpp
  8. 28 12
      AI/BattleAI/BattleExchangeVariant.h
  9. 3 3
      AI/BattleAI/PotentialTargets.cpp
  10. 1 1
      AI/BattleAI/PotentialTargets.h
  11. 3 3
      AI/BattleAI/StackWithBonuses.cpp
  12. 3 3
      AI/BattleAI/StackWithBonuses.h
  13. 1 0
      AI/CMakeLists.txt
  14. 15 5
      AI/Nullkiller/AIGateway.cpp
  15. 2 0
      AI/Nullkiller/AIGateway.h
  16. 328 6
      AI/Nullkiller/AIUtility.cpp
  17. 2 1
      AI/Nullkiller/AIUtility.h
  18. 18 6
      AI/Nullkiller/Analyzers/BuildAnalyzer.cpp
  19. 10 2
      AI/Nullkiller/Behaviors/DefenceBehavior.cpp
  20. 1 1
      AI/Nullkiller/Behaviors/StartupBehavior.cpp
  21. 1 1
      AI/Nullkiller/CMakeLists.txt
  22. 25 0
      AI/Nullkiller/Engine/Nullkiller.cpp
  23. 11 0
      AI/Nullkiller/Engine/Nullkiller.h
  24. 6 33
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  25. 2 0
      AI/Nullkiller/Engine/Settings.cpp
  26. 2 0
      AI/Nullkiller/Engine/Settings.h
  27. 3 3
      AI/Nullkiller/Goals/ExecuteHeroChain.cpp
  28. 1 1
      AI/Nullkiller/Goals/ExploreNeighbourTile.cpp
  29. 1 1
      AI/Nullkiller/Helpers/ExplorationHelper.cpp
  30. 2 0
      AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp
  31. 19 18
      AI/StupidAI/StupidAI.cpp
  32. 2 2
      AI/StupidAI/StupidAI.h
  33. 2 2
      AI/VCAI/AIUtility.cpp
  34. 1 1
      AI/VCAI/FuzzyEngines.cpp
  35. 1 0
      AI/VCAI/Goals/CollectRes.cpp
  36. 15 2
      AI/VCAI/VCAI.cpp
  37. 4 0
      AI/VCAI/VCAI.h
  38. 0 5
      CCallback.cpp
  39. 0 1
      CCallback.h
  40. 15 2
      CMakeLists.txt
  41. 65 0
      ChangeLog.md
  42. 4 4
      Global.h
  43. BIN
      Mods/vcmi/Content/Sprites/lobby/addChannel.png
  44. BIN
      Mods/vcmi/Content/Sprites/lobby/closeChannel.png
  45. BIN
      Mods/vcmi/Content/Sprites2x/lobby/addChannel.png
  46. BIN
      Mods/vcmi/Content/Sprites2x/lobby/closeChannel.png
  47. BIN
      Mods/vcmi/Content/Sprites2x/lobby/iconPlayer.png
  48. BIN
      Mods/vcmi/Content/Sprites3x/lobby/addChannel.png
  49. BIN
      Mods/vcmi/Content/Sprites3x/lobby/closeChannel.png
  50. BIN
      Mods/vcmi/Content/Sprites3x/lobby/iconPlayer.png
  51. BIN
      Mods/vcmi/Content/Sprites4x/lobby/addChannel.png
  52. BIN
      Mods/vcmi/Content/Sprites4x/lobby/closeChannel.png
  53. BIN
      Mods/vcmi/Content/Sprites4x/lobby/iconPlayer.png
  54. 1 0
      Mods/vcmi/Content/config/czech.json
  55. 24 1
      Mods/vcmi/Content/config/english.json
  56. 2 0
      Mods/vcmi/Content/config/german.json
  57. 810 0
      Mods/vcmi/Content/config/hungarian.json
  58. 1 0
      Mods/vcmi/Content/config/ukrainian.json
  59. 120 120
      Mods/vcmi/Content/config/vietnamese.json
  60. 17 8
      Mods/vcmi/mod.json
  61. 48 0
      android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/FileUtil.java
  62. 2 1
      android/vcmi-app/src/main/res/values-cs/strings.xml
  63. 10 5
      client/CMakeLists.txt
  64. 23 4
      client/CPlayerInterface.cpp
  65. 5 1
      client/CPlayerInterface.h
  66. 0 41
      client/Client.cpp
  67. 0 8
      client/Client.h
  68. 12 37
      client/NetPacksClient.cpp
  69. 1 1
      client/PlayerLocalState.cpp
  70. 1 0
      client/PlayerLocalState.h
  71. 2 2
      client/adventureMap/AdventureMapInterface.cpp
  72. 1 2
      client/adventureMap/CMinimap.cpp
  73. 24 24
      client/battle/BattleActionsController.cpp
  74. 16 16
      client/battle/BattleActionsController.h
  75. 6 6
      client/battle/BattleAnimationClasses.cpp
  76. 9 9
      client/battle/BattleAnimationClasses.h
  77. 59 69
      client/battle/BattleFieldController.cpp
  78. 18 18
      client/battle/BattleFieldController.h
  79. 6 6
      client/battle/BattleInterface.cpp
  80. 7 7
      client/battle/BattleInterface.h
  81. 1 1
      client/battle/BattleObstacleController.h
  82. 1 1
      client/battle/BattleOverlayLogVisualizer.cpp
  83. 1 1
      client/battle/BattleOverlayLogVisualizer.h
  84. 1 1
      client/battle/BattleRenderer.cpp
  85. 1 1
      client/battle/BattleRenderer.h
  86. 5 5
      client/battle/BattleSiegeController.cpp
  87. 2 2
      client/battle/BattleSiegeController.h
  88. 3 3
      client/battle/BattleStacksController.cpp
  89. 5 4
      client/battle/BattleStacksController.h
  90. 86 0
      client/globalLobby/GlobalLobbyAddChannelWindow.cpp
  91. 46 0
      client/globalLobby/GlobalLobbyAddChannelWindow.h
  92. 53 4
      client/globalLobby/GlobalLobbyClient.cpp
  93. 2 0
      client/globalLobby/GlobalLobbyClient.h
  94. 21 1
      client/globalLobby/GlobalLobbyWidget.cpp
  95. 1 1
      client/globalLobby/GlobalLobbyWidget.h
  96. 16 0
      client/globalLobby/GlobalLobbyWindow.cpp
  97. 1 0
      client/globalLobby/GlobalLobbyWindow.h
  98. 4 1
      client/gui/CIntObject.cpp
  99. 13 3
      client/gui/EventDispatcher.cpp
  100. 1 0
      client/gui/EventsReceiver.h

+ 18 - 16
AI/BattleAI/AttackPossibility.cpp

@@ -72,7 +72,7 @@ void DamageCache::buildObstacleDamageCache(std::shared_ptr<HypotheticBattle> hb,
 
 			auto damageDealt = stack->getAvailableHealth() - updated->getAvailableHealth();
 
-			for(auto hex : affectedHexes)
+			for(const auto & hex : affectedHexes)
 			{
 				obstacleDamage[hex][stack->unitId()] = damageDealt;
 			}
@@ -92,8 +92,8 @@ void DamageCache::buildDamageCache(std::shared_ptr<HypotheticBattle> hb, BattleS
 			return u->isValidTarget();
 		});
 
-	std::vector<const battle::Unit *> ourUnits;
-	std::vector<const battle::Unit *> enemyUnits;
+	battle::Units ourUnits;
+	battle::Units enemyUnits;
 
 	for(auto stack : stacks)
 	{
@@ -129,7 +129,7 @@ int64_t DamageCache::getDamage(const battle::Unit * attacker, const battle::Unit
 	return damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount();
 }
 
-int64_t DamageCache::getObstacleDamage(BattleHex hex, const battle::Unit * defender)
+int64_t DamageCache::getObstacleDamage(const BattleHex & hex, const battle::Unit * defender)
 {
 	if(parent)
 		return parent->getObstacleDamage(hex, defender);
@@ -166,7 +166,7 @@ int64_t DamageCache::getOriginalDamage(const battle::Unit * attacker, const batt
 	return getDamage(attacker, defender, hb);
 }
 
-AttackPossibility::AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack)
+AttackPossibility::AttackPossibility(const BattleHex & from, const BattleHex & dest, const BattleAttackInfo & attack)
 	: from(from), dest(dest), attack(attack)
 {
 	this->attack.attackerPos = from;
@@ -280,8 +280,8 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(
 	std::set<uint32_t> checkedUnits;
 
 	auto attacker = attackInfo.attacker;
-	auto hexes = attacker->getSurroundingHexes(hex);
-	for(BattleHex tile : hexes)
+	const auto & hexes = attacker->getSurroundingHexes(hex);
+	for(const BattleHex & tile : hexes)
 	{
 		auto st = state->battleGetUnitByPos(tile, true);
 		if(!st || !state->battleMatchOwner(st, attacker))
@@ -326,13 +326,13 @@ AttackPossibility AttackPossibility::evaluate(
 
 	AttackPossibility bestAp(hex, BattleHex::INVALID, attackInfo);
 
-	std::vector<BattleHex> defenderHex;
+	BattleHexArray defenderHex;
 	if(attackInfo.shooting)
-		defenderHex.push_back(defender->getPosition());
+		defenderHex.insert(defender->getPosition());
 	else
 		defenderHex = CStack::meleeAttackHexes(attacker, defender, hex);
 
-	for(BattleHex defHex : defenderHex)
+	for(const BattleHex & defHex : defenderHex)
 	{
 		if(defHex == hex) // should be impossible but check anyway
 			continue;
@@ -346,9 +346,9 @@ AttackPossibility AttackPossibility::evaluate(
 		if (!attackInfo.shooting)
 			ap.attackerState->setPosition(hex);
 
-		std::vector<const battle::Unit *> defenderUnits;
-		std::vector<const battle::Unit *> retaliatedUnits = {attacker};
-		std::vector<const battle::Unit *> affectedUnits;
+		battle::Units defenderUnits;
+		battle::Units retaliatedUnits = {attacker};
+		battle::Units affectedUnits;
 
 		if (attackInfo.shooting)
 			defenderUnits = state->getAttackedBattleUnits(attacker, defender, defHex, true, hex, defender->getPosition());
@@ -384,7 +384,9 @@ AttackPossibility AttackPossibility::evaluate(
 		affectedUnits = defenderUnits;
 		vstd::concatenate(affectedUnits, retaliatedUnits);
 
-		logAi->trace("Attacked battle units count %d, %d->%d", affectedUnits.size(), hex.hex, defHex.hex);
+#if BATTLE_TRACE_LEVEL>=1
+		logAi->trace("Attacked battle units count %d, %d->%d", affectedUnits.size(), hex, defHex);
+#endif
 
 		std::map<uint32_t, std::shared_ptr<battle::CUnitState>> defenderStates;
 
@@ -487,7 +489,7 @@ AttackPossibility AttackPossibility::evaluate(
 		logAi->trace("BattleAI AP: %s -> %s at %d from %d, affects %d units: d:%lld a:%lld c:%lld s:%lld",
 			attackInfo.attacker->unitType()->getJsonKey(),
 			attackInfo.defender->unitType()->getJsonKey(),
-			(int)ap.dest, (int)ap.from, (int)ap.affectedUnits.size(),
+			ap.dest.toInt(), ap.from.toInt(), (int)ap.affectedUnits.size(),
 			ap.defenderDamageReduce, ap.attackerDamageReduce, ap.collateralDamageReduce, ap.shootersBlockedDmg);
 #endif
 
@@ -502,7 +504,7 @@ AttackPossibility AttackPossibility::evaluate(
 	logAi->trace("BattleAI best AP: %s -> %s at %d from %d, affects %d units: d:%lld a:%lld c:%lld s:%lld",
 		attackInfo.attacker->unitType()->getJsonKey(),
 		attackInfo.defender->unitType()->getJsonKey(),
-		(int)bestAp.dest, (int)bestAp.from, (int)bestAp.affectedUnits.size(),
+		bestAp.dest.toInt(), bestAp.from.toInt(), (int)bestAp.affectedUnits.size(),
 		bestAp.defenderDamageReduce, bestAp.attackerDamageReduce, bestAp.collateralDamageReduce, bestAp.shootersBlockedDmg);
 #endif
 

+ 2 - 2
AI/BattleAI/AttackPossibility.h

@@ -29,7 +29,7 @@ public:
 
 	void cacheDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb);
 	int64_t getDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb);
-	int64_t getObstacleDamage(BattleHex hex, const battle::Unit * defender);
+	int64_t getObstacleDamage(const BattleHex & hex, const battle::Unit * defender);
 	int64_t getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb);
 	void buildDamageCache(std::shared_ptr<HypotheticBattle> hb, BattleSide side);
 };
@@ -55,7 +55,7 @@ public:
 	int64_t shootersBlockedDmg = 0;
 	bool defenderDead = false;
 
-	AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack_);
+	AttackPossibility(const BattleHex & from, const BattleHex & dest, const BattleAttackInfo & attack_);
 
 	float damageDiff() const;
 	float attackValue() const;

+ 2 - 2
AI/BattleAI/BattleAI.cpp

@@ -54,8 +54,8 @@ void logHexNumbers()
 #if BATTLE_TRACE_LEVEL >= 1
 	logVisual->updateWithLock("hexes", [](IVisualLogBuilder & b)
 		{
-			for(BattleHex hex = BattleHex(0); hex < GameConstants::BFIELD_SIZE; hex = BattleHex(hex + 1))
-				b.addText(hex, std::to_string(hex.hex));
+			for(BattleHex hex = BattleHex(0); hex < GameConstants::BFIELD_SIZE; ++hex)
+				b.addText(hex, std::to_string(hex.toInt()));
 		});
 #endif
 }

+ 1 - 1
AI/BattleAI/BattleAI.h

@@ -88,7 +88,7 @@ public:
 	//void battleResultsApplied() override; //called when all effects of last battle are applied
 	//void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied;
 	//void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn
-	//void battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance) override;
+	//void battleStackMoved(const CStack * stack, BattleHexArray dest, int distance) override;
 	//void battleSpellCast(const BattleSpellCast *sc) override;
 	//void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks
 	//void battleTriggerEffect(const BattleTriggerEffect & bte) override;

+ 36 - 39
AI/BattleAI/BattleEvaluator.cpp

@@ -214,8 +214,8 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 				bestAttack.attackerState->unitType()->getJsonKey(),
 				bestAttack.affectedUnits[0]->unitType()->getJsonKey(),
 				bestAttack.affectedUnits[0]->getCount(),
-				(int)bestAttack.from,
-				(int)bestAttack.attack.attacker->getPosition().hex,
+				bestAttack.from.toInt(),
+				bestAttack.attack.attacker->getPosition().toInt(),
 				bestAttack.attack.chargeDistance,
 				bestAttack.attack.attacker->getMovementRange(0),
 				bestAttack.defenderDamageReduce,
@@ -252,7 +252,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 
 					if(siegeDefense)
 					{
-						logAi->trace("Evaluating exchange at %d self-defense", stack->getPosition().hex);
+						logAi->trace("Evaluating exchange at %d self-defense", stack->getPosition());
 
 						BattleAttackInfo bai(stack, stack, 0, false);
 						AttackPossibility apDefend(stack->getPosition(), stack->getPosition(), bai);
@@ -278,7 +278,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 		score = moveTarget.score;
 		cachedAttack.ap = moveTarget.cachedAttack;
 		cachedAttack.score = score;
-		cachedAttack.turn = moveTarget.turnsToRich;
+		cachedAttack.turn = moveTarget.turnsToReach;
 
 		if(stack->waited())
 		{
@@ -286,7 +286,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 				"Moving %s towards hex %s[%d], score: %2f",
 				stack->getDescription(),
 				moveTarget.cachedAttack->attack.defender->getDescription(),
-				moveTarget.cachedAttack->attack.defender->getPosition().hex,
+				moveTarget.cachedAttack->attack.defender->getPosition(),
 				moveTarget.score);
 
 			return goTowardsNearest(stack, moveTarget.positions, *targets);
@@ -320,14 +320,14 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 	return stack->waited() ?  BattleAction::makeDefend(stack) : BattleAction::makeWait(stack);
 }
 
-uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock> start)
+uint64_t timeElapsed(std::chrono::time_point<std::chrono::steady_clock> start)
 {
-	auto end = std::chrono::high_resolution_clock::now();
+	auto end = std::chrono::steady_clock::now();
 
 	return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
 }
 
-BattleAction BattleEvaluator::moveOrAttack(const CStack * stack, BattleHex hex, const PotentialTargets & targets)
+BattleAction BattleEvaluator::moveOrAttack(const CStack * stack, const BattleHex & hex, const PotentialTargets & targets)
 {
 	auto additionalScore = 0;
 	std::optional<AttackPossibility> attackOnTheWay;
@@ -355,7 +355,7 @@ BattleAction BattleEvaluator::moveOrAttack(const CStack * stack, BattleHex hex,
 	}
 }
 
-BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes, const PotentialTargets & targets)
+BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, const BattleHexArray & hexes, const PotentialTargets & targets)
 {
 	auto reachability = cb->getBattle(battleID)->getReachability(stack);
 	auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false);
@@ -371,37 +371,36 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
 
 	if (siegeDefense)
 	{
-		vstd::erase_if(avHexes, [&](const BattleHex& hex) {
+		avHexes.eraseIf([&](const BattleHex & hex)
+		{
 			return !cb->getBattle(battleID)->battleIsInsideWalls(hex);
 		});
 	}
 
-	if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked
+	if(avHexes.empty() || hexes.empty()) //we are blocked or dest is blocked
 	{
 		return BattleAction::makeDefend(stack);
 	}
 
-	std::vector<BattleHex> targetHexes = hexes;
-
-	vstd::erase_if(targetHexes, [](const BattleHex & hex) { return !hex.isValid(); });
+	BattleHexArray targetHexes = hexes;
 
-	std::sort(targetHexes.begin(), targetHexes.end(), [&](BattleHex h1, BattleHex h2) -> bool
+	targetHexes.sort([&](const BattleHex & h1, const BattleHex & h2) -> bool
 		{
-			return reachability.distances[h1] < reachability.distances[h2];
+			return reachability.distances[h1.toInt()] < reachability.distances[h2.toInt()];
 		});
 
-	BattleHex bestNeighbor = targetHexes.front();
+	BattleHex bestNeighbour = targetHexes.front();
 
-	if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE)
+	if(reachability.distances[bestNeighbour.toInt()] > GameConstants::BFIELD_SIZE)
 	{
-		logAi->trace("No richable hexes.");
+		logAi->trace("No reachable hexes.");
 		return BattleAction::makeDefend(stack);
 	}
 
 	// this turn
-	for(auto hex : targetHexes)
+	for(const auto & hex : targetHexes)
 	{
-		if(vstd::contains(avHexes, hex))
+		if(avHexes.contains(hex))
 		{
 			return moveOrAttack(stack, hex, targets);
 		}
@@ -419,36 +418,31 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
 
 	if(stack->hasBonusOfType(BonusType::FLYING))
 	{
-		std::set<BattleHex> obstacleHexes;
-
-		auto insertAffected = [](const CObstacleInstance & spellObst, std::set<BattleHex> & obstacleHexes) {
-			auto affectedHexes = spellObst.getAffectedTiles();
-			obstacleHexes.insert(affectedHexes.cbegin(), affectedHexes.cend());
-		};
+		BattleHexArray obstacleHexes;
 
 		const auto & obstacles = hb->battleGetAllObstacles();
 
-		for (const auto & obst: obstacles) {
-
+		for (const auto & obst : obstacles) 
+		{
 			if(obst->triggersEffects())
 			{
 				auto triggerAbility =  VLC->spells()->getById(obst->getTrigger());
 				auto triggerIsNegative = triggerAbility->isNegative() || triggerAbility->isDamage();
 
 				if(triggerIsNegative)
-					insertAffected(*obst, obstacleHexes);
+					obstacleHexes.insert(obst->getAffectedTiles());
 			}
 		}
 		// 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
+		auto nearestAvailableHex = vstd::minElementByFun(avHexes, [&](const BattleHex & hex) -> int
 		{
 			const int NEGATIVE_OBSTACLE_PENALTY = 100; // avoid landing on negative obstacle (moat, fire wall, etc)
 			const int BLOCKED_STACK_PENALTY = 100; // avoid landing on moat
 
-			auto distance = BattleHex::getDistance(bestNeighbor, hex);
+			auto distance = BattleHex::getDistance(bestNeighbour, hex);
 
-			if(vstd::contains(obstacleHexes, hex))
+			if(obstacleHexes.contains(hex))
 				distance += NEGATIVE_OBSTACLE_PENALTY;
 
 			return scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, hex) ? BLOCKED_STACK_PENALTY + distance : distance;
@@ -458,7 +452,8 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
 	}
 	else
 	{
-		BattleHex currentDest = bestNeighbor;
+		BattleHex currentDest = bestNeighbour;
+
 		while(true)
 		{
 			if(!currentDest.isValid())
@@ -466,13 +461,13 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
 				return BattleAction::makeDefend(stack);
 			}
 
-			if(vstd::contains(avHexes, currentDest)
+			if(avHexes.contains(currentDest)
 				&& !scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, currentDest))
 			{
 				return moveOrAttack(stack, currentDest, targets);
 			}
 
-			currentDest = reachability.predecessors[currentDest];
+			currentDest = reachability.predecessors[currentDest.toInt()];
 		}
 	}
 	
@@ -691,7 +686,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 				else
 				{
 					auto psFirst = ps.dest.front();
-					auto strWhere = psFirst.unitValue ? psFirst.unitValue->getDescription() : std::to_string(psFirst.hexValue.hex);
+					auto strWhere = psFirst.unitValue ? psFirst.unitValue->getDescription() : std::to_string(psFirst.hexValue.toInt());
 
 					logAi->trace("Evaluating %s at %s", ps.spell->getNameTranslated(), strWhere);
 				}
@@ -760,7 +755,9 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 
 					auto updatedAttack = AttackPossibility::evaluate(updatedBai, cachedAttack.ap->from, innerCache, state);
 
-					stackActionScore = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state);
+					BattleExchangeEvaluator innerEvaluator(scoreEvaluator);
+
+					stackActionScore = innerEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state);
 				}
 				for(const auto & unit : allUnits)
 				{
@@ -807,7 +804,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 							logAi->trace(
 								"Spell %s to %d affects %s (%d), dps: %2f oldHealth: %d newHealth: %d",
 								ps.spell->getNameTranslated(),
-								ps.dest.at(0).hexValue.hex,  // Safe to access .at(0) now
+								ps.dest.at(0).hexValue.toInt(),  // Safe to access .at(0) now
 								unit->creatureId().toCreature()->getNameSingularTranslated(),
 								unit->getCount(),
 								dpsReduce,

+ 2 - 2
AI/BattleAI/BattleEvaluator.h

@@ -51,12 +51,12 @@ public:
 	bool attemptCastingSpell(const CStack * stack);
 	bool canCastSpell();
 	std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack * stack);
-	BattleAction goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes, const PotentialTargets & targets);
+	BattleAction goTowardsNearest(const CStack * stack, const BattleHexArray & hexes, const PotentialTargets & targets);
 	std::vector<BattleHex> getBrokenWallMoatHexes() const;
 	bool hasWorkingTowers() const;
 	void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only
 	void print(const std::string & text) const;
-	BattleAction moveOrAttack(const CStack * stack, BattleHex hex, const PotentialTargets & targets);
+	BattleAction moveOrAttack(const CStack * stack, const BattleHex & hex, const PotentialTargets & targets);
 
 	BattleEvaluator(
 		std::shared_ptr<Environment> env,

+ 97 - 73
AI/BattleAI/BattleExchangeVariant.cpp

@@ -11,6 +11,7 @@
 #include "BattleExchangeVariant.h"
 #include "BattleEvaluator.h"
 #include "../../lib/CStack.h"
+#include "tbb/parallel_for.h"
 
 AttackerValue::AttackerValue()
 	: value(0),
@@ -21,7 +22,7 @@ AttackerValue::AttackerValue()
 MoveTarget::MoveTarget()
 	: positions(), cachedAttack(), score(EvaluationResult::INEFFECTIVE_SCORE)
 {
-	turnsToRich = 1;
+	turnsToReach = 1;
 }
 
 float BattleExchangeVariant::trackAttack(
@@ -310,7 +311,7 @@ ReachabilityInfo getReachabilityWithEnemyBypass(
 
 			for(auto & hex : unit->getHexes())
 				if(hex.isAvailable()) //towers can have <0 pos; we don't also want to overwrite side columns
-					params.destructibleEnemyTurns[hex] = turnsToKill * unit->getMovementRange();
+					params.destructibleEnemyTurns[hex.toInt()] = turnsToKill * unit->getMovementRange();
 		}
 
 		params.bypassEnemyStacks = true;
@@ -348,7 +349,7 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
 		logAi->trace(
 			"Checking movement towards %d of %s",
 			enemy->getCount(),
-			enemy->creatureId().toCreature()->getNameSingularTranslated());
+			VLC->creatures()->getById(enemy->creatureId())->getJsonKey());
 
 		auto distance = dists.distToNearestNeighbour(activeStack, enemy);
 
@@ -361,7 +362,8 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
 		float penaltyMultiplier = 1.0f; // Default multiplier, no penalty
 		float closestAllyDistance = std::numeric_limits<float>::max();
 
-		for (const battle::Unit* ally : hb->battleAliveUnits()) {
+		for (const battle::Unit* ally : hb->battleAliveUnits()) 
+		{
 			if (ally == activeStack) 
 				continue;
 			if (ally->unitSide() != activeStack->unitSide()) 
@@ -375,12 +377,13 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
 		}
 
 		// If an ally is closer to the enemy, compute the penaltyMultiplier
-		if (closestAllyDistance < distance) {
+		if (closestAllyDistance < distance) 
+		{
 			penaltyMultiplier = closestAllyDistance / distance; // Ratio of distances
 		}
 
-		auto turnsToRich = (distance - 1) / speed + 1;
-		auto hexes = enemy->getSurroundingHexes();
+		auto turnsToReach = (distance - 1) / speed + 1;
+		const BattleHexArray & hexes = enemy->getSurroundingHexes();
 		auto enemySpeed = enemy->getMovementRange();
 		auto speedRatio = speed / static_cast<float>(enemySpeed);
 		auto multiplier = (speedRatio > 1 ? 1 : speedRatio) * penaltyMultiplier;
@@ -393,16 +396,16 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
 
 			attack.shootersBlockedDmg = 0; // we do not want to count on it, it is not for sure
 
-			auto score = calculateExchange(attack, turnsToRich, targets, damageCache, hb);
+			auto score = calculateExchange(attack, turnsToReach, targets, damageCache, hb);
 
 			score.enemyDamageReduce *= multiplier;
 
 #if BATTLE_TRACE_LEVEL >= 1
-			logAi->trace("Multiplier: %f, turns: %d, current score %f, new score %f", multiplier, turnsToRich, result.score, scoreValue(score));
+			logAi->trace("Multiplier: %f, turns: %d, current score %f, new score %f", multiplier, turnsToReach, result.score, scoreValue(score));
 #endif
 
 			if(result.score < scoreValue(score)
-				|| (result.turnsToRich > turnsToRich && vstd::isAlmostEqual(result.score, scoreValue(score))))
+				|| (result.turnsToReach > turnsToReach && vstd::isAlmostEqual(result.score, scoreValue(score))))
 			{
 				result.score = scoreValue(score);
 				result.positions.clear();
@@ -411,25 +414,23 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
 				logAi->trace("New high score");
 #endif
 
-				for(const BattleHex & initialEnemyHex : enemy->getAttackableHexes(activeStack))
+				for(BattleHex enemyHex : enemy->getAttackableHexes(activeStack))
 				{
-					BattleHex enemyHex = initialEnemyHex;
-
-					while(!flying && dists.distances[enemyHex] > speed && dists.predecessors.at(enemyHex).isValid())
+					while(!flying && dists.distances[enemyHex.toInt()] > speed && dists.predecessors.at(enemyHex.toInt()).isValid())
 					{
-						enemyHex = dists.predecessors.at(enemyHex);
+						enemyHex = dists.predecessors.at(enemyHex.toInt());
 
-						if(dists.accessibility[enemyHex] == EAccessibility::ALIVE_STACK)
+						if(dists.accessibility[enemyHex.toInt()] == EAccessibility::ALIVE_STACK)
 						{
 							auto defenderToBypass = hb->battleGetUnitByPos(enemyHex);
 
 							if(defenderToBypass)
 							{
 #if BATTLE_TRACE_LEVEL >= 1
-								logAi->trace("Found target to bypass at %d", enemyHex.hex);
+								logAi->trace("Found target to bypass at %d", enemyHex.toInt());
 #endif
 
-								auto attackHex = dists.predecessors[enemyHex];
+								auto attackHex = dists.predecessors[enemyHex.toInt()];
 								auto baiBypass = BattleAttackInfo(activeStack, defenderToBypass, 0, cb->battleCanShoot(activeStack));
 								auto attackBypass = AttackPossibility::evaluate(baiBypass, attackHex, damageCache, hb);
 
@@ -440,7 +441,7 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
 
 								auto bypassScore = calculateExchange(
 									attackBypass,
-									dists.distances[attackHex],
+									dists.distances[attackHex.toInt()],
 									targets,
 									damageCache,
 									hb,
@@ -458,11 +459,11 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
 						}
 					}
 
-					result.positions.push_back(enemyHex);
+					result.positions.insert(enemyHex);
 				}
 
 				result.cachedAttack = attack;
-				result.turnsToRich = turnsToRich;
+				result.turnsToReach = turnsToReach;
 			}
 		}
 	}
@@ -470,10 +471,10 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
 	return result;
 }
 
-std::vector<const battle::Unit *> BattleExchangeEvaluator::getAdjacentUnits(const battle::Unit * blockerUnit) const
+battle::Units BattleExchangeEvaluator::getAdjacentUnits(const battle::Unit * blockerUnit) const
 {
 	std::queue<const battle::Unit *> queue;
-	std::vector<const battle::Unit *> checkedStacks;
+	battle::Units checkedStacks;
 
 	queue.push(blockerUnit);
 
@@ -484,15 +485,15 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getAdjacentUnits(cons
 		queue.pop();
 		checkedStacks.push_back(stack);
 
-		auto hexes = stack->getSurroundingHexes();
-		for(auto hex : hexes)
+		auto const & hexes = stack->getSurroundingHexes();
+		for(const auto & hex : hexes)
 		{
-			auto neighbor = cb->battleGetUnitByPos(hex);
+			auto neighbour = cb->battleGetUnitByPos(hex);
 
-			if(neighbor && neighbor->unitSide() == stack->unitSide() && !vstd::contains(checkedStacks, neighbor))
+			if(neighbour && neighbour->unitSide() == stack->unitSide() && !vstd::contains(checkedStacks, neighbour))
 			{
-				queue.push(neighbor);
-				checkedStacks.push_back(neighbor);
+				queue.push(neighbour);
+				checkedStacks.push_back(neighbour);
 			}
 		}
 	}
@@ -505,26 +506,27 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
 	uint8_t turn,
 	PotentialTargets & targets,
 	std::shared_ptr<HypotheticBattle> hb,
-	std::vector<const battle::Unit *> additionalUnits) const
+	battle::Units additionalUnits) const
 {
 	ReachabilityData result;
 
 	auto hexes = ap.attack.defender->getSurroundingHexes();
 
-	if(!ap.attack.shooting) hexes.push_back(ap.from);
+	if(!ap.attack.shooting) 
+		hexes.insert(ap.from);
 
-	std::vector<const battle::Unit *> allReachableUnits = additionalUnits;
+	battle::Units allReachableUnits = additionalUnits;
 	
-	for(auto hex : hexes)
+	for(const auto & hex : hexes)
 	{
-		vstd::concatenate(allReachableUnits, turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex));
+		vstd::concatenate(allReachableUnits, getOneTurnReachableUnits(turn, hex));
 	}
 
 	if(!ap.attack.attacker->isTurret())
 	{
-		for(auto hex : ap.attack.attacker->getHexes())
+		for(const auto & hex : ap.attack.attacker->getHexes())
 		{
-			auto unitsReachingAttacker = turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex);
+			auto unitsReachingAttacker = getOneTurnReachableUnits(turn, hex);
 			for(auto unit : unitsReachingAttacker)
 			{
 				if(unit->unitSide() != ap.attack.attacker->unitSide())
@@ -575,7 +577,7 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
 
 		if(!accessible)
 		{
-			for(auto hex : unit->getSurroundingHexes())
+			for(const auto & hex : unit->getSurroundingHexes())
 			{
 				if(ap.attack.defender->coversPos(hex))
 				{
@@ -634,10 +636,10 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
 	PotentialTargets & targets,
 	DamageCache & damageCache,
 	std::shared_ptr<HypotheticBattle> hb,
-	std::vector<const battle::Unit *> additionalUnits) const
+	battle::Units additionalUnits) const
 {
 #if BATTLE_TRACE_LEVEL>=1
-	logAi->trace("Battle exchange at %d", ap.attack.shooting ? ap.dest.hex : ap.from.hex);
+	logAi->trace("Battle exchange at %d", ap.attack.shooting ? ap.dest.toInt() : ap.from.toInt());
 #endif
 
 	if(cb->battleGetMySide() == BattleSide::LEFT_SIDE
@@ -647,8 +649,8 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
 		return BattleScore(EvaluationResult::INEFFECTIVE_SCORE, 0);
 	}
 
-	std::vector<const battle::Unit *> ourStacks;
-	std::vector<const battle::Unit *> enemyStacks;
+	battle::Units ourStacks;
+	battle::Units enemyStacks;
 
 	if(hb->battleGetUnitByID(ap.attack.defender->unitId())->alive())
 		enemyStacks.push_back(ap.attack.defender);
@@ -798,7 +800,9 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
 							if(!u->getPosition().isValid())
 								return false; // e.g. tower shooters
 
-							return vstd::contains_if(reachabilityMap.at(u->getPosition()), [&attacker](const battle::Unit * other) -> bool
+							const auto & reachableUnits = getOneTurnReachableUnits(0, u->getPosition());
+
+							return vstd::contains_if(reachableUnits, [&attacker](const battle::Unit * other) -> bool
 								{
 									return attacker->unitId() == other->unitId();
 								});
@@ -885,7 +889,7 @@ bool BattleExchangeEvaluator::canBeHitThisTurn(const AttackPossibility & ap)
 {
 	for(auto pos : ap.attack.attacker->getSurroundingHexes())
 	{
-		for(auto u : reachabilityMap[pos])
+		for(auto u : getOneTurnReachableUnits(0, pos))
 		{
 			if(u->unitSide() != ap.attack.attacker->unitSide())
 			{
@@ -897,34 +901,48 @@ bool BattleExchangeEvaluator::canBeHitThisTurn(const AttackPossibility & ap)
 	return false;
 }
 
-void BattleExchangeEvaluator::updateReachabilityMap(std::shared_ptr<HypotheticBattle> hb)
+void ReachabilityMapCache::update(const std::vector<battle::Units> & turnOrder, std::shared_ptr<HypotheticBattle> hb)
 {
-	const int TURN_DEPTH = 2;
-
-	turnOrder.clear();
-
-	hb->battleGetTurnOrder(turnOrder, std::numeric_limits<int>::max(), TURN_DEPTH);
-
 	for(auto turn : turnOrder)
 	{
 		for(auto u : turn)
 		{
-			if(!vstd::contains(reachabilityCache, u->unitId()))
+			if(!vstd::contains(unitReachabilityMap, u->unitId()))
 			{
-				reachabilityCache[u->unitId()] = hb->getReachability(u);
+				unitReachabilityMap[u->unitId()] = hb->getReachability(u);
 			}
 		}
 	}
 
-	for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1)
+	hexReachabilityPerTurn.clear();
+}
+
+void BattleExchangeEvaluator::updateReachabilityMap(std::shared_ptr<HypotheticBattle> hb)
+{
+	const int TURN_DEPTH = 2;
+
+	turnOrder.clear();
+
+	hb->battleGetTurnOrder(turnOrder, std::numeric_limits<int>::max(), TURN_DEPTH);
+	reachabilityMap.update(turnOrder, hb);
+}
+
+const battle::Units & ReachabilityMapCache::getOneTurnReachableUnits(std::shared_ptr<CBattleInfoCallback> cb, std::shared_ptr<Environment> env, const std::vector<battle::Units> & turnOrder, uint8_t turn, const BattleHex & hex)
+{
+	auto & turnData = hexReachabilityPerTurn[turn];
+
+	if (!turnData.isValid[hex.toInt()])
 	{
-		reachabilityMap[hex] = getOneTurnReachableUnits(0, hex);
+		turnData.hexes[hex.toInt()] = computeOneTurnReachableUnits(cb, env, turnOrder, turn, hex);
+		turnData.isValid.set(hex.toInt());
 	}
+
+	return turnData.hexes[hex.toInt()];
 }
 
-std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUnits(uint8_t turn, BattleHex hex) const
+battle::Units ReachabilityMapCache::computeOneTurnReachableUnits(std::shared_ptr<CBattleInfoCallback> cb, std::shared_ptr<Environment> env, const std::vector<battle::Units> & turnOrder, uint8_t turn, const BattleHex & hex)
 {
-	std::vector<const battle::Unit *> result;
+	battle::Units result;
 
 	for(int i = 0; i < turnOrder.size(); i++, turn++)
 	{
@@ -946,22 +964,22 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUn
 			auto unitSpeed = unit->getMovementRange(turn);
 			auto radius = unitSpeed * (turn + 1);
 
-			auto reachabilityIter = reachabilityCache.find(unit->unitId());
-			assert(reachabilityIter != reachabilityCache.end()); // missing updateReachabilityMap call?
+			auto reachabilityIter = unitReachabilityMap.find(unit->unitId());
+			assert(reachabilityIter != unitReachabilityMap.end()); // missing updateReachabilityMap call?
 
-			ReachabilityInfo unitReachability = reachabilityIter != reachabilityCache.end() ? reachabilityIter->second : turnBattle.getReachability(unit);
+			ReachabilityInfo unitReachability = reachabilityIter != unitReachabilityMap.end() ? reachabilityIter->second : turnBattle.getReachability(unit);
 
-			bool reachable = unitReachability.distances.at(hex) <= radius;
+			bool reachable = unitReachability.distances.at(hex.toInt()) <= radius;
 
-			if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK)
+			if(!reachable && unitReachability.accessibility[hex.toInt()] == EAccessibility::ALIVE_STACK)
 			{
 				const battle::Unit * hexStack = cb->battleGetUnitByPos(hex);
 
 				if(hexStack && cb->battleMatchOwner(unit, hexStack, false))
 				{
-					for(BattleHex neighbor : hex.neighbouringTiles())
+					for(const BattleHex & neighbour : hex.getNeighbouringTiles())
 					{
-						reachable = unitReachability.distances.at(neighbor) <= radius;
+						reachable = unitReachability.distances.at(neighbour.toInt()) <= radius;
 
 						if(reachable) break;
 					}
@@ -978,8 +996,13 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUn
 	return result;
 }
 
+const battle::Units & BattleExchangeEvaluator::getOneTurnReachableUnits(uint8_t turn, const BattleHex & hex) const
+{
+	return reachabilityMap.getOneTurnReachableUnits(cb, env, turnOrder, turn, hex);
+}
+
 // avoid blocking path for stronger stack by weaker stack
-bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb, const battle::Unit * activeUnit, BattleHex position)
+bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb, const battle::Unit * activeUnit, const BattleHex & position)
 {
 	const int BLOCKING_THRESHOLD = 70;
 	const int BLOCKING_OWN_ATTACK_PENALTY = 100;
@@ -1008,38 +1031,39 @@ bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb
 			auto unitReachability = turnBattle.getReachability(unit);
 			auto unitSpeed = unit->getMovementRange(turn); // Cached value, to avoid performance hit
 
-			for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1)
+			for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); ++hex)
 			{
 				bool enemyUnit = false;
-				bool reachable = unitReachability.distances.at(hex) <= unitSpeed;
+				bool reachable = unitReachability.distances.at(hex.toInt()) <= unitSpeed;
 
-				if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK)
+				if(!reachable && unitReachability.accessibility[hex.toInt()] == EAccessibility::ALIVE_STACK)
 				{
 					const battle::Unit * hexStack = turnBattle.battleGetUnitByPos(hex);
 
 					if(hexStack && cb->battleMatchOwner(unit, hexStack, false))
 					{
 						enemyUnit = true;
-
-						for(BattleHex neighbor : hex.neighbouringTiles())
+						for(const BattleHex & neighbour : hex.getNeighbouringTiles())
 						{
-							reachable = unitReachability.distances.at(neighbor) <= unitSpeed;
+							reachable = unitReachability.distances.at(neighbour.toInt()) <= unitSpeed;
 
 							if(reachable) break;
 						}
 					}
 				}
 
-				if(!reachable && std::count(reachabilityMap[hex].begin(), reachabilityMap[hex].end(), unit) > 1)
+				if(!reachable)
 				{
-					blockingScore += ratio * (enemyUnit ? BLOCKING_OWN_ATTACK_PENALTY : BLOCKING_OWN_MOVE_PENALTY);
+					auto reachableUnits = getOneTurnReachableUnits(0, hex);
+					if (std::count(reachableUnits.begin(), reachableUnits.end(), unit) > 1)
+						blockingScore += ratio * (enemyUnit ? BLOCKING_OWN_ATTACK_PENALTY : BLOCKING_OWN_MOVE_PENALTY);
 				}
 			}
 		}
 	}
 
 #if BATTLE_TRACE_LEVEL>=1
-	logAi->trace("Position %d, blocking score %f", position.hex, blockingScore);
+	logAi->trace("Position %d, blocking score %f", position.toInt(), blockingScore);
 #endif
 
 	return blockingScore > BLOCKING_THRESHOLD;

+ 28 - 12
AI/BattleAI/BattleExchangeVariant.h

@@ -54,9 +54,9 @@ struct AttackerValue
 struct MoveTarget
 {
 	float score;
-	std::vector<BattleHex> positions;
+	BattleHexArray positions;
 	std::optional<AttackPossibility> cachedAttack;
-	uint8_t turnsToRich;
+	uint8_t turnsToReach;
 
 	MoveTarget();
 };
@@ -112,24 +112,40 @@ private:
 
 struct ReachabilityData
 {
-	std::map<int, std::vector<const battle::Unit *>> units;
+	std::map<int, battle::Units> units;
 
 	// shooters which are within mellee attack and mellee units
-	std::vector<const battle::Unit *> melleeAccessible;
+	battle::Units melleeAccessible;
 
 	// far shooters
-	std::vector<const battle::Unit *> shooters;
+	battle::Units shooters;
 
 	std::set<uint32_t> enemyUnitsReachingAttacker;
 };
 
+class ReachabilityMapCache
+{
+	struct PerTurnData{
+		std::bitset<GameConstants::BFIELD_SIZE> isValid;
+		std::array<battle::Units, GameConstants::BFIELD_SIZE> hexes;
+	};
+
+	std::map<uint32_t, ReachabilityInfo> unitReachabilityMap; // unit ID -> reachability
+	std::map<uint32_t, PerTurnData> hexReachabilityPerTurn;
+
+	//const ReachabilityInfo & update();
+	battle::Units computeOneTurnReachableUnits(std::shared_ptr<CBattleInfoCallback> cb, std::shared_ptr<Environment> env, const std::vector<battle::Units> & turnOrder, uint8_t turn, const BattleHex & hex);
+public:
+	const battle::Units & getOneTurnReachableUnits(std::shared_ptr<CBattleInfoCallback> cb, std::shared_ptr<Environment> env, const std::vector<battle::Units> & turnOrder, uint8_t turn, const BattleHex & hex);
+	void update(const std::vector<battle::Units> & turnOrder, std::shared_ptr<HypotheticBattle> hb);
+};
+
 class BattleExchangeEvaluator
 {
 private:
 	std::shared_ptr<CBattleInfoCallback> cb;
 	std::shared_ptr<Environment> env;
-	std::map<uint32_t, ReachabilityInfo> reachabilityCache;
-	std::map<BattleHex, std::vector<const battle::Unit *>> reachabilityMap;
+	mutable ReachabilityMapCache reachabilityMap;
 	std::vector<battle::Units> turnOrder;
 	float negativeEffectMultiplier;
 	int simulationTurnsCount;
@@ -142,7 +158,7 @@ private:
 		PotentialTargets & targets,
 		DamageCache & damageCache,
 		std::shared_ptr<HypotheticBattle> hb,
-		std::vector<const battle::Unit *> additionalUnits = {}) const;
+		battle::Units additionalUnits = {}) const;
 
 	bool canBeHitThisTurn(const AttackPossibility & ap);
 
@@ -169,7 +185,7 @@ public:
 		DamageCache & damageCache,
 		std::shared_ptr<HypotheticBattle> hb) const;
 
-	std::vector<const battle::Unit *> getOneTurnReachableUnits(uint8_t turn, BattleHex hex) const;
+	const battle::Units & getOneTurnReachableUnits(uint8_t turn, const BattleHex & hex) const;
 	void updateReachabilityMap(std::shared_ptr<HypotheticBattle> hb);
 
 	ReachabilityData getExchangeUnits(
@@ -177,9 +193,9 @@ public:
 		uint8_t turn,
 		PotentialTargets & targets,
 		std::shared_ptr<HypotheticBattle> hb,
-		std::vector<const battle::Unit *> additionalUnits = {}) const;
+		battle::Units additionalUnits = {}) const;
 
-	bool checkPositionBlocksOurStacks(HypotheticBattle & hb, const battle::Unit * unit, BattleHex position);
+	bool checkPositionBlocksOurStacks(HypotheticBattle & hb, const battle::Unit * unit, const BattleHex & position);
 
 	MoveTarget findMoveTowardsUnreachable(
 		const battle::Unit * activeStack,
@@ -187,7 +203,7 @@ public:
 		DamageCache & damageCache,
 		std::shared_ptr<HypotheticBattle> hb);
 
-	std::vector<const battle::Unit *> getAdjacentUnits(const battle::Unit * unit) const;
+	battle::Units getAdjacentUnits(const battle::Unit * unit) const;
 
 	float getPositiveEffectMultiplier() const { return 1; }
 	float getNegativeEffectMultiplier() const { return negativeEffectMultiplier; }

+ 3 - 3
AI/BattleAI/PotentialTargets.cpp

@@ -48,9 +48,9 @@ PotentialTargets::PotentialTargets(
 		if(!forceTarget && !state->battleMatchOwner(attackerInfo, defender))
 			continue;
 
-		auto GenerateAttackInfo = [&](bool shooting, BattleHex hex) -> AttackPossibility
+		auto GenerateAttackInfo = [&](bool shooting, const BattleHex & hex) -> AttackPossibility
 		{
-			int distance = hex.isValid() ? reachability.distances[hex] : 0;
+			int distance = hex.isValid() ? reachability.distances[hex.toInt()] : 0;
 			auto bai = BattleAttackInfo(attackerInfo, defender, distance, shooting);
 
 			return AttackPossibility::evaluate(bai, hex, damageCache, state);
@@ -69,7 +69,7 @@ PotentialTargets::PotentialTargets(
 		}
 		else
 		{
-			for(BattleHex hex : avHexes)
+			for(const BattleHex & hex : avHexes)
 			{
 				if(!CStack::isMeleeAttackPossible(attackerInfo, defender, hex))
 					continue;

+ 1 - 1
AI/BattleAI/PotentialTargets.h

@@ -14,7 +14,7 @@ class PotentialTargets
 {
 public:
 	std::vector<AttackPossibility> possibleAttacks;
-	std::vector<const battle::Unit *> unreachableEnemies;
+	battle::Units unreachableEnemies;
 
 	PotentialTargets(){};
 	PotentialTargets(

+ 3 - 3
AI/BattleAI/StackWithBonuses.cpp

@@ -170,7 +170,7 @@ TConstBonusListPtr StackWithBonuses::getAllBonuses(const CSelector & selector, c
 	return ret;
 }
 
-int64_t StackWithBonuses::getTreeVersion() const
+int32_t StackWithBonuses::getTreeVersion() const
 {
 	auto result = owner->getTreeVersion();
 
@@ -360,7 +360,7 @@ void HypotheticBattle::addUnit(uint32_t id, const JsonNode & data)
 	stackStates[newUnit->unitId()] = newUnit;
 }
 
-void HypotheticBattle::moveUnit(uint32_t id, BattleHex destination)
+void HypotheticBattle::moveUnit(uint32_t id, const BattleHex & destination)
 {
 	std::shared_ptr<StackWithBonuses> changed = getForUpdate(id);
 	changed->position = destination;
@@ -485,7 +485,7 @@ BattleLayout HypotheticBattle::getLayout() const
 	return subject->getBattle()->getLayout();
 }
 
-int64_t HypotheticBattle::getTreeVersion() const
+int32_t HypotheticBattle::getTreeVersion() const
 {
 	return getBonusBearer()->getTreeVersion() + bonusTreeVersion;
 }

+ 3 - 3
AI/BattleAI/StackWithBonuses.h

@@ -93,7 +93,7 @@ public:
 	TConstBonusListPtr getAllBonuses(const CSelector & selector, const CSelector & limit,
 		const std::string & cachingStr = "") const override;
 
-	int64_t getTreeVersion() const override;
+	int32_t getTreeVersion() const override;
 
 	void addUnitBonus(const std::vector<Bonus> & bonus);
 	void updateUnitBonus(const std::vector<Bonus> & bonus);
@@ -141,7 +141,7 @@ public:
 
 	void addUnit(uint32_t id, const JsonNode & data) override;
 	void setUnitState(uint32_t id, const JsonNode & data, int64_t healthDelta) override;
-	void moveUnit(uint32_t id, BattleHex destination) override;
+	void moveUnit(uint32_t id, const BattleHex & destination) override;
 	void removeUnit(uint32_t id) override;
 	void updateUnit(uint32_t id, const JsonNode & data) override;
 
@@ -162,7 +162,7 @@ public:
 	int3 getLocation() const override;
 	BattleLayout getLayout() const override;
 
-	int64_t getTreeVersion() const;
+	int32_t getTreeVersion() const;
 
 	void makeWait(const battle::Unit * activeStack);
 

+ 1 - 0
AI/CMakeLists.txt

@@ -35,6 +35,7 @@ if(NOT fuzzylite_FOUND)
 		add_compile_options(-Wno-error=deprecated-declarations)
 	endif()
 	add_subdirectory(FuzzyLite/fuzzylite EXCLUDE_FROM_ALL)
+	set_property(TARGET fl-static PROPERTY CXX_STANDARD 14) # doesn't compile under 17 due to using removed symbol(s)
 	add_library(fuzzylite::fuzzylite ALIAS fl-static)
 	target_include_directories(fl-static PUBLIC ${CMAKE_HOME_DIRECTORY}/AI/FuzzyLite/fuzzylite)
 endif()

+ 15 - 5
AI/Nullkiller/AIGateway.cpp

@@ -1092,19 +1092,24 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance
 				}
 				if(!emptySlotFound) //try to put that atifact in already occupied slot
 				{
+					int64_t artifactScore = getArtifactScoreForHero(target, artifact);
+
 					for(auto slot : artifact->getType()->getPossibleSlots().at(target->bearerType()))
 					{
 						auto otherSlot = target->getSlot(slot);
 						if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one
 						{
+							int64_t otherArtifactScore = getArtifactScoreForHero(target, otherSlot->artifact);
+							logAi->trace( "Comparing artifacts of %s: %s vs %s. Score: %d vs %d", target->getHeroTypeName(), artifact->getType()->getJsonKey(), otherSlot->artifact->getType()->getJsonKey(), artifactScore, otherArtifactScore);
+
 							//if that artifact is better than what we have, pick it
-							if(compareArtifacts(artifact, otherSlot->artifact)
-								&& artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move
+							//combined artifacts are not always allowed to move
+							if(artifactScore > otherArtifactScore && artifact->canBePutAt(target, slot, true))
 							{
 								logAi->trace(
 									"Exchange artifacts %s <-> %s",
-									artifact->getType()->getNameTranslated(),
-									otherSlot->artifact->getType()->getNameTranslated());
+									artifact->getType()->getJsonKey(),
+									otherSlot->artifact->getType()->getJsonKey());
 
 								if(!otherSlot->artifact->canBePutAt(artHolder, location.slot, true))
 								{
@@ -1291,7 +1296,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 	else
 	{
 		CGPath path;
-		cb->getPathsInfo(h.get())->getPath(path, dst);
+		nullkiller->getPathsInfo(h.get())->getPath(path, dst);
 		if(path.nodes.empty())
 		{
 			logAi->error("Hero %s cannot reach %s.", h->getNameTranslated(), dst.toString());
@@ -1803,4 +1808,9 @@ bool AIStatus::channelProbing()
 	return ongoingChannelProbing;
 }
 
+void AIGateway::invalidatePaths()
+{
+	nullkiller->invalidatePaths();
+}
+
 }

+ 2 - 0
AI/Nullkiller/AIGateway.h

@@ -159,6 +159,8 @@ public:
 	void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, BattleSide side, bool replayAllowed) override;
 	void battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) override;
 
+	void invalidatePaths() override;
+
 	void makeTurn();
 
 	void buildArmyIn(const CGTownInstance * t);

+ 328 - 6
AI/Nullkiller/AIUtility.cpp

@@ -18,6 +18,8 @@
 #include "../../lib/mapping/CMapDefines.h"
 #include "../../lib/gameState/QuestInfo.h"
 #include "../../lib/IGameSettings.h"
+#include "../../lib/bonuses/Limiters.h"
+#include "../../lib/bonuses/Propagators.h"
 
 #include <vcmi/CreatureService.h>
 
@@ -265,15 +267,335 @@ bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2)
 	return a1->getArmyStrength() < a2->getArmyStrength();
 }
 
-bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2)
+double getArtifactBonusRelevance(const CGHeroInstance * hero, const std::shared_ptr<Bonus> & bonus)
 {
-	auto art1 = a1->getType();
-	auto art2 = a2->getType();
+	if (bonus->propagator && bonus->limiter && bonus->propagator->getPropagatorType() == CBonusSystemNode::BATTLE)
+	{
+		// assume that this is battle wide / other side propagator+limiter
+		// consider it as fully relevant since we don't know about future combat when equipping artifacts
+		return 1.0;
+	}
+
+	const auto & getArmyRatioAffectedByLimiter = [&]()
+	{
+		if (!bonus->limiter)
+			return 1.0;
+
+		uint64_t totalStrength = 0;
+		uint64_t affectedStrength = 0;
+
+		const BonusList stillUndecided;
+
+		for (const auto & slot : hero->Slots())
+		{
+			const auto allBonuses = slot.second->getAllBonuses(Selector::all, Selector::all);
+			BonusLimitationContext context = {*bonus, *slot.second, *allBonuses, stillUndecided};
+
+			uint64_t unitStrength = slot.second->getPower();
+
+			if (bonus->limiter->limit(context) == ILimiter::EDecision::ACCEPT)
+				affectedStrength += unitStrength;
+			totalStrength += unitStrength;
+		}
+
+		if (totalStrength == 0)
+			return 0.0;
+
+		return static_cast<double>(affectedStrength) / totalStrength;
+	};
+
+	const auto & getArmyPercentageWithBonus = [&](BonusType type)
+	{
+		uint64_t totalStrength = 0;
+		uint64_t affectedStrength = 0;
+
+		for (const auto & slot : hero->Slots())
+		{
+			uint64_t unitStrength = slot.second->getPower();
+			if (slot.second->hasBonusOfType(type))
+				affectedStrength += unitStrength;
+			totalStrength += unitStrength;
+		}
+
+		if (totalStrength == 0)
+			return 0.0;
+
+		return static_cast<double>(affectedStrength) / totalStrength;
+	};
 
-	if(art1->getPrice() == art2->getPrice())
-		return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL);
+	const auto & getSpellSchoolKnownSpellsFactor = [&](SpellSchool school)
+	{
+		uint64_t totalWeight = 0;
+		uint64_t knownWeight = 0;
+
+		for (auto spellID : VLC->spellh->getDefaultAllowed())
+		{
+			auto spell = spellID.toEntity(VLC);
+			if (!spell->hasSchool(school))
+				continue;
+
+			uint64_t spellLevel = spell->getLevel();
+			uint64_t spellWeight = spellLevel * spellLevel;
+			if (!hero->spellbookContainsSpell(spellID))
+				knownWeight += spellWeight;
+			totalWeight += spellWeight;
+		}
+		if (totalWeight == 0)
+			return 0.0;
+
+		return static_cast<double>(knownWeight) / totalWeight;
+	};
+
+	const auto & getSpellLevelKnownSpellsFactor = [&](int level)
+	{
+		uint64_t totalWeight = 0;
+		uint64_t knownWeight = 0;
+
+		for (auto spellID : VLC->spellh->getDefaultAllowed())
+		{
+			auto spell = spellID.toEntity(VLC);
+			if (spell->getLevel() != level)
+				continue;
+
+			if (!hero->spellbookContainsSpell(spellID))
+				knownWeight += 1;
+			totalWeight += 1;
+		}
+		if (totalWeight == 0)
+			return 0.0;
+
+		return static_cast<double>(knownWeight) / totalWeight;
+	};
+
+	constexpr double notRelevant = 0.0;  // artifact is not useful in current conditions
+	constexpr double relevant = 1.0;
+	constexpr double veryRelevant = 2.0; // for very situational artifacts, e.g. skill-specific, or army composition-specific
+
+	switch (bonus->type)
+	{
+		case BonusType::MOVEMENT:
+			if (hero->boat && bonus->subtype == BonusCustomSubtype::heroMovementSea)
+				return veryRelevant;
+
+			if (!hero->boat && bonus->subtype == BonusCustomSubtype::heroMovementLand)
+				return relevant;
+			return notRelevant;
+		case BonusType::STACKS_SPEED:
+		case BonusType::STACK_HEALTH:
+			return getArmyRatioAffectedByLimiter();
+		case BonusType::MORALE:
+			return getArmyRatioAffectedByLimiter() * (1 - getArmyPercentageWithBonus(BonusType::UNDEAD)); // TODO: other unaffected, e.g. Golems
+		case BonusType::LUCK:
+			return getArmyRatioAffectedByLimiter(); // Do we have luck?
+		case BonusType::PRIMARY_SKILL:
+			if (bonus->subtype == PrimarySkill::ATTACK || bonus->subtype == PrimarySkill::DEFENSE)
+				return getArmyRatioAffectedByLimiter(); // e.g. Vial of Dragonblood - consider only affected unit
+			else
+				return relevant; // spellpower / knowledge - always relevant
+		case BonusType::WATER_WALKING:
+		case BonusType::FLYING_MOVEMENT:
+			return hero->boat ? notRelevant : relevant; // boat can't fly
+		case BonusType::WHIRLPOOL_PROTECTION:
+			return hero->boat ? relevant : notRelevant;
+		case BonusType::UNDEAD_RAISE_PERCENTAGE:
+			return hero->hasBonusOfType(BonusType::IMPROVED_NECROMANCY) ? veryRelevant : notRelevant;
+		case BonusType::SPELL_DAMAGE:
+		case BonusType::SPELL_DURATION:
+			return hero->hasSpellbook() ? relevant : notRelevant;
+		case BonusType::PERCENTAGE_DAMAGE_BOOST:
+			if (bonus->subtype == BonusCustomSubtype::damageTypeRanged)
+				return veryRelevant * getArmyPercentageWithBonus(BonusType::SHOOTER);
+			if (bonus->subtype == BonusCustomSubtype::damageTypeMelee)
+				return veryRelevant * (1 - getArmyPercentageWithBonus(BonusType::SHOOTER));
+			return 0;
+		case BonusType::FULL_MANA_REGENERATION:
+		case BonusType::MANA_REGENERATION:
+			return hero->mana < hero->manaLimit() ? relevant : notRelevant;
+		case BonusType::LEARN_BATTLE_SPELL_CHANCE:
+			return hero->hasBonusOfType(BonusType::LEARN_BATTLE_SPELL_LEVEL_LIMIT) ? relevant : notRelevant;
+		case BonusType::NO_DISTANCE_PENALTY:
+		case BonusType::NO_WALL_PENALTY:
+			return getArmyPercentageWithBonus(BonusType::SHOOTER) * veryRelevant;
+		case BonusType::SPELLS_OF_SCHOOL:
+			if (!hero->hasSpellbook())
+				return notRelevant;
+			return 1 - getSpellSchoolKnownSpellsFactor(bonus->subtype.as<SpellSchool>());
+		case BonusType::SPELLS_OF_LEVEL:
+			if (!hero->hasSpellbook())
+				return notRelevant;
+			return 1 - getSpellLevelKnownSpellsFactor(bonus->subtype.getNum());
+// Potential TODO's
+//		case BonusType::MAGIC_RESISTANCE:
+//		case BonusType::FREE_SHIP_BOARDING:
+//		case BonusType::GENERATE_RESOURCE:
+//		case BonusType::CREATURE_GROWTH:
+//
+//		case BonusType::SPELLS_OF_LEVEL:
+//		case BonusType::SIGHT_RADIUS:
+	}
+
+	return 1.0;
+}
+
+int32_t getArtifactBonusScoreImpl(const std::shared_ptr<Bonus> & bonus)
+{
+	switch (bonus->type)
+	{
+		case BonusType::MOVEMENT:
+			if (bonus->subtype == BonusCustomSubtype::heroMovementLand)
+				return bonus->val * 20;
+			if (bonus->subtype == BonusCustomSubtype::heroMovementSea)
+				return bonus->val * 10;
+			return 0;
+		case BonusType::STACKS_SPEED:
+			return bonus->val * 8000;
+		case BonusType::MORALE:
+			return bonus->val * 1500;
+		case BonusType::LUCK:
+			return bonus->val * 1000;
+		case BonusType::PRIMARY_SKILL:
+			return bonus->val * 1000;
+		case BonusType::SURRENDER_DISCOUNT:
+			return 0; // irrelevant in gameplay
+		case BonusType::WATER_WALKING:
+			return 5000;
+		case BonusType::FREE_SHIP_BOARDING:
+			return 10000;
+		case BonusType::WHIRLPOOL_PROTECTION:
+			return 5000;
+		case BonusType::FLYING_MOVEMENT:
+			return 20000;
+		case BonusType::UNDEAD_RAISE_PERCENTAGE:
+			return bonus->val * 400;
+		case BonusType::GENERATE_RESOURCE:
+			return bonus->val * VLC->objh->resVals.at(bonus->subtype.as<GameResID>().getNum()) * 10;
+		case BonusType::SPELL_DURATION:
+			return bonus->val * 200;
+		case BonusType::MAGIC_RESISTANCE:
+			return bonus->val * 400;
+		case BonusType::PERCENTAGE_DAMAGE_BOOST:
+			if (bonus->subtype == BonusCustomSubtype::damageTypeRanged)
+				return bonus->val * 200;
+			if (bonus->subtype == BonusCustomSubtype::damageTypeMelee)
+				return bonus->val * 500;
+			return 0;
+		case BonusType::CREATURE_GROWTH:
+			return (1+bonus->subtype.getNum()) * bonus->val * 400;
+		case BonusType::FULL_MANA_REGENERATION:
+			return 15000;
+		case BonusType::MANA_REGENERATION:
+			return bonus->val * 500;
+		case BonusType::SPELLS_OF_SCHOOL:
+			return 20000;
+		case BonusType::SPELLS_OF_LEVEL:
+			return bonus->subtype.getNum() * 6000;
+		case BonusType::SPELL_DAMAGE:
+			return bonus->val * 120;
+		case BonusType::SIGHT_RADIUS:
+			return bonus->val * 1000;
+		case BonusType::LEARN_BATTLE_SPELL_CHANCE:
+			return 0; // irrelevant in gameplay
+		case BonusType::STACK_HEALTH:
+			return bonus->val * 5000;
+		case BonusType::NO_DISTANCE_PENALTY:
+			return 10000;
+		case BonusType::NO_WALL_PENALTY:
+			return 5000;
+	}
+	return 0;
+	// Additional bonuses to consider from H3 artifacts:
+	// MIND_IMMUNITY
+	// BLOCK_MAGIC_ABOVE
+	// SPELL_IMMUNITY
+	// NEGATE_ALL_NATURAL_IMMUNITIES
+	// SPELL_RESISTANCE_AURA
+	// SPELL
+	// BATTLE_NO_FLEEING
+	// BLOCK_ALL_MAGIC
+	// NONEVIL_ALIGNMENT_MIX
+	// OPENING_BATTLE_SPELL
+	// IMPROVED_NECROMANCY
+	// HP_REGENERATION
+	// CREATURE_GROWTH_PERCENT
+	// LEVEL_SPELL_IMMUNITY
+	// FREE_SHOOTING
+	// FULL_MANA_REGENERATION
+}
+
+int32_t getArtifactBonusScore(const std::shared_ptr<Bonus> & bonus)
+{
+	if (bonus->propagator && bonus->propagator->getPropagatorType() == CBonusSystemNode::BATTLE)
+	{
+		if (bonus->limiter)
+		{
+			// assume that this is battle wide / other side propagator+limiter -> invert value
+			return -getArtifactBonusScoreImpl(bonus);
+		}
+		else
+		{
+			return 0; // TODO? How to consider battle-wide bonuses that affect everyone?
+		}
+	}
 	else
-		return art1->getPrice() > art2->getPrice();
+	{
+		return getArtifactBonusScoreImpl(bonus);
+	}
+
+}
+
+int64_t getPotentialArtifactScore(const CArtifact * type)
+{
+	int64_t totalScore = 0;
+
+	for (const auto & bonus : type->getExportedBonusList())
+		totalScore += getArtifactBonusScore(bonus);
+
+	if (type->hasParts())
+	{
+		for (const auto & part : type->getConstituents())
+		{
+			for (const auto & bonus : part->getExportedBonusList())
+				totalScore += getArtifactBonusScore(bonus);
+		}
+	}
+
+	int64_t finalScore = std::max<int64_t>(type->getPrice() / 5, totalScore );
+
+	return finalScore;
+}
+
+int64_t getArtifactScoreForHero(const CGHeroInstance * hero, const CArtifactInstance * artifact)
+{
+	if (artifact->isScroll())
+	{
+		auto spellID = artifact->getScrollSpellID();
+		auto spell = spellID.toEntity(VLC);
+
+		if (hero->getSpellsInSpellbook().count(spellID))
+			return 0;
+		else
+			return spell->getLevel() * 100;
+	}
+
+	const CArtifact * type = artifact->getType();
+	int64_t totalScore = 0;
+
+	if (type->getId() == ArtifactID::SPELLBOOK)
+		return 0;
+
+	for (const auto & bonus : type->getExportedBonusList())
+		totalScore += getArtifactBonusRelevance(hero, bonus) * getArtifactBonusScore(bonus);
+
+	if (type->hasParts())
+	{
+		for (const auto & part : type->getConstituents())
+		{
+			for (const auto & bonus : part->getExportedBonusList())
+				totalScore += getArtifactBonusRelevance(hero, bonus) * getArtifactBonusScore(bonus);
+		}
+	}
+
+	return totalScore;
 }
 
 bool isWeeklyRevisitable(const Nullkiller * ai, const CGObjectInstance * obj)

+ 2 - 1
AI/Nullkiller/AIUtility.h

@@ -213,7 +213,8 @@ bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet *, uint64_t dang
 
 bool compareHeroStrength(const CGHeroInstance * h1, const CGHeroInstance * h2);
 bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2);
-bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2);
+int64_t getArtifactScoreForHero(const CGHeroInstance * hero, const CArtifactInstance * artifact);
+int64_t getPotentialArtifactScore(const CArtifact * art);
 bool townHasFreeTavern(const CGTownInstance * town);
 
 uint64_t getHeroArmyStrengthWithCommander(const CGHeroInstance * hero, const CCreatureSet * heroArmy);

+ 18 - 6
AI/Nullkiller/Analyzers/BuildAnalyzer.cpp

@@ -20,7 +20,9 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
 {
 	for(int level = 0; level < developmentInfo.town->getTown()->creatures.size(); level++)
 	{
+#if NKAI_TRACE_LEVEL >= 1
 		logAi->trace("Checking dwelling level %d", level);
+#endif
 		std::vector<BuildingID> dwellingsInTown;
 
 		for(BuildingID buildID = BuildingID::getDwellingFromLevel(level, 0); buildID.hasValue(); BuildingID::advanceDwelling(buildID))
@@ -143,9 +145,9 @@ void BuildAnalyzer::update()
 	{
 		if(town->built >= cb->getSettings().getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP))
 			continue; // Not much point in trying anything - can't built in this town anymore today
-
+#if NKAI_TRACE_LEVEL >= 1
 		logAi->trace("Checking town %s", town->getNameTranslated());
-
+#endif
 		developmentInfos.push_back(TownDevelopmentInfo(town));
 		TownDevelopmentInfo & developmentInfo = developmentInfos.back();
 
@@ -161,10 +163,10 @@ void BuildAnalyzer::update()
 		}
 		armyCost += developmentInfo.armyCost;
 
+#if NKAI_TRACE_LEVEL >= 1
 		for(auto bi : developmentInfo.toBuild)
-		{
 			logAi->trace("Building preferences %s", bi.toString());
-		}
+#endif
 	}
 
 	std::sort(developmentInfos.begin(), developmentInfos.end(), [](const TownDevelopmentInfo & t1, const TownDevelopmentInfo & t2) -> bool
@@ -179,7 +181,9 @@ void BuildAnalyzer::update()
 
 	goldPressure = (ai->getLockedResources()[EGameResID::GOLD] + (float)armyCost[EGameResID::GOLD] + economyDevelopmentCost) / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
 
+#if NKAI_TRACE_LEVEL >= 1
 	logAi->trace("Gold pressure: %f", goldPressure);
+#endif
 }
 
 void BuildAnalyzer::reset()
@@ -268,12 +272,15 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
 
 			if(vstd::contains_if(missingBuildings, otherDwelling))
 			{
+#if NKAI_TRACE_LEVEL >= 1
 				logAi->trace("cant build %d. Need other dwelling %d", toBuild.getNum(), missingBuildings.front().getNum());
+#endif
 			}
 			else if(missingBuildings[0] != toBuild)
 			{
+#if NKAI_TRACE_LEVEL >= 1
 				logAi->trace("cant build %d. Need %d", toBuild.getNum(), missingBuildings[0].num);
-
+#endif
 				BuildingInfo prerequisite = getBuildingOrPrerequisite(town, missingBuildings[0], excludeDwellingDependencies);
 
 				prerequisite.buildCostWithPrerequisites += info.buildCost;
@@ -298,19 +305,24 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
 			}
 			else
 			{
+#if NKAI_TRACE_LEVEL >= 1
 				logAi->trace("Cant build. The building requires itself as prerequisite");
-
+#endif
 				return info;
 			}
 		}
 		else
 		{
+#if NKAI_TRACE_LEVEL >= 1
 			logAi->trace("Cant build. Reason: %d", static_cast<int>(canBuild));
+#endif
 		}
 	}
 	else
 	{
+#if NKAI_TRACE_LEVEL >= 1
 		logAi->trace("Dwelling %d exists", toBuild.getNum());
+#endif
 		info.exists = true;
 	}
 

+ 10 - 2
AI/Nullkiller/Behaviors/DefenceBehavior.cpp

@@ -151,7 +151,9 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
 
 void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town, const Nullkiller * ai) const
 {
+#if NKAI_TRACE_LEVEL >= 1
 	logAi->trace("Evaluating defence for %s", town->getNameTranslated());
+#endif
 
 	auto threatNode = ai->dangerHitMap->getObjectThreat(town);
 	std::vector<HitMapInfo> threats = ai->dangerHitMap->getTownThreats(town);
@@ -164,8 +166,9 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 	}
 	if(!threatNode.fastestDanger.hero)
 	{
+#if NKAI_TRACE_LEVEL >= 1
 		logAi->trace("No threat found for town %s", town->getNameTranslated());
-
+#endif
 		return;
 	}
 	
@@ -173,7 +176,9 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 
 	if(reinforcement)
 	{
+#if NKAI_TRACE_LEVEL >= 1
 		logAi->trace("Town %s can buy defence army %lld", town->getNameTranslated(), reinforcement);
+#endif
 		tasks.push_back(Goals::sptr(Goals::BuyArmy(town, reinforcement).setpriority(0.5f)));
 	}
 
@@ -181,13 +186,14 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 
 	for(auto & threat : threats)
 	{
+#if NKAI_TRACE_LEVEL >= 1
 		logAi->trace(
 			"Town %s has threat %lld in %s turns, hero: %s",
 			town->getNameTranslated(),
 			threat.danger,
 			std::to_string(threat.turn),
 			threat.hero ? threat.hero->getNameTranslated() : std::string("<no hero>"));
-
+#endif
 		handleCounterAttack(town, threat, threatNode.maximumDanger, ai, tasks);
 
 		if(isThreatUnderControl(town, threat, ai, paths))
@@ -199,7 +205,9 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 
 		if(paths.empty())
 		{
+#if NKAI_TRACE_LEVEL >= 1
 			logAi->trace("No ways to defend town %s", town->getNameTranslated());
+#endif
 
 			continue;
 		}

+ 1 - 1
AI/Nullkiller/Behaviors/StartupBehavior.cpp

@@ -15,7 +15,7 @@
 #include "../Goals/RecruitHero.h"
 #include "../Goals/ExecuteHeroChain.h"
 #include "../Goals/ExchangeSwapTownHeroes.h"
-#include "lib/mapObjects/MapObjects.h" //for victory conditions
+#include "../../../lib/mapObjects/CGResource.h"
 #include "../Engine/Nullkiller.h"
 
 namespace NKAI

+ 1 - 1
AI/Nullkiller/CMakeLists.txt

@@ -157,7 +157,7 @@ else()
 endif()
 
 target_include_directories(Nullkiller PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
-target_link_libraries(Nullkiller PUBLIC vcmi fuzzylite::fuzzylite)
+target_link_libraries(Nullkiller PUBLIC vcmi fuzzylite::fuzzylite TBB::tbb)
 
 vcmi_set_output_dir(Nullkiller "AI")
 enable_pch(Nullkiller)

+ 25 - 0
AI/Nullkiller/Engine/Nullkiller.cpp

@@ -24,6 +24,8 @@
 #include "../Goals/Composition.h"
 #include "../../../lib/CPlayerState.h"
 #include "../../lib/StartInfo.h"
+#include "../../lib/pathfinder/PathfinderCache.h"
+#include "../../lib/pathfinder/PathfinderOptions.h"
 
 namespace NKAI
 {
@@ -43,6 +45,8 @@ Nullkiller::Nullkiller()
 
 }
 
+Nullkiller::~Nullkiller() = default;
+
 bool canUseOpenMap(std::shared_ptr<CCallback> cb, PlayerColor playerID)
 {
 	if(!cb->getStartInfo()->extraOptionsInfo.cheatsAllowed)
@@ -73,6 +77,14 @@ void Nullkiller::init(std::shared_ptr<CCallback> cb, AIGateway * gateway)
 
 	settings = std::make_unique<Settings>(cb->getStartInfo()->difficulty);
 
+	PathfinderOptions pathfinderOptions(cb.get());
+
+	pathfinderOptions.useTeleportTwoWay = true;
+	pathfinderOptions.useTeleportOneWay = settings->isOneWayMonolithUsageAllowed();
+	pathfinderOptions.useTeleportOneWayRandom = settings->isOneWayMonolithUsageAllowed();
+
+	pathfinderCache = std::make_unique<PathfinderCache>(cb.get(), pathfinderOptions);
+
 	if(canUseOpenMap(cb, playerID))
 	{
 		useObjectGraph = settings->isObjectGraphAllowed();
@@ -547,6 +559,9 @@ void Nullkiller::makeTurn()
 			return;
 		}
 
+		for (auto heroInfo : cb->getHeroesInfo())
+			gateway->pickBestArtifacts(heroInfo);
+
 		if(i == settings->getMaxPass())
 		{
 			logAi->warn("Maxpass exceeded. Terminating AI turn.");
@@ -718,4 +733,14 @@ bool Nullkiller::handleTrading()
 	return haveTraded;
 }
 
+std::shared_ptr<const CPathsInfo> Nullkiller::getPathsInfo(const CGHeroInstance * h) const
+{
+	return pathfinderCache->getPathsInfo(h);
+}
+
+void Nullkiller::invalidatePaths()
+{
+	pathfinderCache->invalidatePaths();
+}
+
 }

+ 11 - 0
AI/Nullkiller/Engine/Nullkiller.h

@@ -21,6 +21,12 @@
 #include "../Analyzers/ObjectClusterizer.h"
 #include "../Helpers/ArmyFormation.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+
+class PathfinderCache;
+
+VCMI_LIB_NAMESPACE_END
+
 namespace NKAI
 {
 
@@ -72,6 +78,7 @@ private:
 	int3 targetTile;
 	ObjectInstanceID targetObject;
 	std::map<const CGHeroInstance *, HeroLockedReason> lockedHeroes;
+	std::unique_ptr<PathfinderCache> pathfinderCache;
 	ScanDepth scanDepth;
 	TResources lockedResources;
 	bool useHeroChain;
@@ -101,6 +108,7 @@ public:
 	std::mutex aiStateMutex;
 
 	Nullkiller();
+	~Nullkiller();
 	void init(std::shared_ptr<CCallback> cb, AIGateway * gateway);
 	void makeTurn();
 	bool isActive(const CGHeroInstance * hero) const { return activeHero == hero; }
@@ -124,6 +132,9 @@ public:
 	bool handleTrading();
 	void invalidatePathfinderData();
 
+	std::shared_ptr<const CPathsInfo> getPathsInfo(const CGHeroInstance * h) const;
+	void invalidatePaths();
+
 private:
 	void resetAiState();
 	void updateAiState(int pass, bool fast = false);

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

@@ -14,7 +14,7 @@
 #include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h"
 #include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
 #include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
-#include "../../../lib/mapObjects/MapObjects.h"
+#include "../../../lib/mapObjects/CGResource.h"
 #include "../../../lib/mapping/CMapDefines.h"
 #include "../../../lib/RoadHandler.h"
 #include "../../../lib/CCreatureHandler.h"
@@ -250,36 +250,7 @@ static uint64_t evaluateArtifactArmyValue(const CArtifact * art)
 	if(art->getId() == ArtifactID::SPELL_SCROLL)
 		return 1500;
 
-	auto statsValue =
-		10 * art->valOfBonuses(BonusType::MOVEMENT, BonusCustomSubtype::heroMovementLand)
-		+ 1200 * art->valOfBonuses(BonusType::STACKS_SPEED)
-		+ 700 * art->valOfBonuses(BonusType::MORALE)
-		+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK))
-		+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE))
-		+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::KNOWLEDGE))
-		+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::SPELL_POWER))
-		+ 500 * art->valOfBonuses(BonusType::LUCK);
-
-	auto classValue = 0;
-
-	switch(art->aClass)
-	{
-	case CArtifact::EartClass::ART_TREASURE:
-		//FALL_THROUGH
-	case CArtifact::EartClass::ART_MINOR:
-		classValue = 1000;
-		break;
-
-	case CArtifact::EartClass::ART_MAJOR:
-		classValue = 3000;
-		break;
-	case CArtifact::EartClass::ART_RELIC:
-	case CArtifact::EartClass::ART_SPECIAL:
-		classValue = 8000;
-		break;
-	}
-
-	return statsValue > classValue ? statsValue : classValue;
+	return getPotentialArtifactScore(art);
 }
 
 uint64_t RewardEvaluator::getArmyReward(
@@ -537,7 +508,7 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target, cons
 	{
 		auto resource = dynamic_cast<const CGResource *>(target);
 		TResources res;
-		res[resource->resourceID()] = resource->amount;
+		res[resource->resourceID()] = resource->getAmount();
 		
 		return getResourceRequirementStrength(res);
 	}
@@ -1191,7 +1162,7 @@ public:
 		Goals::BuildThis & buildThis = dynamic_cast<Goals::BuildThis &>(*task);
 		auto & bi = buildThis.buildingInfo;
 		
-		evaluationContext.goldReward += 7 * bi.dailyIncome[EGameResID::GOLD] / 2; // 7 day income but half we already have
+		evaluationContext.goldReward += 7 * bi.dailyIncome.marketValue() / 2; // 7 day income but half we already have
 		evaluationContext.heroRole = HeroRole::MAIN;
 		evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount;
 		int32_t cost = bi.buildCost[EGameResID::GOLD];
@@ -1201,7 +1172,9 @@ public:
 		if (bi.id == BuildingID::MARKETPLACE || bi.dailyIncome[EGameResID::WOOD] > 0)
 			evaluationContext.isTradeBuilding = true;
 
+#if NKAI_TRACE_LEVEL >= 1
 		logAi->trace("Building costs for %s : %s MarketValue: %d",bi.toString(), evaluationContext.buildingCost.toString(), evaluationContext.buildingCost.marketValue());
+#endif
 
 		if(bi.creatureID != CreatureID::NONE)
 		{

+ 2 - 0
AI/Nullkiller/Engine/Settings.cpp

@@ -38,6 +38,7 @@ namespace NKAI
 		pathfinderBucketsCount(1),
 		pathfinderBucketSize(32),
 		allowObjectGraph(true),
+		useOneWayMonoliths(false),
 		useTroopsFromGarrisons(false),
 		updateHitmapOnTileReveal(false),
 		openMap(true),
@@ -64,5 +65,6 @@ namespace NKAI
 		openMap = node["openMap"].Bool();
 		useFuzzy = node["useFuzzy"].Bool();
 		useTroopsFromGarrisons = node["useTroopsFromGarrisons"].Bool();
+		useOneWayMonoliths = node["useOneWayMonoliths"].Bool();
 	}
 }

+ 2 - 0
AI/Nullkiller/Engine/Settings.h

@@ -36,6 +36,7 @@ namespace NKAI
 		float maxArmyLossTarget;
 		bool allowObjectGraph;
 		bool useTroopsFromGarrisons;
+		bool useOneWayMonoliths;
 		bool updateHitmapOnTileReveal;
 		bool openMap;
 		bool useFuzzy;
@@ -58,6 +59,7 @@ namespace NKAI
 		int getPathfinderBucketSize() const { return pathfinderBucketSize; }
 		bool isObjectGraphAllowed() const { return allowObjectGraph; }
 		bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; }
+		bool isOneWayMonolithUsageAllowed() const { return useOneWayMonoliths; }
 		bool isUpdateHitmapOnTileReveal() const { return updateHitmapOnTileReveal; }
 		bool isOpenMap() const { return openMap; }
 		bool isUseFuzzy() const { return useFuzzy; }

+ 3 - 3
AI/Nullkiller/Goals/ExecuteHeroChain.cpp

@@ -166,7 +166,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 						if(nextNode.specialAction || nextNode.chainMask != chainMask)
 							break;
 
-						auto targetNode = cb->getPathsInfo(hero)->getPathInfo(nextNode.coord);
+						auto targetNode = ai->nullkiller->getPathsInfo(hero)->getPathInfo(nextNode.coord);
 
 						if(!targetNode->reachable()
 							|| targetNode->getCost() > nextNode.cost)
@@ -182,7 +182,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 
 				if(node->turns == 0 && node->coord != hero->visitablePos())
 				{
-					auto targetNode = cb->getPathsInfo(hero)->getPathInfo(node->coord);
+					auto targetNode = ai->nullkiller->getPathsInfo(hero)->getPathInfo(node->coord);
 
 					if(targetNode->accessible == EPathAccessibility::NOT_SET
 						|| targetNode->accessible == EPathAccessibility::BLOCKED
@@ -239,7 +239,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 						if(hero->movementPointsRemaining() > 0)
 						{
 							CGPath path;
-							bool isOk = cb->getPathsInfo(hero)->getPath(path, node->coord);
+							bool isOk = ai->nullkiller->getPathsInfo(hero)->getPath(path, node->coord);
 
 							if(isOk && path.nodes.back().turns > 0)
 							{

+ 1 - 1
AI/Nullkiller/Goals/ExploreNeighbourTile.cpp

@@ -35,7 +35,7 @@ void ExploreNeighbourTile::accept(AIGateway * ai)
 		int3 target = int3(-1);
 		foreach_neighbour(pos, [&](int3 tile)
 			{
-				auto pathInfo = ai->myCb->getPathsInfo(hero)->getPathInfo(tile);
+				auto pathInfo = ai->nullkiller->getPathsInfo(hero)->getPathInfo(tile);
 
 				if(pathInfo->turns > 0)
 					return;

+ 1 - 1
AI/Nullkiller/Helpers/ExplorationHelper.cpp

@@ -223,7 +223,7 @@ bool ExplorationHelper::hasReachableNeighbor(const int3 & pos) const
 		if(cbp->isInTheMap(tile))
 		{
 			auto isAccessible = useCPathfinderAccessibility
-				? ai->cb->getPathsInfo(hero)->getPathInfo(tile)->reachable()
+				? ai->getPathsInfo(hero)->getPathInfo(tile)->reachable()
 				: ai->pathfinder->isTileAccessible(hero, tile);
 
 			if(isAccessible)

+ 2 - 0
AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp

@@ -50,6 +50,8 @@ namespace AIPathfinding
 		options.allowLayerTransitioningAfterBattle = true;
 		options.useTeleportWhirlpool = true;
 		options.forceUseTeleportWhirlpool = true;
+		options.useTeleportOneWay = ai->settings->isOneWayMonolithUsageAllowed();;
+		options.useTeleportOneWayRandom = ai->settings->isOneWayMonolithUsageAllowed();;
 	}
 
 	AIPathfinderConfig::~AIPathfinderConfig() = default;

+ 19 - 18
AI/StupidAI/StupidAI.cpp

@@ -69,7 +69,7 @@ public:
 	const CStack * s;
 	int adi;
 	int adr;
-	std::vector<BattleHex> attackFrom; //for melee fight
+	BattleHexArray attackFrom; //for melee fight
 	EnemyInfo(const CStack * _s) : s(_s), adi(0), adr(0)
 	{}
 	void calcDmg(std::shared_ptr<CBattleCallback> cb, const BattleID & battleID, const CStack * ourStack)
@@ -107,7 +107,8 @@ static bool willSecondHexBlockMoreEnemyShooters(std::shared_ptr<CBattleCallback>
 
 	for(int i = 0; i < 2; i++)
 	{
-		for (auto & neighbour : (i ? h2 : h1).neighbouringTiles())
+		BattleHex hex = i ? h2 : h1;
+		for (auto neighbour : hex.getNeighbouringTiles())
 			if(const auto * s = cb->getBattle(battleID)->battleGetUnitByPos(neighbour))
 				if(s->isShooter())
 					shooters[i]++;
@@ -157,9 +158,9 @@ void CStupidAI::activeStack(const BattleID & battleID, const CStack * stack)
 		}
 		else
 		{
-			std::vector<BattleHex> avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(stack, false);
+			BattleHexArray avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(stack, false);
 
-			for (BattleHex hex : avHexes)
+			for (const BattleHex & hex : avHexes)
 			{
 				if(CStack::isMeleeAttackPossible(stack, s, hex))
 				{
@@ -170,7 +171,7 @@ void CStupidAI::activeStack(const BattleID & battleID, const CStack * stack)
 						i = enemiesReachable.begin() + (enemiesReachable.size() - 1);
 					}
 
-					i->attackFrom.push_back(hex);
+					i->attackFrom.insert(hex);
 				}
 			}
 
@@ -247,7 +248,7 @@ 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)
+void CStupidAI::battleStackMoved(const BattleID & battleID, const CStack * stack, const BattleHexArray & dest, int distance, bool teleport)
 {
 	print("battleStackMoved called");
 }
@@ -278,7 +279,7 @@ 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
+BattleAction CStupidAI::goTowards(const BattleID & battleID, const CStack * stack, BattleHexArray hexes) const
 {
 	auto reachability = cb->getBattle(battleID)->getReachability(stack);
 	auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false);
@@ -288,14 +289,14 @@ BattleAction CStupidAI::goTowards(const BattleID & battleID, const CStack * stac
 		return BattleAction::makeDefend(stack);
 	}
 
-	std::sort(hexes.begin(), hexes.end(), [&](BattleHex h1, BattleHex h2) -> bool
+	hexes.sort([&](const BattleHex & h1, const BattleHex & h2) -> bool
 	{
-		return reachability.distances[h1] < reachability.distances[h2];
+		return reachability.distances[h1.toInt()] < reachability.distances[h2.toInt()];
 	});
 
-	for(auto hex : hexes)
+	for(const auto & hex : hexes)
 	{
-		if(vstd::contains(avHexes, hex))
+		if(avHexes.contains(hex))
 		{
 			if(stack->position == hex)
 				return BattleAction::makeDefend(stack);
@@ -310,9 +311,9 @@ BattleAction CStupidAI::goTowards(const BattleID & battleID, const CStack * stac
 		}
 	}
 
-	BattleHex bestNeighbor = hexes.front();
+	BattleHex bestneighbour = hexes.front();
 
-	if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE)
+	if(reachability.distances[bestneighbour.toInt()] > GameConstants::BFIELD_SIZE)
 	{
 		return BattleAction::makeDefend(stack);
 	}
@@ -321,16 +322,16 @@ BattleAction CStupidAI::goTowards(const BattleID & battleID, const CStack * stac
 	{
 		// 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
+		auto nearestAvailableHex = vstd::minElementByFun(avHexes, [&](const BattleHex & hex) -> int
 		{
-			return BattleHex::getDistance(bestNeighbor, hex);
+			return BattleHex::getDistance(bestneighbour, hex);
 		});
 
 		return BattleAction::makeMove(stack, *nearestAvailableHex);
 	}
 	else
 	{
-		BattleHex currentDest = bestNeighbor;
+		BattleHex currentDest = bestneighbour;
 		while(1)
 		{
 			if(!currentDest.isValid())
@@ -339,14 +340,14 @@ BattleAction CStupidAI::goTowards(const BattleID & battleID, const CStack * stac
 				return BattleAction::makeDefend(stack);
 			}
 
-			if(vstd::contains(avHexes, currentDest))
+			if(avHexes.contains(currentDest))
 			{
 				if(stack->position == currentDest)
 					return BattleAction::makeDefend(stack);
 				return BattleAction::makeMove(stack, currentDest);
 			}
 
-			currentDest = reachability.predecessors[currentDest];
+			currentDest = reachability.predecessors[currentDest.toInt()];
 		}
 	}
 }

+ 2 - 2
AI/StupidAI/StupidAI.h

@@ -43,7 +43,7 @@ public:
 	//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 battleStackMoved(const BattleID & battleID, const CStack * stack, const BattleHexArray & 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;
@@ -51,6 +51,6 @@ public:
 	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;
+	BattleAction goTowards(const BattleID & battleID, const CStack * stack, BattleHexArray hexes) const;
 };
 

+ 2 - 2
AI/VCAI/AIUtility.cpp

@@ -133,8 +133,8 @@ bool HeroPtr::operator==(const HeroPtr & rhs) const
 
 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());
+	const CGPathNode * ln = ai->getPathsInfo(hero)->getPathInfo(lhs->visitablePos());
+	const CGPathNode * rn = ai->getPathsInfo(hero)->getPathInfo(rhs->visitablePos());
 
 	return ln->getCost() < rn->getCost();
 }

+ 1 - 1
AI/VCAI/FuzzyEngines.cpp

@@ -96,7 +96,7 @@ float HeroMovementGoalEngineBase::calculateTurnDistanceInputValue(const Goals::A
 	}
 	else
 	{
-		auto pathInfo = ai->myCb->getPathsInfo(goal.hero.h)->getPathInfo(goal.tile);
+		auto pathInfo = ai->getPathsInfo(goal.hero.h)->getPathInfo(goal.tile);
 		return pathInfo->getCost();
 	}
 }

+ 1 - 0
AI/VCAI/Goals/CollectRes.cpp

@@ -16,6 +16,7 @@
 #include "../ResourceManager.h"
 #include "../BuildingManager.h"
 #include "../../../lib/mapObjects/CGMarket.h"
+#include "../../../lib/mapObjects/CGResource.h"
 #include "../../../lib/constants/StringConstants.h"
 
 using namespace Goals;

+ 15 - 2
AI/VCAI/VCAI.cpp

@@ -31,6 +31,8 @@
 #include "../../lib/networkPacks/PacksForClientBattle.h"
 #include "../../lib/networkPacks/PacksForServer.h"
 #include "../../lib/serializer/CTypeList.h"
+#include "../../lib/pathfinder/PathfinderCache.h"
+#include "../../lib/pathfinder/PathfinderOptions.h"
 
 #include "AIhelper.h"
 
@@ -621,6 +623,7 @@ void VCAI::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<C
 	playerID = *myCb->getPlayerID();
 	myCb->waitTillRealize = true;
 	myCb->unlockGsWhenWaiting = true;
+	pathfinderCache = std::make_unique<PathfinderCache>(myCb.get(), PathfinderOptions(myCb.get()));
 
 	if(!fh)
 		fh = new FuzzyHelper();
@@ -628,6 +631,16 @@ void VCAI::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<C
 	retrieveVisitableObjs();
 }
 
+std::shared_ptr<const CPathsInfo> VCAI::getPathsInfo(const CGHeroInstance * h) const
+{
+	return pathfinderCache->getPathsInfo(h);
+}
+
+void VCAI::invalidatePaths()
+{
+	pathfinderCache->invalidatePaths();
+}
+
 void VCAI::yourTurn(QueryID queryID)
 {
 	LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID);
@@ -1800,7 +1813,7 @@ bool VCAI::isAccessibleForHero(const int3 & pos, HeroPtr h, bool includeAllies)
 			}
 		}
 	}
-	return cb->getPathsInfo(h.get())->getPathInfo(pos)->reachable();
+	return getPathsInfo(h.get())->getPathInfo(pos)->reachable();
 }
 
 bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
@@ -1837,7 +1850,7 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
 	else
 	{
 		CGPath path;
-		cb->getPathsInfo(h.get())->getPath(path, dst);
+		getPathsInfo(h.get())->getPath(path, dst);
 		if(path.nodes.empty())
 		{
 			logAi->error("Hero %s cannot reach %s.", h->getNameTranslated(), dst.toString());

+ 4 - 0
AI/VCAI/VCAI.h

@@ -26,6 +26,7 @@
 VCMI_LIB_NAMESPACE_BEGIN
 
 struct QuestInfo;
+class PathfinderCache;
 
 VCMI_LIB_NAMESPACE_END
 
@@ -80,6 +81,7 @@ public:
 	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;
+	std::unique_ptr<PathfinderCache> pathfinderCache;
 
 	//part of mainLoop, but accessible from outside
 	std::vector<Goals::TSubgoal> basicGoals;
@@ -254,6 +256,8 @@ public:
 	std::vector<HeroPtr> getMyHeroes() const;
 	HeroPtr primaryHero() const;
 	void checkHeroArmy(HeroPtr h);
+	std::shared_ptr<const CPathsInfo> getPathsInfo(const CGHeroInstance * h) const;
+	void invalidatePaths() override;
 
 	void requestSent(const CPackForServer * pack, int requestID) override;
 	void answerQuery(QueryID queryID, int selection);

+ 0 - 5
CCallback.cpp

@@ -384,11 +384,6 @@ bool CCallback::canMoveBetween(const int3 &a, const int3 &b)
 	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();

+ 0 - 1
CCallback.h

@@ -157,7 +157,6 @@ public:
 	//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;
 

+ 15 - 2
CMakeLists.txt

@@ -25,6 +25,13 @@ if(APPLE)
 	endif()
 endif()
 
+if(POLICY CMP0142)
+  cmake_policy(SET CMP0142 NEW)
+endif()
+if(POLICY CMP0177)
+  cmake_policy(SET CMP0177 NEW)
+endif()
+
 ############################################
 #        User-provided options             #
 ############################################
@@ -41,6 +48,7 @@ endif()
 option(ENABLE_CLIENT "Enable compilation of game client" ON)
 option(ENABLE_ERM "Enable compilation of ERM scripting module" OFF)
 option(ENABLE_LUA "Enable compilation of LUA scripting module" OFF)
+option(ENABLE_VIDEO "Enable video support using ffmpeg" ON)
 option(ENABLE_TRANSLATIONS "Enable generation of translations for launcher and editor" ON)
 option(ENABLE_NULLKILLER_AI "Enable compilation of Nullkiller AI library" ON)
 option(ENABLE_MINIMAL_LIB "Build only core parts of vcmi library that are required for game lobby" OFF)
@@ -472,8 +480,11 @@ if(NOT FORCE_BUNDLED_MINIZIP)
 	endif()
 endif()
 
+
 if (ENABLE_CLIENT)
-	find_package(ffmpeg COMPONENTS avutil swscale avformat avcodec swresample)
+	if (ENABLE_VIDEO)
+		find_package(ffmpeg REQUIRED COMPONENTS avutil swscale avformat avcodec swresample)
+	endif()
 
 	find_package(SDL2 REQUIRED)
 	find_package(SDL2_image REQUIRED)
@@ -870,7 +881,9 @@ elseif(APPLE_MACOS AND NOT ENABLE_MONOLITHIC_INSTALL)
 	# Workaround for this issue:
 	# CPack Error: Error executing: /usr/bin/hdiutil detach "/Volumes/VCMI"
 	# https://github.com/actions/runner-images/issues/7522#issuecomment-1564467252
-	set(CPACK_COMMAND_HDIUTIL "/usr/bin/sudo /usr/bin/hdiutil")
+	if(DEFINED ENV{GITHUB_RUN_ID})
+		set(CPACK_COMMAND_HDIUTIL "/usr/bin/sudo /usr/bin/hdiutil")
+	endif()
 	# CMake code for CPACK_DMG_DS_STORE executed before DS_STORE_SETUP_SCRIPT
 	# So we can keep both enabled and this shouldn't hurt
 	# set(CPACK_DMG_DS_STORE "${CMAKE_SOURCE_DIR}/osx/dmg_DS_Store")

+ 65 - 0
ChangeLog.md

@@ -1,5 +1,70 @@
 # VCMI Project Changelog
 
+## 1.6.3 -> 1.6.4
+
+### General
+
+* xbrz image upscaling is now performed in background threads to avoid freezes in UI
+* Fixed a bug which caused importing data to fail on some Android devices.
+* It is now possible to add or remove per-language channels in lobby.
+* Fixed bug causing broken water tile animation when player opens launcher while game is running
+* Fixed smooth map dragging not working with right click drag
+* Game will no longer play sound on new chat message in global chat unless lobby UI is currently open
+* Fixed new building sound playing twice on costructing some buildings, such as town hall
+
+### AI
+
+* Significantly improved Battle AI performance
+* Slightly improved performance of Nullkiller AI
+* Improved scoring of on-map artifacts by Nullkiller AI
+* Nullkiller AI will now select artefact loadouts based on the hero's army, skills, spells, and mana points.
+* Nullkiller AI will now consider building resource silos in towns.
+* Fixed possible bug which could lead to AI avoiding map locations with placed events
+
+### Stability
+
+* Fixed a crash that could occur when winning a game by capturing a city that was set as a victory condition without first killing all enemies.
+* Fixed a possible crash on some platforms when opening the creature window if the hero has equipped artefacts that provide spell immunity.
+* Fixed crash when renaming preset to same name as before
+* Fixed possible crash when opening mod screenshots tab without selected mod
+* Fixed possible crash when loading game with broken mods active
+* Fixed crash on loading some user-made maps with objects that have unknown to VCMI map object ID or subID
+* Fixed crash on loading map in Wake of Gods format with pre-placed Mithril resource pile on map
+
+### Mechanics
+
+* Fixed war machines or units under the Bow of the Sharpshooter effect being unable to fire when blocked by enemy units.
+* Enemy corpses will no longer block adjacent enemy ranged units from using ranged attacks.
+* Spells banned on the map can no longer appear in towns.
+* Arrow towers will now consider units standing on wall tiles as inside town for target selection
+* Fixed possible integer overflow when player has more than 20 million gold or other resources.
+* Fixed the loading of vcmp campaigns when a specific hero is used in the bonuses of a scenario.
+* Fixed regression causing movement bonus from Stables (adventure map objects) and Stables (Castle town building) to stack with each other.
+* Fixed regression causing Pathfinding skill to reduce movement costs by only 1 movement point
+* Fixed bug causing hero paths not updating immediately after leveling up Pathfinding skill
+
+### Interface
+
+* Fixed multiple cases where town buidings were not ordered correctly and overlapping each other on town screen
+* Object search functionality is now case-insensitive and can search for similar strings to protect from typos
+* Thieves Guild will now show icons instead of text for resources comparison
+* Added support for custom images in multiplayer mode selection
+* TCP host/join dialogue now displays correct text in header
+* Main menu buttons for unavailable campaigns automatically hidden
+* Fixed graphical artefact near 3DO video when resolution is high and interface scaling is low
+
+### Map Editor
+
+* Object properties now show actual values instead of '...'.
+* Hovering over object properties now shows tooltip with full value
+
+### Modding
+
+* It is now possible to configure amount of creatures that would join on successful diplomacy check
+* It is now possible to disable joining for free for diplomacy
+* It is now possible to use images with `-shadow` or `-overlay` suffixes for 1x / unscaled mode
+* It is now possible to load pregenerated player-colored interface images using suffixes like `-red` or `-blue` in place of palette-based effects
+
 ## 1.6.2 -> 1.6.3
 
 ### Stability

+ 4 - 4
Global.h

@@ -670,15 +670,15 @@ namespace vstd
 		return false;
 	}
 
-	template<typename T>
-	void removeDuplicates(std::vector<T> &vec)
+	template <typename Container>
+	void removeDuplicates(Container &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)
+	template <typename Container>
+	void concatenate(Container &dest, const Container &src)
 	{
 		dest.reserve(dest.size() + src.size());
 		dest.insert(dest.end(), src.begin(), src.end());

BIN
Mods/vcmi/Content/Sprites/lobby/addChannel.png


BIN
Mods/vcmi/Content/Sprites/lobby/closeChannel.png


BIN
Mods/vcmi/Content/Sprites2x/lobby/addChannel.png


BIN
Mods/vcmi/Content/Sprites2x/lobby/closeChannel.png


BIN
Mods/vcmi/Content/Sprites2x/lobby/iconPlayer.png


BIN
Mods/vcmi/Content/Sprites3x/lobby/addChannel.png


BIN
Mods/vcmi/Content/Sprites3x/lobby/closeChannel.png


BIN
Mods/vcmi/Content/Sprites3x/lobby/iconPlayer.png


BIN
Mods/vcmi/Content/Sprites4x/lobby/addChannel.png


BIN
Mods/vcmi/Content/Sprites4x/lobby/closeChannel.png


BIN
Mods/vcmi/Content/Sprites4x/lobby/iconPlayer.png


+ 1 - 0
Mods/vcmi/Content/config/czech.json

@@ -222,6 +222,7 @@
 
 	"vcmi.client.errors.invalidMap" : "{Neplatná mapa nebo kampaň}\n\nChyba při startu hry! Vybraná mapa nebo kampaň může být neplatná nebo poškozená. Důvod:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{Chybějící datové soubory}\n\nDatové soubory kampaně nebyly nalezeny! Možná máte nekompletní nebo poškozené datové soubory Heroes 3. Prosíme, přeinstalujte hru.",
+	"vcmi.client.errors.modLoadingFailure" : "{Chyba při načítání modifikací}\n\nPři načítání modifikací byly nalezeny kritické problémy! Hra nemusí fungovat správně nebo může spadnout! Aktualizujte nebo deaktivujte následující modifikace:\n\n",
 	"vcmi.server.errors.disconnected" : "{Chyba sítě}\n\nPřipojení k hernímu serveru bylo ztraceno!",
 	"vcmi.server.errors.playerLeft" : "{Hráč opustil hru}\n\nHráč %s se odpojil ze hry!", //%s -> player color
 	"vcmi.server.errors.existingProcess" : "Již běží jiný server VCMI. Prosím, ukončete ho před startem nové hry.",

+ 24 - 1
Mods/vcmi/Content/config/english.json

@@ -199,6 +199,7 @@
 	"vcmi.lobby.preview.error.invite" : "You were not invited to this room.",
 	"vcmi.lobby.preview.error.mods" : "You are using different set of mods.",
 	"vcmi.lobby.preview.error.version" : "You are using different version of VCMI.",
+	"vcmi.lobby.channel.add" : "Add Channel",
 	"vcmi.lobby.room.new" : "New Game",
 	"vcmi.lobby.room.load" : "Load Game",
 	"vcmi.lobby.room.type" : "Room Type",
@@ -783,5 +784,27 @@
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.water": "This unit is immune to all Water school spells",
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.earth": "This unit is immune to all Earth school spells",
 	"core.bonus.OPENING_BATTLE_SPELL.name": "Starts with spell",
-	"core.bonus.OPENING_BATTLE_SPELL.description": "Casts ${subtype.spell} on battle start"
+	"core.bonus.OPENING_BATTLE_SPELL.description": "Casts ${subtype.spell} on battle start",
+	
+	"spell.core.castleMoat.name" : "Moat",
+	"spell.core.castleMoatTrigger.name" : "Moat",
+	"spell.core.catapultShot.name" : "Catapult shot",
+	"spell.core.cyclopsShot.name" : "Siege shot",
+	"spell.core.dungeonMoat.name" : "Boiling Oil",
+	"spell.core.dungeonMoatTrigger.name" : "Boiling Oil",
+	"spell.core.fireWallTrigger.name" : "Fire Wall",
+	"spell.core.firstAid.name" : "First Aid",
+	"spell.core.fortressMoat.name" : "Boiling Tar",
+	"spell.core.fortressMoatTrigger.name" : "Boiling Tar",
+	"spell.core.infernoMoat.name" : "Lava",
+	"spell.core.infernoMoatTrigger.name" : "Lava",
+	"spell.core.landMineTrigger.name" : "Land Mine",
+	"spell.core.necropolisMoat.name" : "Boneyard",
+	"spell.core.necropolisMoatTrigger.name" : "Boneyard",
+	"spell.core.rampartMoat.name" : "Brambles",
+	"spell.core.rampartMoatTrigger.name" : "Brambles",
+	"spell.core.strongholdMoat.name" : "Wooden Spikes",
+	"spell.core.strongholdMoatTrigger.name" : "Wooden Spikes",
+	"spell.core.summonDemons.name" : "Summon Demons",
+	"spell.core.towerMoat.name" : "Land Mine"
 }

+ 2 - 0
Mods/vcmi/Content/config/german.json

@@ -199,6 +199,7 @@
 	"vcmi.lobby.preview.error.invite" : "Ihr wurdet nicht in diesen Raum eingeladen.",
 	"vcmi.lobby.preview.error.mods" : "Ihr verwendet andere Mods.",
 	"vcmi.lobby.preview.error.version" : "Ihr verwendet eine andere Version von VCMI.",
+	"vcmi.lobby.channel.add" : "Kanal hinzufügen",
 	"vcmi.lobby.room.new" : "Neues Spiel",
 	"vcmi.lobby.room.load" : "Spiel laden",
 	"vcmi.lobby.room.type" : "Raumtyp",
@@ -222,6 +223,7 @@
 
 	"vcmi.client.errors.invalidMap" : "{Ungültige Karte oder Kampagne}\n\nDas Spiel konnte nicht gestartet werden! Die ausgewählte Karte oder Kampagne ist möglicherweise ungültig oder beschädigt. Grund:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{Fehlende Dateien}\n\nEs wurden keine Kampagnendateien gefunden! Möglicherweise verwendest du unvollständige oder beschädigte Heroes 3 Datendateien. Bitte installiere die Spieldaten neu.",
+	"vcmi.client.errors.modLoadingFailure" : "{Mod Ladefehler}\n\nKritische Probleme beim Laden von Mods gefunden! Das Spiel könnte nicht korrekt funktionieren oder abstürzen! Bitte aktualisiere oder deaktiviere folgende Mods:\n\n",
 	"vcmi.server.errors.disconnected" : "{Netzwerkfehler}\n\nDie Verbindung zum Spielserver wurde unterbrochen!",
 	"vcmi.server.errors.playerLeft" : "{Verlassen eines Spielers}\n\n%s Spieler hat die Verbindung zum Spiel unterbrochen!", //%s -> player color
 	"vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst",

+ 810 - 0
Mods/vcmi/Content/config/hungarian.json

@@ -0,0 +1,810 @@
+{
+	"vcmi.adventureMap.monsterThreat.title"     : "\n\nFenyegetés: ",
+	"vcmi.adventureMap.monsterThreat.levels.0"  : "Gyerekjáték",
+	"vcmi.adventureMap.monsterThreat.levels.1"  : "Nagyon gyenge",
+	"vcmi.adventureMap.monsterThreat.levels.2"  : "Gyenge",
+	"vcmi.adventureMap.monsterThreat.levels.3"  : "Kissé gyengébb",
+	"vcmi.adventureMap.monsterThreat.levels.4"  : "Egyenlő",
+	"vcmi.adventureMap.monsterThreat.levels.5"  : "Kissé erősebb",
+	"vcmi.adventureMap.monsterThreat.levels.6"  : "Erős",
+	"vcmi.adventureMap.monsterThreat.levels.7"  : "Nagyon erős",
+	"vcmi.adventureMap.monsterThreat.levels.8"  : "Kihívást jelentő",
+	"vcmi.adventureMap.monsterThreat.levels.9"  : "Mindent elsöprő",
+	"vcmi.adventureMap.monsterThreat.levels.10" : "Halálos",
+	"vcmi.adventureMap.monsterThreat.levels.11" : "Lehetetlen",
+	"vcmi.adventureMap.monsterLevel"            : "\n\nSzint %LEVEL %TOWN %ATTACK_TYPE egység",
+	"vcmi.adventureMap.monsterMeleeType"        : "közelharci",
+	"vcmi.adventureMap.monsterRangedType"       : "távolsági",
+	"vcmi.adventureMap.search.hover"            : "Térképobjektum keresése",
+	"vcmi.adventureMap.search.help"             : "Válassza ki a keresni kívánt térképobjektumot.",
+
+	"vcmi.adventureMap.confirmRestartGame"               : "Biztosan újra akarja indítani a játékot?",
+	"vcmi.adventureMap.noTownWithMarket"                 : "Nincsenek elérhető piacok!",
+	"vcmi.adventureMap.noTownWithTavern"                 : "Nincsenek elérhető kocsmával rendelkező városok!",
+	"vcmi.adventureMap.spellUnknownProblem"              : "Ismeretlen probléma van ezzel a varázslattal! További információ nem érhető el.",
+	"vcmi.adventureMap.playerAttacked"                   : "A játékost megtámadták: %s",
+	"vcmi.adventureMap.moveCostDetails"                  : "Mozgáspontok - Költség: %TURNS kör + %POINTS pont, Hátralévő pontok: %REMAINING",
+	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Mozgáspontok - Költség: %POINTS pont, Hátralévő pontok: %REMAINING",
+	"vcmi.adventureMap.movementPointsHeroInfo" 			 : "(Mozgáspontok: %REMAINING / %POINTS)",
+	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Sajnáljuk, az ellenfél körének visszajátszása még nincs megvalósítva!",
+
+	"vcmi.bonusSource.artifact" : "Műtárgy",
+	"vcmi.bonusSource.creature" : "Képesség",
+	"vcmi.bonusSource.spell" : "Varázslat",
+	"vcmi.bonusSource.hero" : "Hős",
+	"vcmi.bonusSource.commander" : "Parancsnok",
+	"vcmi.bonusSource.other" : "Egyéb",
+
+	"vcmi.capitalColors.0" : "Piros",
+	"vcmi.capitalColors.1" : "Kék",
+	"vcmi.capitalColors.2" : "Barna",
+	"vcmi.capitalColors.3" : "Zöld",
+	"vcmi.capitalColors.4" : "Narancs",
+	"vcmi.capitalColors.5" : "Lila",
+	"vcmi.capitalColors.6" : "Türkiz",
+	"vcmi.capitalColors.7" : "Rózsaszín",
+	
+	"vcmi.heroOverview.startingArmy" : "Kezdő egységek",
+	"vcmi.heroOverview.warMachine" : "Hadigépek",
+	"vcmi.heroOverview.secondarySkills" : "Másodlagos képességek",
+	"vcmi.heroOverview.spells" : "Varázslatok",
+	
+	"vcmi.quickExchange.moveUnit" : "Egység áthelyezése",
+	"vcmi.quickExchange.moveAllUnits" : "Minden egység áthelyezése",
+	"vcmi.quickExchange.swapAllUnits" : "Hadseregek cseréje",
+	"vcmi.quickExchange.moveAllArtifacts" : "Minden műtárgy áthelyezése",
+	"vcmi.quickExchange.swapAllArtifacts" : "Műtárgyak cseréje",
+	
+	"vcmi.radialWheel.mergeSameUnit" : "Azonos lények összevonása",
+	"vcmi.radialWheel.fillSingleUnit" : "Egyes lényekkel való feltöltés",
+	"vcmi.radialWheel.splitSingleUnit" : "Egy lény leválasztása",
+	"vcmi.radialWheel.splitUnitEqually" : "Lények egyenlő felosztása",
+	"vcmi.radialWheel.moveUnit" : "Lények áthelyezése másik hadseregbe",
+	"vcmi.radialWheel.splitUnit" : "Lény felosztása másik helyre",
+	
+	"vcmi.radialWheel.heroGetArmy" : "Hadsereg átvétele másik hőstől",
+	"vcmi.radialWheel.heroSwapArmy" : "Hadsereg cseréje másik hőssel",
+	"vcmi.radialWheel.heroExchange" : "Hős cseréje megnyitása",
+	"vcmi.radialWheel.heroGetArtifacts" : "Műtárgyak átvétele másik hőstől",
+	"vcmi.radialWheel.heroSwapArtifacts" : "Műtárgyak cseréje másik hőssel",
+	"vcmi.radialWheel.heroDismiss" : "Hős elbocsátása",
+
+	"vcmi.radialWheel.moveTop" : "Legfelülre helyezés",
+	"vcmi.radialWheel.moveUp" : "Feljebb mozgatás",
+	"vcmi.radialWheel.moveDown" : "Lejjebb mozgatás",
+	"vcmi.radialWheel.moveBottom" : "Legalulra helyezés",
+	
+	"vcmi.randomMap.description" : "A térképet a Véletlen Térkép Generátor készítette.\nSablon: %s, Méret: %dx%d, Szintek: %d, Játékosok: %d, Számítógépek: %d, Víz: %s, Szörnyek: %s, VCMI térkép",
+	"vcmi.randomMap.description.isHuman" : ", %s emberi játékos",
+	"vcmi.randomMap.description.townChoice" : ", %s városválasztása: %s",
+	"vcmi.randomMap.description.water.none" : "nincs",
+	"vcmi.randomMap.description.water.normal" : "normál",
+	"vcmi.randomMap.description.water.islands" : "szigetek",
+	"vcmi.randomMap.description.monster.weak" : "gyenge",
+	"vcmi.randomMap.description.monster.normal" : "normál",
+	"vcmi.randomMap.description.monster.strong" : "erős",
+
+	"vcmi.spellBook.search" : "keresés...",
+
+	"vcmi.spellResearch.canNotAfford" : "Nem engedheti meg magának, hogy {%SPELL1}-et lecserélje {%SPELL2}-re. De még mindig elvetheti ezt a varázslatot, és folytathatja a varázskutatást.",
+	"vcmi.spellResearch.comeAgain" : "A mai napra a kutatás már elkészült. Jöjjön vissza holnap.",
+	"vcmi.spellResearch.pay" : "Szeretné lecserélni {%SPELL1}-et {%SPELL2}-re? Vagy elveti ezt a varázslatot, és folytatja a kutatást?",
+	"vcmi.spellResearch.research" : "Ezt a varázslatot kutatja",
+	"vcmi.spellResearch.skip" : "Ugorja át ezt a varázslatot",
+	"vcmi.spellResearch.abort" : "Megszakítás",
+	"vcmi.spellResearch.noMoreSpells" : "Nincs több elérhető varázslat kutatásra.",
+
+	"vcmi.mainMenu.serverConnecting" : "Kapcsolódás...",
+	"vcmi.mainMenu.serverAddressEnter" : "Írja be a címet:",
+	"vcmi.mainMenu.serverConnectionFailed" : "Kapcsolódás sikertelen",
+	"vcmi.mainMenu.serverClosing" : "Bezárás...",
+	"vcmi.mainMenu.hostTCP" : "Host TCP/IP játék",
+	"vcmi.mainMenu.joinTCP" : "Csatlakozás TCP/IP játékhoz",
+
+	"vcmi.lobby.filepath" : "Fájl elérési út",
+	"vcmi.lobby.creationDate" : "Létrehozás dátuma",
+	"vcmi.lobby.scenarioName" : "Forgatókönyv neve",
+	"vcmi.lobby.mapPreview" : "Térkép előnézete",
+	"vcmi.lobby.noPreview" : "nincs előnézet",
+	"vcmi.lobby.noUnderground" : "nincs földalatti szint",
+	"vcmi.lobby.sortDate" : "Térképek rendezése módosítás dátuma szerint",
+	"vcmi.lobby.backToLobby" : "Vissza a lobbyba",
+	"vcmi.lobby.author" : "Szerző",
+	"vcmi.lobby.handicap" : "Handicap",
+	"vcmi.lobby.handicap.resource" : "Az induló nyersanyagokat a játékos számára biztosítja az alapnyersanyagokon felül. Negatív értékek is megadhatók, de az összérték sosem lehet 0 alatt (a játékos sosem indulhat negatív nyersanyaggal).",
+	"vcmi.lobby.handicap.income" : "Módosítja a játékos különféle bevételeit százalékos arányban. Kerekítve van.",
+	"vcmi.lobby.handicap.growth" : "Módosítja a játékos tulajdonában lévő városokban a lények növekedési arányát. Kerekítve van.",
+	"vcmi.lobby.deleteUnsupportedSave" : "{Nem támogatott mentések találhatók}\n\nA VCMI %d mentést talált, amelyek már nem támogatottak, valószínűleg a VCMI verziók közötti különbségek miatt.\n\nTörölni szeretné őket?",
+	"vcmi.lobby.deleteSaveGameTitle" : "Válassza ki a törlendő mentést",
+	"vcmi.lobby.deleteMapTitle" : "Válassza ki a törlendő forgatókönyvet",
+	"vcmi.lobby.deleteFile" : "Törölni kívánja a következő fájlt?",
+	"vcmi.lobby.deleteFolder" : "Törölni kívánja a következő mappát?",
+	"vcmi.lobby.deleteMode" : "Váltás törlés módba és vissza",
+
+	"vcmi.broadcast.failedLoadGame" : "A játék betöltése sikertelen",
+	"vcmi.broadcast.command" : "Használja a '!help' parancsot az elérhető parancsok listázásához",
+	"vcmi.broadcast.simturn.end" : "Az egyidejű körök véget értek",
+	"vcmi.broadcast.simturn.endBetween" : "Az egyidejű körök véget értek a következő játékosok között: %s és %s",
+	"vcmi.broadcast.serverProblem" : "A szerver problémába ütközött",
+	"vcmi.broadcast.gameTerminated" : "a játék megszakadt",
+	"vcmi.broadcast.gameSavedAs" : "a játék mentve: ",
+	"vcmi.broadcast.noCheater" : "Nincs regisztrált csaló!",
+	"vcmi.broadcast.playerCheater" : "%s játékos csalónak bizonyult!",
+	"vcmi.broadcast.statisticFile" : "A statisztikai fájlok megtalálhatók a következő könyvtárban: %s",
+	"vcmi.broadcast.help.commands" : "A gazdagép számára elérhető parancsok:",
+	"vcmi.broadcast.help.exit" : "'!exit' - azonnal befejezi a jelenlegi játékot",
+	"vcmi.broadcast.help.kick" : "'!kick <játékos>' - a megadott játékos eltávolítása a játékból",
+	"vcmi.broadcast.help.save" : "'!save <fájlnév>' - a játék mentése a megadott név alatt",
+	"vcmi.broadcast.help.statistic" : "'!statistic' - a játék statisztikáinak mentése CSV fájlba",
+	"vcmi.broadcast.help.commandsAll" : "Minden játékos számára elérhető parancsok:",
+	"vcmi.broadcast.help.help" : "'!help' - a súgó megjelenítése",
+	"vcmi.broadcast.help.cheaters" : "'!cheaters' - a játék során csalási parancsot használó játékosok listája",
+	"vcmi.broadcast.help.vote" : "'!vote' - lehetővé teszi néhány játékkörülmény megváltoztatását, ha minden játékos megszavazza",
+	"vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - engedélyezett egyidejű körök meghatározott napokig vagy amíg nem lépnek kapcsolatba",
+	"vcmi.broadcast.vote.force" : "'!vote simturns force X' - az egyidejű körök kényszerítése meghatározott napokig, a játékosok közötti kapcsolatok blokkolásával",
+	"vcmi.broadcast.vote.abort" : "'!vote simturns abort' - az egyidejű körök megszakítása a következő fordulótól kezdve",
+	"vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - az összes játékos alapidőzítőjének meghosszabbítása a megadott másodpercek számával",
+	"vcmi.broadcast.vote.noActive" : "Nincs aktív szavazás!",
+	"vcmi.broadcast.vote.yes" : "igen",
+	"vcmi.broadcast.vote.no" : "nem",
+	"vcmi.broadcast.vote.notRecognized" : "A szavazási parancs nem ismerhető fel!",
+	"vcmi.broadcast.vote.success.untilContacts" : "A szavazás sikeres. Az egyidejű körök még %s napig fognak tartani, vagy amíg kapcsolatot nem létesítenek.",
+	"vcmi.broadcast.vote.success.contactsBlocked" : "A szavazás sikeres. Az egyidejű körök még %s napig fognak tartani. A kapcsolatok blokkolva vannak.",
+	"vcmi.broadcast.vote.success.nextDay" : "A szavazás sikeres. Az egyidejű körök a következő napon véget érnek.",
+	"vcmi.broadcast.vote.success.timer" : "A szavazás sikeres. Az időzítő minden játékos számára meghosszabbodott %s másodperccel.",
+	"vcmi.broadcast.vote.aborted" : "Egy játékos ellenszavazott. A szavazás megszakadt.",
+	"vcmi.broadcast.vote.start.untilContacts" : "Szavazás indult az egyidejű körök engedélyezéséről még %s napig",
+	"vcmi.broadcast.vote.start.contactsBlocked" : "Szavazás indult az egyidejű körök kényszerítéséről még %s napig",
+	"vcmi.broadcast.vote.start.nextDay" : "Szavazás indult az egyidejű körök befejezéséről a következő naptól kezdve",
+	"vcmi.broadcast.vote.start.timer" : "Szavazás indult az összes játékos időzítőjének meghosszabbításáról %s másodperccel",
+	"vcmi.broadcast.vote.hint" : "Írja be a '!vote yes' parancsot a változtatás elfogadásához, vagy a '!vote no' parancsot az elutasításhoz",
+		
+	"vcmi.lobby.login.title" : "VCMI Online Lobby",
+	"vcmi.lobby.login.username" : "Felhasználónév:",
+	"vcmi.lobby.login.connecting" : "Kapcsolódás...",
+	"vcmi.lobby.login.error" : "Kapcsolódási hiba: %s",
+	"vcmi.lobby.login.create" : "Új fiók",
+	"vcmi.lobby.login.login" : "Bejelentkezés",
+	"vcmi.lobby.login.as" : "Bejelentkezés mint %s",
+	"vcmi.lobby.login.spectator" : "Néző",
+	"vcmi.lobby.header.rooms" : "Játékszobák - %d",
+	"vcmi.lobby.header.channels" : "Csevegő csatornák",
+	"vcmi.lobby.header.chat.global" : "Globális játékon belüli csevegés - %s", // %s -> language name
+	"vcmi.lobby.header.chat.match" : "Csevegés az előző játékból: %s", // %s -> game start date & time
+	"vcmi.lobby.header.chat.player" : "Privát csevegés %s-szel", // %s -> nickname of another player
+	"vcmi.lobby.header.history" : "Korábbi játékaid",
+	"vcmi.lobby.header.players" : "Online játékosok - %d",
+	"vcmi.lobby.match.solo" : "Egyszemélyes játék",
+	"vcmi.lobby.match.duel" : "Játék %s-szel", // %s -> nickname of another player
+	"vcmi.lobby.match.multi" : "%d játékos",
+	"vcmi.lobby.room.create" : "Új szoba létrehozása",
+	"vcmi.lobby.room.players.limit" : "Játékosok száma",
+	"vcmi.lobby.room.description.public" : "Bárki csatlakozhat a nyilvános szobához.",
+	"vcmi.lobby.room.description.private" : "Csak meghívott játékosok csatlakozhatnak a privát szobához.",
+	"vcmi.lobby.room.description.new" : "A játék indításához válasszon egy forgatókönyvet, vagy állítson be egy véletlenszerű térképet.",
+	"vcmi.lobby.room.description.load" : "A játék indításához használja az egyik mentett játékot.",
+	"vcmi.lobby.room.description.limit" : "A szobájába legfeljebb %d játékos léphet be, beleértve Önt is.",
+	"vcmi.lobby.invite.header" : "Játékosok meghívása",
+	"vcmi.lobby.invite.notification" : "Egy játékos meghívta Önt az ő játékszobájába. Most csatlakozhat a privát szobához.",
+	"vcmi.lobby.preview.title" : "Csatlakozás a játékszobához",
+	"vcmi.lobby.preview.subtitle" : "Játék %s-en, gazdagép: %s", //TL Note: 1) name of map or RMG template 2) nickname of game host
+	"vcmi.lobby.preview.version" : "Játék verzió:",
+	"vcmi.lobby.preview.players" : "Játékosok:",
+	"vcmi.lobby.preview.mods" : "Használt modok:",
+	"vcmi.lobby.preview.allowed" : "Csatlakozik a játékszobához?",
+	"vcmi.lobby.preview.error.header" : "Nem sikerült csatlakozni ehhez a szobához.",
+	"vcmi.lobby.preview.error.playing" : "Előbb ki kell lépnie a jelenlegi játékából.",
+	"vcmi.lobby.preview.error.full" : "A szoba már tele van.",
+	"vcmi.lobby.preview.error.busy" : "A szoba már nem fogad új játékosokat.",
+	"vcmi.lobby.preview.error.invite" : "Nem kapott meghívót ebbe a szobába.",
+	"vcmi.lobby.preview.error.mods" : "Más modokat használ.",
+	"vcmi.lobby.preview.error.version" : "Másik VCMI verziót használ.",
+	"vcmi.lobby.room.new" : "Új játék",
+	"vcmi.lobby.room.load" : "Játék betöltése",
+	"vcmi.lobby.room.type" : "Szoba típusa",
+	"vcmi.lobby.room.mode" : "Játék módja",
+	"vcmi.lobby.room.state.public" : "Nyilvános",
+	"vcmi.lobby.room.state.private" : "Privát",
+	"vcmi.lobby.room.state.busy" : "Játékban",
+	"vcmi.lobby.room.state.invited" : "Meghívott",
+	"vcmi.lobby.mod.state.compatible" : "Kompatibilis",
+	"vcmi.lobby.mod.state.disabled" : "Engedélyezni kell",
+	"vcmi.lobby.mod.state.version" : "Verzióeltérés",
+	"vcmi.lobby.mod.state.excessive" : "Letiltani kell",
+	"vcmi.lobby.mod.state.missing" : "Nincs telepítve",
+	"vcmi.lobby.pvp.coin.hover" : "Érme",
+	"vcmi.lobby.pvp.coin.help" : "Pénzfeldobás",
+	"vcmi.lobby.pvp.randomTown.hover" : "Véletlenszerű város",
+	"vcmi.lobby.pvp.randomTown.help" : "Írjon véletlenszerű várost a csevegésbe",
+	"vcmi.lobby.pvp.randomTownVs.hover" : "Véletlenszerű város vs.",
+	"vcmi.lobby.pvp.randomTownVs.help" : "Írjon két véletlenszerű várost a csevegésbe",
+	"vcmi.lobby.pvp.versus" : "vs.",
+
+	"vcmi.client.errors.invalidMap" : "{Érvénytelen térkép vagy kampány}\n\nNem sikerült elindítani a játékot! A kiválasztott térkép vagy kampány érvénytelen vagy sérült lehet. Indok:\n%s",
+	"vcmi.client.errors.missingCampaigns" : "{Hiányzó adatfájlok}\n\nNem találhatók kampány adatfájlok! Lehet, hogy hiányos vagy sérült Heroes 3 adatfájlokat használ. Telepítse újra az adatokat.",
+	"vcmi.client.errors.modLoadingFailure" : "{Mod betöltési hiba}\n\nKritikus problémák léptek fel a modok betöltésekor! A játék nem működhet megfelelően, vagy összeomolhat! Frissítse vagy tiltsa le az alábbi modokat:\n\n",
+	"vcmi.server.errors.disconnected" : "{Hálózati hiba}\n\nA kapcsolat a játékszerverrel megszakadt!",
+	"vcmi.server.errors.playerLeft" : "{Játékos kilépett}\n\n%s játékos kilépett a játékból!", //%s -> player color
+	"vcmi.server.errors.existingProcess" : "Egy másik VCMI szerver folyamat fut. Kérjük, zárja be, mielőtt új játékot indítana.",
+	"vcmi.server.errors.modsToEnable"    : "{Az alábbi modok szükségesek}",
+	"vcmi.server.errors.modsToDisable"   : "{Az alábbi modokat le kell tiltani}",
+	"vcmi.server.errors.unknownEntity" : "Nem sikerült betölteni a mentést! Ismeretlen entitás '%s' található a mentett játékban! A mentés nem kompatibilis a jelenleg telepített modverziókkal!",
+	"vcmi.server.errors.wrongIdentified"   : "Önt %s játékosként azonosították, miközben %s játékosra számítottak",
+	"vcmi.server.errors.notAllowed"   : "Nem engedélyezett művelet!",
+	
+	"vcmi.dimensionDoor.seaToLandError" : "Nem lehet a Dimenziókapuval tengerről szárazföldre vagy fordítva teleportálni.",
+
+	"vcmi.settingsMainWindow.generalTab.hover" : "Általános",
+	"vcmi.settingsMainWindow.generalTab.help"     : "Váltás az Általános beállítások fülre, amely a játék kliens általános viselkedésével kapcsolatos beállításokat tartalmazza.",
+	"vcmi.settingsMainWindow.battleTab.hover" : "Csata",
+	"vcmi.settingsMainWindow.battleTab.help"     : "Váltás a Csata beállítások fülre, amely lehetővé teszi a csaták viselkedésének konfigurálását.",
+	"vcmi.settingsMainWindow.adventureTab.hover" : "Kalandtérkép",
+	"vcmi.settingsMainWindow.adventureTab.help"  : "Váltás a Kalandtérkép beállítások fülre (a kalandtérkép az a játék része, ahol a játékosok irányítják hőseik mozgását).",
+
+	"vcmi.systemOptions.videoGroup" : "Videó beállítások",
+	"vcmi.systemOptions.audioGroup" : "Hangbeállítások",
+	"vcmi.systemOptions.otherGroup" : "Egyéb beállítások", // unused right now
+	"vcmi.systemOptions.townsGroup" : "Város képernyő",
+
+	"vcmi.statisticWindow.statistics" : "Statisztikák",
+	"vcmi.statisticWindow.tsvCopy" : "Adatok vágólapra másolása",
+	"vcmi.statisticWindow.selectView" : "Nézet kiválasztása",
+	"vcmi.statisticWindow.value" : "Érték",
+	"vcmi.statisticWindow.title.overview" : "Áttekintés",
+	"vcmi.statisticWindow.title.resources" : "Erőforrások",
+	"vcmi.statisticWindow.title.income" : "Bevétel",
+	"vcmi.statisticWindow.title.numberOfHeroes" : "Hősök száma",
+	"vcmi.statisticWindow.title.numberOfTowns" : "Városok száma",
+	"vcmi.statisticWindow.title.numberOfArtifacts" : "Tárgyak száma",
+	"vcmi.statisticWindow.title.numberOfDwellings" : "Lakóhelyek száma",
+	"vcmi.statisticWindow.title.numberOfMines" : "Bányák száma",
+	"vcmi.statisticWindow.title.armyStrength" : "Hadsereg ereje",
+	"vcmi.statisticWindow.title.experience" : "Tapasztalat",
+	"vcmi.statisticWindow.title.resourcesSpentArmy" : "Hadsereg költségei",
+	"vcmi.statisticWindow.title.resourcesSpentBuildings" : "Épületköltségek",
+	"vcmi.statisticWindow.title.mapExplored" : "Felfedezett térkép aránya",
+	"vcmi.statisticWindow.param.playerName" : "Játékos neve",
+	"vcmi.statisticWindow.param.daysSurvived" : "Túlélési napok száma",
+	"vcmi.statisticWindow.param.maxHeroLevel" : "Legmagasabb hősszint",
+	"vcmi.statisticWindow.param.battleWinRatioHero" : "Győzelmi arány (hős ellen)",
+	"vcmi.statisticWindow.param.battleWinRatioNeutral" : "Győzelmi arány (semleges ellen)",
+	"vcmi.statisticWindow.param.battlesHero" : "Csaták (hős ellen)",
+	"vcmi.statisticWindow.param.battlesNeutral" : "Csaták (semleges ellen)",
+	"vcmi.statisticWindow.param.maxArmyStrength" : "Maximális teljes hadsereg erő",
+	"vcmi.statisticWindow.param.tradeVolume" : "Kereskedelmi forgalom",
+	"vcmi.statisticWindow.param.obeliskVisited" : "Obeliszkek meglátogatva",
+	"vcmi.statisticWindow.icon.townCaptured" : "Elfoglalt város",
+	"vcmi.statisticWindow.icon.strongestHeroDefeated" : "Az ellenfél legerősebb hőse legyőzve",
+	"vcmi.statisticWindow.icon.grailFound" : "Szent Grál megtalálva",
+	"vcmi.statisticWindow.icon.defeated" : "Legyőzve",
+
+	"vcmi.systemOptions.fullscreenBorderless.hover" : "Teljes képernyő (keret nélküli)",
+	"vcmi.systemOptions.fullscreenBorderless.help"  : "{Teljes képernyő (keret nélküli)}\n\nHa be van jelölve, a VCMI keret nélküli teljes képernyős módban fut. Ebben a módban a játék mindig az asztal felbontását használja, figyelmen kívül hagyva a kiválasztott felbontást.",
+	"vcmi.systemOptions.fullscreenExclusive.hover"  : "Teljes képernyő (exkluzív)",
+	"vcmi.systemOptions.fullscreenExclusive.help"   : "{Teljes képernyő}\n\nHa be van jelölve, a VCMI exkluzív teljes képernyős módban fut. Ebben a módban a játék megváltoztatja a monitor felbontását a kiválasztott felbontásra.",
+	"vcmi.systemOptions.resolutionButton.hover" : "Felbontás: %wx%h",
+	"vcmi.systemOptions.resolutionButton.help"  : "{Felbontás kiválasztása}\n\nA játék képernyőfelbontásának módosítása.",
+	"vcmi.systemOptions.resolutionMenu.hover"   : "Felbontás kiválasztása",
+	"vcmi.systemOptions.resolutionMenu.help"    : "A játék képernyőfelbontásának módosítása.",
+	"vcmi.systemOptions.scalingButton.hover"   : "Felület méretezés: %p%",
+	"vcmi.systemOptions.scalingButton.help"    : "{Felület méretezés}\n\nA játékbeli felület méretezésének módosítása.",
+	"vcmi.systemOptions.scalingMenu.hover"     : "Felület méretezés kiválasztása",
+	"vcmi.systemOptions.scalingMenu.help"      : "A játékbeli felület méretezésének módosítása.",
+	"vcmi.systemOptions.longTouchButton.hover"   : "Hosszú érintés időtartama: %d ms", // Translation note: "ms" = "milliseconds"
+	"vcmi.systemOptions.longTouchButton.help"    : "{Hosszú érintés időtartama}\n\nÉrintőképernyő használatakor az előugró ablakok megjelennek, miután a képernyőt meghatározott ideig, milliszekundumban megérintették.",
+	"vcmi.systemOptions.longTouchMenu.hover"     : "Hosszú érintés időtartamának kiválasztása",
+	"vcmi.systemOptions.longTouchMenu.help"      : "Hosszú érintés időtartamának módosítása.",
+	"vcmi.systemOptions.longTouchMenu.entry"     : "%d milliszekundum",
+	"vcmi.systemOptions.framerateButton.hover"  : "FPS megjelenítése",
+	"vcmi.systemOptions.framerateButton.help"   : "{FPS megjelenítése}\n\nA másodpercenkénti képkocka számláló láthatóságának váltása a játékablak sarkában.",
+	"vcmi.systemOptions.hapticFeedbackButton.hover"  : "Rezgő visszacsatolás",
+	"vcmi.systemOptions.hapticFeedbackButton.help"   : "{Rezgő visszacsatolás}\n\nA rezgő visszacsatolás engedélyezése vagy tiltása érintőbemeneteknél.",
+	"vcmi.systemOptions.enableUiEnhancementsButton.hover"  : "Felület fejlesztések",
+	"vcmi.systemOptions.enableUiEnhancementsButton.help"   : "{Felület fejlesztések}\n\nKülönféle életminőség-javítások engedélyezése vagy tiltása. Például egy hátizsák gomb stb. Kikapcsolásával klasszikusabb élményt érhet el.",
+	"vcmi.systemOptions.enableLargeSpellbookButton.hover"  : "Nagy Varázskönyv",
+	"vcmi.systemOptions.enableLargeSpellbookButton.help"   : "{Nagy Varázskönyv}\n\nNagyobb varázskönyv engedélyezése, amely több varázslatot tartalmaz oldalanként. A varázskönyv oldalmódosító animáció nem működik ezzel a beállítással.",
+	"vcmi.systemOptions.audioMuteFocus.hover"  : "Elhallgattatás inaktivitáskor",
+	"vcmi.systemOptions.audioMuteFocus.help"   : "{Elhallgattatás inaktivitáskor}\n\nAz audio elhallgattatása inaktív ablak fókusz esetén. Kivételek a játékbeli üzenetek és az új kör hang.",
+
+	"vcmi.adventureOptions.infoBarPick.hover" : "Üzenetek megjelenítése az Információs Panelen",
+	"vcmi.adventureOptions.infoBarPick.help" : "{Üzenetek megjelenítése az Információs Panelen}\n\nAmikor csak lehetséges, a térképi objektumok meglátogatásából származó üzenetek megjelennek az információs panelen, a különálló ablakban való felugrás helyett.",
+	"vcmi.adventureOptions.numericQuantities.hover" : "Számszerű lények mennyisége",
+	"vcmi.adventureOptions.numericQuantities.help" : "{Számszerű lények mennyisége}\n\nAz ellenséges lények hozzávetőleges mennyiségének megjelenítése az A-B numerikus formátumban.",
+	"vcmi.adventureOptions.forceMovementInfo.hover" : "Mindig mutassa a mozgás költségét",
+	"vcmi.adventureOptions.forceMovementInfo.help" : "{Mindig mutassa a mozgás költségét}\n\nMindig mutassa a mozgáspontok adatait az állapotsáv információiban (ahelyett, hogy csak az ALT billentyű lenyomva tartásakor jelenítené meg).",
+	"vcmi.adventureOptions.showGrid.hover" : "Rács megjelenítése",
+	"vcmi.adventureOptions.showGrid.help" : "{Rács megjelenítése}\n\nA kalandtérkép mezői közötti határok kiemelését megjelenítő rács engedélyezése.",
+	"vcmi.adventureOptions.borderScroll.hover" : "Szélső görgetés",
+	"vcmi.adventureOptions.borderScroll.help" : "{Szélső görgetés}\n\nA kalandtérkép görgetése, amikor a kurzor az ablak széléhez ér. A CTRL billentyű lenyomva tartásával letiltható.",
+	"vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Információs Panel Lénykezelés",
+	"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Információs Panel Lénykezelés}\n\nLehetővé teszi a lények átrendezését az információs panelen az alapértelmezett komponensek közötti váltás helyett.",
+	"vcmi.adventureOptions.leftButtonDrag.hover" : "Bal egérgombos húzás",
+	"vcmi.adventureOptions.leftButtonDrag.help" : "{Bal egérgombos húzás}\n\nHa engedélyezve van, az egér mozgatása bal gombbal lenyomva húzza a kalandtérkép nézetét.",
+	"vcmi.adventureOptions.rightButtonDrag.hover" : "Jobb egérgombos húzás",
+	"vcmi.adventureOptions.rightButtonDrag.help" : "{Jobb egérgombos húzás}\n\nHa engedélyezve van, az egér mozgatása jobb gombbal lenyomva húzza a kalandtérkép nézetét.",
+	"vcmi.adventureOptions.smoothDragging.hover" : "Sima térképhúzás",
+	"vcmi.adventureOptions.smoothDragging.help" : "{Sima térképhúzás}\n\nHa engedélyezve van, a térképhúzás modern kifutási hatással rendelkezik.",
+	"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Fakítási effektusok kihagyása",
+	"vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Fakítási effektusok kihagyása}\n\nHa engedélyezve van, kihagyja az objektumok elhalványulását és hasonló effektusokat (erőforrás-gyűjtés, hajóra szállás stb.). Bizonyos esetekben a felhasználói felület reaktívabbá válik az esztétika rovására. Különösen hasznos PvP játékokban. Maximális mozgási sebesség esetén a kihagyás aktív, függetlenül ettől a beállítástól.",
+	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed1.help": "Állítsa a térkép görgetési sebességét nagyon lassúra.",
+	"vcmi.adventureOptions.mapScrollSpeed5.help": "Állítsa a térkép görgetési sebességét nagyon gyorsra.",
+	"vcmi.adventureOptions.mapScrollSpeed6.help": "Állítsa a térkép görgetési sebességét azonnalira.",
+	"vcmi.adventureOptions.hideBackground.hover" : "Háttér elrejtése",
+	"vcmi.adventureOptions.hideBackground.help" : "{Háttér elrejtése}\n\nRejtse el a kalandtérképet a háttérben, és jelenítsen meg helyette egy textúrát.",
+
+	"vcmi.battleOptions.queueSizeLabel.hover": "Kör sorrendjének megjelenítése",
+	"vcmi.battleOptions.queueSizeNoneButton.hover": "KI",
+	"vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO",
+	"vcmi.battleOptions.queueSizeSmallButton.hover": "KICSI",
+	"vcmi.battleOptions.queueSizeBigButton.hover": "NAGY",
+	"vcmi.battleOptions.queueSizeNoneButton.help": "Ne jelenítse meg a kör sorrendjét.",
+	"vcmi.battleOptions.queueSizeAutoButton.help": "Automatikusan állítsa be a kör sorrendjének méretét a játék felbontása alapján (kisebb méretet használ, ha a játék felbontásának magassága kevesebb mint 700 pixel, különben nagy méretet).",
+	"vcmi.battleOptions.queueSizeSmallButton.help": "Állítsa a kör sorrendjének méretét KICSI-re.",
+	"vcmi.battleOptions.queueSizeBigButton.help": "Állítsa a kör sorrendjének méretét NAGY-ra (nem támogatott, ha a játék felbontásának magassága kevesebb mint 700 pixel).",
+	"vcmi.battleOptions.animationsSpeed1.hover": "",
+	"vcmi.battleOptions.animationsSpeed5.hover": "",
+	"vcmi.battleOptions.animationsSpeed6.hover": "",
+	"vcmi.battleOptions.animationsSpeed1.help": "Állítsa az animáció sebességét nagyon lassúra.",
+	"vcmi.battleOptions.animationsSpeed5.help": "Állítsa az animáció sebességét nagyon gyorsra.",
+	"vcmi.battleOptions.animationsSpeed6.help": "Állítsa az animáció sebességét azonnalira.",
+	"vcmi.battleOptions.movementHighlightOnHover.hover": "Mozgás kiemelése fölé húzással",
+	"vcmi.battleOptions.movementHighlightOnHover.help": "{Mozgás kiemelése fölé húzással}\n\nEmelje ki az egység mozgási tartományát, amikor fölé viszi az egeret.",
+	"vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Lövők hatótávolságának megjelenítése",
+	"vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Lövők hatótávolságának megjelenítése fölé húzással}\n\nJelenítse meg a lövők hatótávolságát, amikor fölé viszi az egeret.",
+	"vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Hősök statisztikai ablaka megjelenítése",
+	"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Hősök statisztikai ablaka megjelenítése}\n\nÁllandóan bekapcsolja a hősök statisztikai ablakait, amelyek az elsődleges statisztikákat és a varázspontokat mutatják.",
+	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Bevezető zene kihagyása",
+	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Bevezető zene kihagyása}\n\nLehetővé teszi a műveleteket az egyes csaták elején játszott bevezető zene alatt.",	
+	"vcmi.battleOptions.endWithAutocombat.hover": "Csata befejezése",
+	"vcmi.battleOptions.endWithAutocombat.help": "{Csata befejezése}\n\nAz Auto-Combat azonnal végigjátsza a csatát.",
+	"vcmi.battleOptions.showQuickSpell.hover": "Gyorsvarázslás panel megjelenítése",
+	"vcmi.battleOptions.showQuickSpell.help": "{Gyorsvarázslás panel megjelenítése}\n\nPanel megjelenítése a varázslatok gyors kiválasztásához.",
+
+	"vcmi.adventureMap.revisitObject.hover" : "Objektum újralátogatása",
+	"vcmi.adventureMap.revisitObject.help" : "{Objektum újralátogatása}\n\nHa egy hős jelenleg egy térképobjektumon áll, újralátogathatja a helyszínt.",
+
+	"vcmi.battleWindow.pressKeyToSkipIntro" : "Nyomjon meg egy billentyűt a csata azonnali elindításához",
+	"vcmi.battleWindow.damageEstimation.melee" : "Támadás %CREATURE (%DAMAGE).",
+	"vcmi.battleWindow.damageEstimation.meleeKills" : "Támadás %CREATURE (%DAMAGE, %KILLS).",
+	"vcmi.battleWindow.damageEstimation.ranged" : "Lövés %CREATURE (%SHOTS, %DAMAGE).",
+	"vcmi.battleWindow.damageEstimation.rangedKills" : "Lövés %CREATURE (%SHOTS, %DAMAGE, %KILLS).",
+	"vcmi.battleWindow.damageEstimation.shots" : "%d lövés maradt",
+	"vcmi.battleWindow.damageEstimation.shots.1" : "%d lövés maradt",
+	"vcmi.battleWindow.damageEstimation.damage" : "%d sebzés",
+	"vcmi.battleWindow.damageEstimation.damage.1" : "%d sebzés",
+	"vcmi.battleWindow.damageEstimation.kills" : "%d meghal",
+	"vcmi.battleWindow.damageEstimation.kills.1" : "%d meghal",
+	
+	"vcmi.battleWindow.damageRetaliation.will" : "Megtorlás lesz",
+	"vcmi.battleWindow.damageRetaliation.may" : "Lehetséges megtorlás",
+	"vcmi.battleWindow.damageRetaliation.never" : "Nem lesz megtorlás.",
+	"vcmi.battleWindow.damageRetaliation.damage" : "(%DAMAGE).",
+	"vcmi.battleWindow.damageRetaliation.damageKills" : "(%DAMAGE, %KILLS).",
+	
+	"vcmi.battleWindow.killed" : "Megölve",
+	"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s pontos lövéssel ölte meg!",
+	"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s pontos lövéssel ölte meg!",
+	"vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s pontos lövéssel ölte meg!",
+	"vcmi.battleWindow.endWithAutocombat" : "Biztos, hogy befejezi a csatát automatikus küzdelemmel?",
+
+	"vcmi.battleResultsWindow.applyResultsLabel" : "Elfogadja a csata eredményét?",
+
+	"vcmi.tutorialWindow.title" : "Érintőképernyő bemutató",
+	"vcmi.tutorialWindow.decription.RightClick" : "Érintse meg és tartsa lenyomva azt az elemet, amelyre jobb gombbal szeretne kattintani. Érintse meg a szabad területet a bezáráshoz.",
+	"vcmi.tutorialWindow.decription.MapPanning" : "Érintse meg és húzza egy ujjal a térkép mozgatásához.",
+	"vcmi.tutorialWindow.decription.MapZooming" : "Csípje össze két ujjal a térkép nagyításának megváltoztatásához.",
+	"vcmi.tutorialWindow.decription.RadialWheel" : "Húzás különféle műveletekhez, például lény/hős kezeléshez és város rendeléshez nyit meg egy radiális kereket.",
+	"vcmi.tutorialWindow.decription.BattleDirection" : "Egy adott irányból történő támadáshoz húzza az ujját abból az irányból, amelyből a támadást végre szeretné hajtani.",
+	"vcmi.tutorialWindow.decription.BattleDirectionAbort" : "A támadásirány gesztusa megszakítható, ha az ujj elég messze van.",
+	"vcmi.tutorialWindow.decription.AbortSpell" : "Érintse meg és tartsa lenyomva a varázslat megszakításához.",
+
+	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Elérhető lények megjelenítése",
+	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Elérhető lények megjelenítése}\n\nA lények elérhető számának megjelenítése a város összegzésében (a város képernyő bal alsó sarkában) növekedés helyett.",
+	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Lények heti növekedésének megjelenítése",
+	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Lények heti növekedésének megjelenítése}\n\nA lények heti növekedésének megjelenítése az elérhető mennyiség helyett a város összegzésében (a város képernyő bal alsó sarkában).",
+	"vcmi.otherOptions.compactTownCreatureInfo.hover": "Kompakt lényinformáció",
+	"vcmi.otherOptions.compactTownCreatureInfo.help": "{Kompakt lényinformáció}\n\nKisebb információk megjelenítése a városi lényekről a város összegzésében (a város képernyő bal alsó sarkában).",
+
+	"vcmi.townHall.missingBase"             : "Alap épület %s először megépítendő",
+	"vcmi.townHall.noCreaturesToRecruit"    : "Nincs lény toborzásra!",
+
+	"vcmi.townStructure.bank.borrow" : "Belépett a bankba. A bankár meglátja önt, és így szól: \"Különleges ajánlatot tettünk önnek. 2500 arany kölcsönt vehet fel tőlünk 5 napra. Naponta 500 aranyat kell visszafizetnie.\"",
+	"vcmi.townStructure.bank.payBack" : "Belépett a bankba. A bankár meglátja önt, és így szól: \"Már felvette a kölcsönt. Fizesse vissza, mielőtt újat venne fel.\"",
+
+
+	"vcmi.logicalExpressions.anyOf"  : "Az alábbiak egyike:",
+	"vcmi.logicalExpressions.allOf"  : "Az alábbiak mindegyike:",
+	"vcmi.logicalExpressions.noneOf" : "Az alábbiak egyike sem:",
+
+	"vcmi.heroWindow.openCommander.hover" : "Parancsnok információs ablak megnyitása",
+	"vcmi.heroWindow.openCommander.help"  : "A hős parancsnokáról részletek megjelenítése.",
+	"vcmi.heroWindow.openBackpack.hover" : "Műtárgy hátizsák ablak megnyitása",
+	"vcmi.heroWindow.openBackpack.help"  : "Az ablak megnyitása, amely megkönnyíti a műtárgy hátizsák kezelését.",
+	"vcmi.heroWindow.sortBackpackByCost.hover"  : "Rendezés ár szerint",
+	"vcmi.heroWindow.sortBackpackByCost.help"  : "A műtárgyak ár szerinti rendezése a hátizsákban.",
+	"vcmi.heroWindow.sortBackpackBySlot.hover"  : "Rendezés nyílás szerint",
+	"vcmi.heroWindow.sortBackpackBySlot.help"  : "A műtárgyak nyílás szerinti rendezése a hátizsákban.",
+	"vcmi.heroWindow.sortBackpackByClass.hover"  : "Rendezés osztály szerint",
+	"vcmi.heroWindow.sortBackpackByClass.help"  : "A műtárgyak osztály szerinti rendezése a hátizsákban. Kincs, Kisebb, Nagyobb, Relikvia",
+	"vcmi.heroWindow.fusingArtifact.fusing" : "Ön birtokában van az összes szükséges komponensnek a(z) %s összeolvasztásához. Szeretné elvégezni az összeolvasztást? {Minden komponens elfogy az összeolvasztás során.}",
+
+	"vcmi.tavernWindow.inviteHero"  : "Hős meghívása",
+
+	"vcmi.commanderWindow.artifactMessage" : "Szeretné visszaadni ezt a műtárgyat a hősnek?",
+
+	"vcmi.creatureWindow.showBonuses.hover"    : "Váltás bónuszok nézetre",
+	"vcmi.creatureWindow.showBonuses.help"     : "Az összes aktív bónusz megjelenítése a parancsnokról.",
+	"vcmi.creatureWindow.showSkills.hover"     : "Váltás képességek nézetre",
+	"vcmi.creatureWindow.showSkills.help"      : "A parancsnok által elsajátított összes képesség megjelenítése.",
+	"vcmi.creatureWindow.returnArtifact.hover" : "Műtárgy visszaadása",
+	"vcmi.creatureWindow.returnArtifact.help"  : "Kattintson erre a gombra, hogy visszaadja a műtárgyat a hős hátizsákjába.",
+
+	"vcmi.questLog.hideComplete.hover" : "Teljesített küldetések elrejtése",
+	"vcmi.questLog.hideComplete.help"  : "Az összes teljesített küldetés elrejtése.",
+
+	"vcmi.randomMapTab.widgets.randomTemplate"      : "(Véletlenszerű)",
+	"vcmi.randomMapTab.widgets.templateLabel"        : "Sablon",
+	"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Beállítás...",
+	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Csapatok összeállítása",
+	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Úttípusok",
+
+	"vcmi.optionsTab.turnOptions.hover" : "Kör opciók",
+	"vcmi.optionsTab.turnOptions.help" : "Kör időzítő és egyidejű kör opciók kiválasztása",
+
+	"vcmi.optionsTab.chessFieldBase.hover" : "Alap időzítő",
+	"vcmi.optionsTab.chessFieldTurn.hover" : "Kör időzítő",
+	"vcmi.optionsTab.chessFieldBattle.hover" : "Csata időzítő",
+	"vcmi.optionsTab.chessFieldUnit.hover" : "Egység időzítő",
+	"vcmi.optionsTab.chessFieldBase.help" : "A {Kör időzítő} 0-ra érkezésekor használható. Egyszer beállítva a játék kezdésekor. Amikor eléri a nullát, befejezi az aktuális kört. Bármilyen folyamatban lévő csata vereséggel zárul.",
+	"vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Harcon kívül vagy a {Csata időzítő} lejártakor használható. Minden körben újraindul. A megmaradt idő hozzáadódik az {Alap időzítőhöz} a kör végén.",
+	"vcmi.optionsTab.chessFieldTurnDiscard.help" : "Harcon kívül vagy a {Csata időzítő} lejártakor használható. Minden körben újraindul. A fel nem használt idő elveszik.",
+	"vcmi.optionsTab.chessFieldBattle.help" : "Csatában az AI-val vagy PvP harcban, amikor az {Egység időzítő} lejár. Minden csata kezdetén újraindul.",
+	"vcmi.optionsTab.chessFieldUnitAccumulate.help" : "PvP harcban egység műveletek kiválasztásakor használható. A megmaradt idő hozzáadódik a {Csata időzítőhöz} az egység körének végén.",
+	"vcmi.optionsTab.chessFieldUnitDiscard.help" : "PvP harcban egység műveletek kiválasztásakor használható. Minden egység körének kezdetén újraindul. A fel nem használt idő elveszik.",
+
+	"vcmi.optionsTab.accumulate" : "Felhalmozás",
+
+	"vcmi.optionsTab.simturnsTitle" : "Egyidejű körök",
+	"vcmi.optionsTab.simturnsMin.hover" : "Legalább",
+	"vcmi.optionsTab.simturnsMax.hover" : "Legfeljebb",
+	"vcmi.optionsTab.simturnsAI.hover" : "(Kísérleti) Egyidejű AI körök",
+	"vcmi.optionsTab.simturnsMin.help" : "Adott számú napig egyidejű játék. Játékosok közötti kapcsolatok blokkolva ezen időszak alatt.",
+	"vcmi.optionsTab.simturnsMax.help" : "Adott számú napig vagy más játékossal való kapcsolatfelvételig egyidejű játék.",
+	"vcmi.optionsTab.simturnsAI.help" : "{Egyidejű AI körök}\nKísérleti opció. Lehetővé teszi, hogy az AI játékosok emberi játékossal egyidejűleg cselekedjenek, amikor egyidejű körök engedélyezve vannak.",
+
+	"vcmi.optionsTab.turnTime.select"     : "Kör időzítő előbeállítás kiválasztása",
+	"vcmi.optionsTab.turnTime.unlimited"  : "Korlátlan köridő",
+	"vcmi.optionsTab.turnTime.classic.1"  : "Klasszikus időzítő: 1 perc",
+	"vcmi.optionsTab.turnTime.classic.2"  : "Klasszikus időzítő: 2 perc",
+	"vcmi.optionsTab.turnTime.classic.5"  : "Klasszikus időzítő: 5 perc",
+	"vcmi.optionsTab.turnTime.classic.10" : "Klasszikus időzítő: 10 perc",
+	"vcmi.optionsTab.turnTime.classic.20" : "Klasszikus időzítő: 20 perc",
+	"vcmi.optionsTab.turnTime.classic.30" : "Klasszikus időzítő: 30 perc",
+	"vcmi.optionsTab.turnTime.chess.20"   : "Sakk: 20:00 + 10:00 + 02:00 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.16"   : "Sakk: 16:00 + 08:00 + 01:30 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.8"    : "Sakk: 08:00 + 04:00 + 01:00 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.4"    : "Sakk: 04:00 + 02:00 + 00:30 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.2"    : "Sakk: 02:00 + 01:00 + 00:15 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.1"    : "Sakk: 01:00 + 01:00 + 00:00 + 00:00",
+
+	"vcmi.optionsTab.simturns.select"         : "Egyidejű körök előbeállítás kiválasztása",
+	"vcmi.optionsTab.simturns.none"           : "Nincs egyidejű kör",
+	"vcmi.optionsTab.simturns.tillContactMax" : "Simultán körök: Kapcsolatig",
+	"vcmi.optionsTab.simturns.tillContact1"   : "Simultán körök: 1 hét, kapcsolat megszakítással",
+	"vcmi.optionsTab.simturns.tillContact2"   : "Simultán körök: 2 hét, kapcsolat megszakítással",
+	"vcmi.optionsTab.simturns.tillContact4"   : "Simultán körök: 1 hónap, kapcsolat megszakítással",
+	"vcmi.optionsTab.simturns.blocked1"       : "Simultán körök: 1 hét, blokkolt kapcsolatok",
+	"vcmi.optionsTab.simturns.blocked2"       : "Simultán körök: 2 hét, blokkolt kapcsolatok",
+	"vcmi.optionsTab.simturns.blocked4"       : "Simultán körök: 1 hónap, blokkolt kapcsolatok",
+	
+	// Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language
+	// Using this information, VCMI will automatically select correct plural form for every possible amount
+	"vcmi.optionsTab.simturns.days.0" : " %d nap",
+	"vcmi.optionsTab.simturns.days.1" : " %d nap",
+	"vcmi.optionsTab.simturns.days.2" : " %d nap",
+	"vcmi.optionsTab.simturns.weeks.0" : " %d hét",
+	"vcmi.optionsTab.simturns.weeks.1" : " %d hét",
+	"vcmi.optionsTab.simturns.weeks.2" : " %d hét",
+	"vcmi.optionsTab.simturns.months.0" : " %d hónap",
+	"vcmi.optionsTab.simturns.months.1" : " %d hónap",
+	"vcmi.optionsTab.simturns.months.2" : " %d hónap",
+
+	"vcmi.optionsTab.extraOptions.hover" : "Extra beállítások",
+	"vcmi.optionsTab.extraOptions.help" : "További beállítások a játékhoz",
+
+	"vcmi.optionsTab.cheatAllowed.hover" : "Csalások engedélyezése",
+	"vcmi.optionsTab.unlimitedReplay.hover" : "Korlátlan csata visszajátszás",
+	"vcmi.optionsTab.cheatAllowed.help" : "{Csalások engedélyezése}\nLehetővé teszi a csalások bevitelét a játék során.",
+	"vcmi.optionsTab.unlimitedReplay.help" : "{Korlátlan csata visszajátszás}\nNincs korlát a csaták visszajátszására.",
+
+	// Custom victory conditions for H3 campaigns and HotA maps
+	"vcmi.map.victoryCondition.daysPassed.toOthers" : "Az ellenségnek sikerült túlélni a mai napig. Győzelem az övék!",
+	"vcmi.map.victoryCondition.daysPassed.toSelf" : "Gratulálok! Sikerült túlélni. Győzelem a tiéd!",
+	"vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "Az ellenség legyőzte az összes szörnyet, amely e földet sújtotta, és igényt tart a győzelemre!",
+	"vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Gratulálok! Legyőzted az összes szörnyet, amely e földet sújtotta, és igényt tarthatsz a győzelemre!",
+	"vcmi.map.victoryCondition.collectArtifacts.message" : "Szerezz három műtárgyat",
+	"vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Gratulálok! Minden ellenségedet legyőzted, és megszerezted az Angyali Szövetséget! Győzelem a tiéd!",
+	"vcmi.map.victoryCondition.angelicAlliance.message" : "Győzd le az összes ellenséget és hozd létre az Angyali Szövetséget",
+	"vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf" : "Sajnos elvesztetted az Angyali Szövetség egy részét. Minden elveszett.",
+
+	// few strings from WoG used by vcmi
+	"vcmi.stackExperience.description" : "» C s o m a g   E l m é l e t i   R é s z l e t e i «\n\nLény típusa ................... : %s\nTapasztalati rang ................. : %s (%i)\nTapasztalati pontok ............... : %i\nKövetkező ranghoz szükséges tapasztalati pontok .. : %i\nMaximális tapasztalati pont csatánként ... : %i%% (%i)\nLények száma a csomagban .... : %i\nMaximális új toborzások\n a jelenlegi rang elvesztése nélkül .... : %i\nTapasztalati szorzó ........... : %.2f\nFejlesztési szorzó .............. : %.2f\nTapasztalat 10-es rang után ........ : %i\nMaximális új toborzások a\n 10-es rang fenntartásához, ha maximális a tapasztalat : %i",
+	"vcmi.stackExperience.rank.0" : "Alap",
+	"vcmi.stackExperience.rank.1" : "Kezdő",
+	"vcmi.stackExperience.rank.2" : "Képzett",
+	"vcmi.stackExperience.rank.3" : "Ügyes",
+	"vcmi.stackExperience.rank.4" : "Tapasztalt",
+	"vcmi.stackExperience.rank.5" : "Veterán",
+	"vcmi.stackExperience.rank.6" : "Haladó",
+	"vcmi.stackExperience.rank.7" : "Szakértő",
+	"vcmi.stackExperience.rank.8" : "Elit",
+	"vcmi.stackExperience.rank.9" : "Mester",
+	"vcmi.stackExperience.rank.10" : "Ász",
+	
+	// Strings for HotA Seer Hut / Quest Guards
+	"core.seerhut.quest.heroClass.complete.0" : "Ó, te vagy %s. Íme egy ajándék neked. Elfogadod?",
+	"core.seerhut.quest.heroClass.complete.1" : "Ó, te vagy %s. Íme egy ajándék neked. Elfogadod?",
+	"core.seerhut.quest.heroClass.complete.2" : "Ó, te vagy %s. Íme egy ajándék neked. Elfogadod?",
+	"core.seerhut.quest.heroClass.complete.3" : "Az őrök észreveszik, hogy te vagy %s, és felajánlják, hogy átengednek. Elfogadod?",
+	"core.seerhut.quest.heroClass.complete.4" : "Az őrök észreveszik, hogy te vagy %s, és felajánlják, hogy átengednek. Elfogadod?",
+	"core.seerhut.quest.heroClass.complete.5" : "Az őrök észreveszik, hogy te vagy %s, és felajánlják, hogy átengednek. Elfogadod?",
+	"core.seerhut.quest.heroClass.description.0" : "Küldj %s-t %s-hoz",
+	"core.seerhut.quest.heroClass.description.1" : "Küldj %s-t %s-hoz",
+	"core.seerhut.quest.heroClass.description.2" : "Küldj %s-t %s-hoz",
+	"core.seerhut.quest.heroClass.description.3" : "Küldj %s-t a kapuhoz",
+	"core.seerhut.quest.heroClass.description.4" : "Küldj %s-t a kapuhoz",
+	"core.seerhut.quest.heroClass.description.5" : "Küldj %s-t a kapuhoz",
+	"core.seerhut.quest.heroClass.hover.0" : "(%s osztályú hőst keres)",
+	"core.seerhut.quest.heroClass.hover.1" : "(%s osztályú hőst keres)",
+	"core.seerhut.quest.heroClass.hover.2" : "(%s osztályú hőst keres)",
+	"core.seerhut.quest.heroClass.hover.3" : "(%s osztályú hőst keres)",
+	"core.seerhut.quest.heroClass.hover.4" : "(%s osztályú hőst keres)",
+	"core.seerhut.quest.heroClass.hover.5" : "(%s osztályú hőst keres)",
+	"core.seerhut.quest.heroClass.receive.0" : "Van egy ajándékom %s számára.",
+	"core.seerhut.quest.heroClass.receive.1" : "Van egy ajándékom %s számára.",
+	"core.seerhut.quest.heroClass.receive.2" : "Van egy ajándékom %s számára.",
+	"core.seerhut.quest.heroClass.receive.3" : "Az őrök azt mondják, hogy csak %s-t engedik át.",
+	"core.seerhut.quest.heroClass.receive.4" : "Az őrök azt mondják, hogy csak %s-t engedik át.",
+	"core.seerhut.quest.heroClass.receive.5" : "Az őrök azt mondják, hogy csak %s-t engedik át.",
+	"core.seerhut.quest.heroClass.visit.0" : "Te nem vagy %s. Nincs semmim neked. Távozz!",
+	"core.seerhut.quest.heroClass.visit.1" : "Te nem vagy %s. Nincs semmim neked. Távozz!",
+	"core.seerhut.quest.heroClass.visit.2" : "Te nem vagy %s. Nincs semmim neked. Távozz!",
+	"core.seerhut.quest.heroClass.visit.3" : "Az őrök itt csak %s-t engednek át.",
+	"core.seerhut.quest.heroClass.visit.4" : "Az őrök itt csak %s-t engednek át.",
+	"core.seerhut.quest.heroClass.visit.5" : "Az őrök itt csak %s-t engednek át.",
+	
+	"core.seerhut.quest.reachDate.complete.0" : "Most szabad vagyok. Itt van, amim van neked. Elfogadod?",
+	"core.seerhut.quest.reachDate.complete.1" : "Most szabad vagyok. Itt van, amim van neked. Elfogadod?",
+	"core.seerhut.quest.reachDate.complete.2" : "Most szabad vagyok. Itt van, amim van neked. Elfogadod?",
+	"core.seerhut.quest.reachDate.complete.3" : "Szabadon átmehetsz most. Át szeretnél menni?",
+	"core.seerhut.quest.reachDate.complete.4" : "Szabadon átmehetsz most. Át szeretnél menni?",
+	"core.seerhut.quest.reachDate.complete.5" : "Szabadon átmehetsz most. Át szeretnél menni?",
+	"core.seerhut.quest.reachDate.description.0" : "Várj %s-ig %s-ért",
+	"core.seerhut.quest.reachDate.description.1" : "Várj %s-ig %s-ért",
+	"core.seerhut.quest.reachDate.description.2" : "Várj %s-ig %s-ért",
+	"core.seerhut.quest.reachDate.description.3" : "Várj %s-ig a kapuhoz",
+	"core.seerhut.quest.reachDate.description.4" : "Várj %s-ig a kapuhoz",
+	"core.seerhut.quest.reachDate.description.5" : "Várj %s-ig a kapuhoz",
+	"core.seerhut.quest.reachDate.hover.0" : "(Ne térj vissza %s előtt)",
+	"core.seerhut.quest.reachDate.hover.1" : "(Ne térj vissza %s előtt)",
+	"core.seerhut.quest.reachDate.hover.2" : "(Ne térj vissza %s előtt)",
+	"core.seerhut.quest.reachDate.hover.3" : "(Ne térj vissza %s előtt)",
+	"core.seerhut.quest.reachDate.hover.4" : "(Ne térj vissza %s előtt)",
+	"core.seerhut.quest.reachDate.hover.5" : "(Ne térj vissza %s előtt)",
+	"core.seerhut.quest.reachDate.receive.0" : "Elfoglalt vagyok. Ne térj vissza %s előtt",
+	"core.seerhut.quest.reachDate.receive.1" : "Elfoglalt vagyok. Ne térj vissza %s előtt",
+	"core.seerhut.quest.reachDate.receive.2" : "Elfoglalt vagyok. Ne térj vissza %s előtt",
+	"core.seerhut.quest.reachDate.receive.3" : "Zárva %s-ig.",
+	"core.seerhut.quest.reachDate.receive.4" : "Zárva %s-ig.",
+	"core.seerhut.quest.reachDate.receive.5" : "Zárva %s-ig.",
+	"core.seerhut.quest.reachDate.visit.0" : "Elfoglalt vagyok. Ne térj vissza %s előtt.",
+	"core.seerhut.quest.reachDate.visit.1" : "Elfoglalt vagyok. Ne térj vissza %s előtt.",
+	"core.seerhut.quest.reachDate.visit.2" : "Elfoglalt vagyok. Ne térj vissza %s előtt.",
+	"core.seerhut.quest.reachDate.visit.3" : "Zárva %s-ig.",
+	"core.seerhut.quest.reachDate.visit.4" : "Zárva %s-ig.",
+	"core.seerhut.quest.reachDate.visit.5" : "Zárva %s-ig.",
+	
+	"mapObject.core.hillFort.object.description" : "Lények fejlesztése. Az 1-4. szint olcsóbb, mint az adott városban.",
+	
+	"core.bonus.ADDITIONAL_ATTACK.name": "Dupla csapás",
+	"core.bonus.ADDITIONAL_ATTACK.description": "Kétszer támad",
+	"core.bonus.ADDITIONAL_RETALIATION.name": "További visszatámadások",
+	"core.bonus.ADDITIONAL_RETALIATION.description": "Még ${val} alkalommal visszatámadhat",
+	"core.bonus.AIR_IMMUNITY.name": "Levegő immunitás",
+	"core.bonus.AIR_IMMUNITY.description": "Immunis az összes levegő mágiához tartozó varázslattal szemben",
+	"core.bonus.ATTACKS_ALL_ADJACENT.name": "Mindenirányú támadás",
+	"core.bonus.ATTACKS_ALL_ADJACENT.description": "Minden szomszédos ellenséget támad",
+	"core.bonus.BLOCKS_RETALIATION.name": "Nincs visszatámadás",
+	"core.bonus.BLOCKS_RETALIATION.description": "Az ellenség nem tud visszatámadni",
+	"core.bonus.BLOCKS_RANGED_RETALIATION.name": "Nincs távolsági visszatámadás",
+	"core.bonus.BLOCKS_RANGED_RETALIATION.description": "Az ellenség nem tud távolsági támadással visszatámadni",
+	"core.bonus.CATAPULT.name": "Katapult",
+	"core.bonus.CATAPULT.description": "A várfalakat támadja",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Csökkentett varázsköltség (${val})",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Csökkenti a hős varázslatainak költségét ${val}-mal",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Varázslatgyengítés (${val})",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Növeli az ellenség varázslatainak költségét ${val}-mal",
+	"core.bonus.CHARGE_IMMUNITY.name": "Töltési immunitás",
+	"core.bonus.CHARGE_IMMUNITY.description": "Immunis a lovagok és bajnokok töltése ellen",
+	"core.bonus.DARKNESS.name": "Sötétség takarója",
+	"core.bonus.DARKNESS.description": "Sötétség burkát hozza létre ${val} sugarú körben",
+	"core.bonus.DEATH_STARE.name": "Halálszem (${val}%)",
+	"core.bonus.DEATH_STARE.description": "${val}% eséllyel öl meg egy lényt",
+	"core.bonus.DEFENSIVE_STANCE.name": "Védelmi bónusz",
+	"core.bonus.DEFENSIVE_STANCE.description": "+${val} védelem védekezéskor",
+	"core.bonus.DESTRUCTION.name": "Pusztítás",
+	"core.bonus.DESTRUCTION.description": "${val}% eséllyel további egységeket öl meg támadás után",
+	"core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Halálos csapás",
+	"core.bonus.DOUBLE_DAMAGE_CHANCE.description": "${val}% eséllyel duplázza az alap sebzést támadáskor",
+	"core.bonus.DRAGON_NATURE.name": "Sárkány",
+	"core.bonus.DRAGON_NATURE.description": "A lénynek sárkány természete van",
+	"core.bonus.EARTH_IMMUNITY.name": "Föld immunitás",
+	"core.bonus.EARTH_IMMUNITY.description": "Immunis az összes föld mágiához tartozó varázslattal szemben",
+	"core.bonus.ENCHANTER.name": "Varázsló",
+	"core.bonus.ENCHANTER.description": "Tömeges ${subtype.spell} varázslatot használ minden körben",
+	"core.bonus.ENCHANTED.name": "Elvarázsolt",
+	"core.bonus.ENCHANTED.description": "Állandóan hatással van a(z) ${subtype.spell}",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.name": "Támadás figyelmen kívül hagyása (${val}%)",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.description": "Támadáskor az ellenség támadásának ${val}%-át figyelmen kívül hagyja",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Védelem figyelmen kívül hagyása (${val}%)",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Támadáskor az ellenség védekezésének ${val}%-át figyelmen kívül hagyja",
+	"core.bonus.FIRE_IMMUNITY.name": "Tűz immunitás",
+	"core.bonus.FIRE_IMMUNITY.description": "Immunis az összes tűz mágiához tartozó varázslattal szemben",
+	"core.bonus.FIRE_SHIELD.name": "Tűzpajzs (${val}%)",
+	"core.bonus.FIRE_SHIELD.description": "Visszaver egy részét a közelharci sebzésnek",
+	"core.bonus.FIRST_STRIKE.name": "Első csapás",
+	"core.bonus.FIRST_STRIKE.description": "Ez a lény még a támadás előtt visszatámad",
+	"core.bonus.FEAR.name": "Félelem",
+	"core.bonus.FEAR.description": "Félelmet kelt az ellenséges egységekben",
+	"core.bonus.FEARLESS.name": "Félelem nélküli",
+	"core.bonus.FEARLESS.description": "Immunis a félelem képességre",
+	"core.bonus.FEROCITY.name": "Vadság",
+	"core.bonus.FEROCITY.description": "${val} további alkalommal támad, ha bárkit megölt",
+	"core.bonus.FLYING.name": "Repülés",
+	"core.bonus.FLYING.description": "Repül, amikor mozog (figyelmen kívül hagyja az akadályokat)",
+	"core.bonus.FREE_SHOOTING.name": "Közeli lövés",
+	"core.bonus.FREE_SHOOTING.description": "Távolsági támadást használhat közelharcban is",
+	"core.bonus.GARGOYLE.name": "Gargoyle",
+	"core.bonus.GARGOYLE.description": "Nem kelthető újra vagy gyógyítható",
+	"core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Sebzéscsökkentés (${val}%)",
+	"core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Csökkenti a közelharci vagy távolsági támadások fizikai sebzését",
+	"core.bonus.HATE.name": "Gyűlöli ${subtype.creature}",
+	"core.bonus.HATE.description": "${val}%-kal több sebzést okoz ${subtype.creature}-nek",
+	"core.bonus.HEALER.name": "Gyógyító",
+	"core.bonus.HEALER.description": "Gyógyítja a szövetséges egységeket",
+	"core.bonus.HP_REGENERATION.name": "Regeneráció",
+	"core.bonus.HP_REGENERATION.description": "${val} életerőt gyógyít minden körben",
+	"core.bonus.JOUSTING.name": "Bajnoki töltés",
+	"core.bonus.JOUSTING.description": "+${val}% sebzés minden megtett hatszögért",
+	"core.bonus.KING.name": "Király",
+	"core.bonus.KING.description": "Érzékeny a SLAYER ${val} vagy magasabb szintjére",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.name": "Varázslatimmunitás 1-${val}",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.description": "Immunis az 1-${val} szintű varázslatokra",
+	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Korlátozott lövőtáv",
+	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Nem célozhat ${val} hatszögnél távolabb lévő egységeket",
+	"core.bonus.LIFE_DRAIN.name": "Életelvonás (${val}%)",
+	"core.bonus.LIFE_DRAIN.description": "A sebzés ${val}%-át elszívja",
+	"core.bonus.MANA_CHANNELING.name": "Varázs csatorna ${val}%",
+	"core.bonus.MANA_CHANNELING.description": "A hősöd megkapja az ellenség által költött mana ${val}%-át",
+	"core.bonus.MANA_DRAIN.name": "Manaelszívás",
+	"core.bonus.MANA_DRAIN.description": "Minden körben elszív ${val} manát",
+	"core.bonus.MAGIC_MIRROR.name": "Varázstükör (${val}%)",
+	"core.bonus.MAGIC_MIRROR.description": "${val}% eséllyel visszairányít egy támadó varázslatot egy ellenségre",
+	"core.bonus.MAGIC_RESISTANCE.name": "Varázsellenállás (${val}%)",
+	"core.bonus.MAGIC_RESISTANCE.description": "${val}% eséllyel ellenáll egy ellenséges varázslatnak",
+	"core.bonus.MIND_IMMUNITY.name": "Elmevarázslat-immunitás",
+	"core.bonus.MIND_IMMUNITY.description": "Immunis az Elme-típusú varázslatokra",
+	"core.bonus.NO_DISTANCE_PENALTY.name": "Nincs távolsági büntetés",
+	"core.bonus.NO_DISTANCE_PENALTY.description": "Teljes sebzést okoz bármilyen távolságból",
+	"core.bonus.NO_MELEE_PENALTY.name": "Nincs közelharci büntetés",
+	"core.bonus.NO_MELEE_PENALTY.description": "A lénynek nincs közelharci büntetése",
+	"core.bonus.NO_MORALE.name": "Semleges morál",
+	"core.bonus.NO_MORALE.description": "A lény immunis a morálhatásokra",
+	"core.bonus.NO_WALL_PENALTY.name": "Nincs falbüntetés",
+	"core.bonus.NO_WALL_PENALTY.description": "Teljes sebzést okoz ostrom közben",
+	"core.bonus.NON_LIVING.name": "Nem élő",
+	"core.bonus.NON_LIVING.description": "Immunis számos hatásra",
+	"core.bonus.RANDOM_SPELLCASTER.name": "Véletlenszerű varázsló",
+	"core.bonus.RANDOM_SPELLCASTER.description": "Véletlenszerű varázslatot tud használni",
+	"core.bonus.RANGED_RETALIATION.name": "Távolsági visszatámadás",
+	"core.bonus.RANGED_RETALIATION.description": "Távolsági visszatámadást tud végrehajtani",
+	"core.bonus.RECEPTIVE.name": "Befogadó",
+	"core.bonus.RECEPTIVE.description": "Nem immunis a baráti varázslatokra",
+	"core.bonus.REBIRTH.name": "Újjászületés (${val}%)",
+	"core.bonus.REBIRTH.description": "${val}% eséllyel a lények újraélednek haláluk után",
+	"core.bonus.RETURN_AFTER_STRIKE.name": "Támad és visszatér",
+	"core.bonus.RETURN_AFTER_STRIKE.description": "Visszatér közelharci támadás után",
+	"core.bonus.REVENGE.name": "Bosszú",
+	"core.bonus.REVENGE.description": "További sebzést okoz a támadó elvesztett életerejétől függően",
+	"core.bonus.SHOOTER.name": "Távolsági",
+	"core.bonus.SHOOTER.description": "A lény távolsági támadást hajthat végre",
+	"core.bonus.SHOOTS_ALL_ADJACENT.name": "Mindenirányú lövés",
+	"core.bonus.SHOOTS_ALL_ADJACENT.description": "A lény távolsági támadásai egy kis területen minden célpontot érintenek",
+	"core.bonus.SOUL_STEAL.name": "Lélekrablás",
+	"core.bonus.SOUL_STEAL.description": "Minden megölt ellenség után ${val} új lényt kap",
+	"core.bonus.SPELLCASTER.name": "Varázsló",
+	"core.bonus.SPELLCASTER.description": "Képes a(z) ${subtype.spell} varázslatot elmondani",
+	"core.bonus.SPELL_AFTER_ATTACK.name": "Varázslat támadás után",
+	"core.bonus.SPELL_AFTER_ATTACK.description": "${val}% eséllyel a lény a támadás után a(z) ${subtype.spell} varázslatot használja",
+	"core.bonus.SPELL_BEFORE_ATTACK.name": "Varázslat támadás előtt",
+	"core.bonus.SPELL_BEFORE_ATTACK.description": "${val}% eséllyel a lény a támadás előtt a(z) ${subtype.spell} varázslatot használja",
+	"core.bonus.SPELL_IMMUNITY.name": "Varázslatimmunitás",
+	"core.bonus.SPELL_IMMUNITY.description": "Immunis a(z) ${subtype.spell} varázslattal szemben",
+	"core.bonus.SPELL_LIKE_ATTACK.name": "Varázslatszerű támadás",
+	"core.bonus.SPELL_LIKE_ATTACK.description": "A(z) ${subtype.spell} varázslattal támad",
+	"core.bonus.SPELL_RESISTANCE_AURA.name": "Ellenállás aurája",
+	"core.bonus.SPELL_RESISTANCE_AURA.description": "Közeli egységek ${val}% varázsellenállást kapnak",
+	"core.bonus.SUMMON_GUARDIANS.name": "Őrök idézése",
+	"core.bonus.SUMMON_GUARDIANS.description": "A csata kezdetén idézi a(z) ${subtype.creature} lényeket (${val}%)",
+	"core.bonus.SYNERGY_TARGET.name": "Szinkronizálható",
+	"core.bonus.SYNERGY_TARGET.description": "Ez a lény érzékeny a szinergiahatásokra",
+	"core.bonus.TWO_HEX_ATTACK_BREATH.name": "Lélegzet",
+	"core.bonus.TWO_HEX_ATTACK_BREATH.description": "Lélegzési támadás (2 hatszögnyi távolság)",
+	"core.bonus.THREE_HEADED_ATTACK.name": "Háromfejű támadás",
+	"core.bonus.THREE_HEADED_ATTACK.description": "Három szomszédos egységet támad",
+	"core.bonus.TRANSMUTATION.name": "Átalakítás",
+	"core.bonus.TRANSMUTATION.description": "${val}% eséllyel az ellenfelet más egységtípussá alakítja",
+	"core.bonus.UNDEAD.name": "Élőholt",
+	"core.bonus.UNDEAD.description": "A lény élőholt",
+	"core.bonus.UNLIMITED_RETALIATIONS.name": "Korlátlan visszatámadás",
+	"core.bonus.UNLIMITED_RETALIATIONS.description": "Korlátlan számú támadásra képes visszatámadni",
+	"core.bonus.WATER_IMMUNITY.name": "Víz immunitás",
+	"core.bonus.WATER_IMMUNITY.description": "Immunis az összes víz mágiához tartozó varázslattal szemben",
+	"core.bonus.WIDE_BREATH.name": "Széles lélegzet",
+	"core.bonus.WIDE_BREATH.description": "Széles lélegzési támadás (több hatszög)",
+	"core.bonus.DISINTEGRATE.name": "Megsemmisítés",
+	"core.bonus.DISINTEGRATE.description": "Halál után nem marad hátra holttest",
+	"core.bonus.INVINCIBLE.name": "Legyőzhetetlen",
+	"core.bonus.INVINCIBLE.description": "Nem befolyásolható semmivel",
+	"core.bonus.MECHANICAL.name": "Mechanikus",
+	"core.bonus.MECHANICAL.description": "Immunis számos hatásra, javítható",
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.name": "Prizma Lélegzet",
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.description": "Prizma Lélegzet támadás (három irányban)",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "Varázsellenállás",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.air": "Levegő varázsellenállás",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.fire": "Tűz varázsellenállás",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.water": "Víz varázsellenállás",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.earth": "Föld varázsellenállás",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description": "Minden varázslat által okozott sebzés ${val}%-kal csökken.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.air": "Az összes levegő varázslat által okozott sebzés ${val}%-kal csökken.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.fire": "Az összes tűz varázslat által okozott sebzés ${val}%-kal csökken.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.water": "Az összes víz varázslat által okozott sebzés ${val}%-kal csökken.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.earth": "Az összes föld varázslat által okozott sebzés ${val}%-kal csökken.",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name": "Varázslatimmunitás",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.air": "Levegő immunitás",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.fire": "Tűz immunitás",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.water": "Víz immunitás",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.earth": "Föld immunitás",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description": "Ez az egység immunis minden varázslattal szemben",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.air": "Ez az egység immunis minden levegő mágia iskolájához tartozó varázslattal szemben",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.fire": "Ez az egység immunis minden tűz mágia iskolájához tartozó varázslattal szemben",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.water": "Ez az egység immunis minden víz mágia iskolájához tartozó varázslattal szemben",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.earth": "Ez az egység immunis minden föld mágia iskolájához tartozó varázslattal szemben",
+	"core.bonus.OPENING_BATTLE_SPELL.name": "Varázslattal indul",
+	"core.bonus.OPENING_BATTLE_SPELL.description": "A csata elején a(z) ${subtype.spell} varázslatot idézi meg",
+	
+	"spell.core.castleMoat.name" : "Vizesárok",
+	"spell.core.castleMoatTrigger.name" : "Vizesárok",
+	"spell.core.catapultShot.name" : "Katapult lövés",
+	"spell.core.cyclopsShot.name" : "Ostrom lövés",
+	"spell.core.dungeonMoat.name" : "Forró olaj",
+	"spell.core.dungeonMoatTrigger.name" : "Forró olaj",
+	"spell.core.fireWallTrigger.name" : "Tűzfal",
+	"spell.core.firstAid.name" : "Elsősegély",
+	"spell.core.fortressMoat.name" : "Forró kátrány",
+	"spell.core.fortressMoatTrigger.name" : "Forró kátrány",
+	"spell.core.infernoMoat.name" : "Láva",
+	"spell.core.infernoMoatTrigger.name" : "Láva",
+	"spell.core.landMineTrigger.name" : "Aknamező",
+	"spell.core.necropolisMoat.name" : "Csontvázudvar",
+	"spell.core.necropolisMoatTrigger.name" : "Csontvázudvar",
+	"spell.core.rampartMoat.name" : "Tövisek",
+	"spell.core.rampartMoatTrigger.name" : "Tövisek",
+	"spell.core.strongholdMoat.name" : "Fa tüskék",
+	"spell.core.strongholdMoatTrigger.name" : "Fa tüskék",
+	"spell.core.summonDemons.name" : "Démonok idézése",
+	"spell.core.towerMoat.name" : "Aknamező"
+}

+ 1 - 0
Mods/vcmi/Content/config/ukrainian.json

@@ -199,6 +199,7 @@
 	"vcmi.lobby.preview.error.invite" : "Ви не були запрошені до цієї кімнати.",
 	"vcmi.lobby.preview.error.mods" : "Ви використовуєте інший набір модифікацій.",
 	"vcmi.lobby.preview.error.version" : "Ви використовуєте іншу версію VCMI.",
+	"vcmi.lobby.channel.add" : "Додати канал чату",
 	"vcmi.lobby.room.new" : "Нова гра",
 	"vcmi.lobby.room.load" : "Завантажити гру",
 	"vcmi.lobby.room.type" : "Тип кімнати",

+ 120 - 120
Mods/vcmi/Content/config/vietnamese.json

@@ -76,7 +76,7 @@
 	
 	"vcmi.randomMap.description" : "Bản đồ được tạo ngẫu nhiên.\nMẫu là %s, kích cỡ %dx%d, cấp %d, người chơi %d, máy %d, nước %s, quái vật %s, bản đồ VCMI",
 	"vcmi.randomMap.description.isHuman" : ", người chơi %s",
-	"vcmi.randomMap.description.townChoice" : ", %s town choice is %s",
+	"vcmi.randomMap.description.townChoice" : ", %s chọn thành %s",
 	"vcmi.randomMap.description.water.none" : "không",
 	"vcmi.randomMap.description.water.normal" : "bình thường",
 	"vcmi.randomMap.description.water.islands" : "đảo",
@@ -101,130 +101,130 @@
 	"vcmi.mainMenu.hostTCP" : "Chủ phòng TCP/IP",
 	"vcmi.mainMenu.joinTCP" : "Tham gia TCP/IP",
 
-	"vcmi.lobby.filepath" : "File path",
-	"vcmi.lobby.creationDate" : "Creation date",
-	"vcmi.lobby.scenarioName" : "Scenario name",
-	"vcmi.lobby.mapPreview" : "Map preview",
-	"vcmi.lobby.noPreview" : "no preview",
-	"vcmi.lobby.noUnderground" : "no underground",
-	"vcmi.lobby.sortDate" : "Sorts maps by change date",
-	"vcmi.lobby.backToLobby" : "Return to lobby",
-	"vcmi.lobby.author" : "Author",
-	"vcmi.lobby.handicap" : "Handicap",
-	"vcmi.lobby.handicap.resource" : "Gives players appropriate resources to start with in addition to the normal starting resources. Negative values are allowed, but are limited to 0 in total (the player never starts with negative resources).",
-	"vcmi.lobby.handicap.income" : "Changes the player's various incomes by the percentage. Is rounded up.",
-	"vcmi.lobby.handicap.growth" : "Changes the growth rate of creatures in the towns owned by the player. Is rounded up.",
-	"vcmi.lobby.deleteUnsupportedSave" : "{Unsupported saves found}\n\nVCMI has found %d saved games that are no longer supported, possibly due to differences in VCMI versions.\n\nDo you want to delete them?",
-	"vcmi.lobby.deleteSaveGameTitle" : "Select a Saved Game to delete",
-	"vcmi.lobby.deleteMapTitle" : "Select a Scenario to delete",
-	"vcmi.lobby.deleteFile" : "Do you want to delete following file?",
-	"vcmi.lobby.deleteFolder" : "Do you want to delete following folder?",
-	"vcmi.lobby.deleteMode" : "Switch to delete mode and back",
-
-	"vcmi.broadcast.failedLoadGame" : "Failed to load game",
-	"vcmi.broadcast.command" : "Use '!help' to list available commands",
-	"vcmi.broadcast.simturn.end" : "Simultaneous turns have ended",
-	"vcmi.broadcast.simturn.endBetween" : "Simultaneous turns between players %s and %s have ended",
-	"vcmi.broadcast.serverProblem" : "Server encountered a problem",
-	"vcmi.broadcast.gameTerminated" : "game was terminated",
-	"vcmi.broadcast.gameSavedAs" : "game saved as",
-	"vcmi.broadcast.noCheater" : "No cheaters registered!",
-	"vcmi.broadcast.playerCheater" : "Player %s is cheater!",
-	"vcmi.broadcast.statisticFile" : "Statistic files can be found in %s directory",
-	"vcmi.broadcast.help.commands" : "Available commands to host:",
-	"vcmi.broadcast.help.exit" : "'!exit' - immediately ends current game",
-	"vcmi.broadcast.help.kick" : "'!kick <player>' - kick specified player from the game",
-	"vcmi.broadcast.help.save" : "'!save <filename>' - save game under specified filename",
-	"vcmi.broadcast.help.statistic" : "'!statistic' - save game statistics as csv file",
-	"vcmi.broadcast.help.commandsAll" : "Available commands to all players:",
-	"vcmi.broadcast.help.help" : "'!help' - display this help",
-	"vcmi.broadcast.help.cheaters" : "'!cheaters' - list players that entered cheat command during game",
-	"vcmi.broadcast.help.vote" : "'!vote' - allows to change some game settings if all players vote for it",
-	"vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - allow simultaneous turns for specified number of days, or until contact",
-	"vcmi.broadcast.vote.force" : "'!vote simturns force X' - force simultaneous turns for specified number of days, blocking player contacts",
-	"vcmi.broadcast.vote.abort" : "'!vote simturns abort' - abort simultaneous turns once this turn ends",
-	"vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - prolong base timer for all players by specified number of seconds",
-	"vcmi.broadcast.vote.noActive" : "No active voting!",
+	"vcmi.lobby.filepath" : "Nơi lưu",
+	"vcmi.lobby.creationDate" : "Ngày tạo",
+	"vcmi.lobby.scenarioName" : "Tên bản đồ",
+	"vcmi.lobby.mapPreview" : "Xem trước bản đồ",
+	"vcmi.lobby.noPreview" : "không xem trước",
+	"vcmi.lobby.noUnderground" : "không có lòng đất",
+	"vcmi.lobby.sortDate" : "Sắp xếp bản đồ theo ngày sửa đổi",
+	"vcmi.lobby.backToLobby" : "Quay lại sảnh",
+	"vcmi.lobby.author" : "Tác giả",
+	"vcmi.lobby.handicap" : "Chấp",
+	"vcmi.lobby.handicap.resource" : "Ngoài các nguồn tài nguyên ban đầu người chơi sẽ được cung cấp thêm các tài nguyên phù hợp. Giới hạn ở mức 0 nên người chơi không khởi đầu với tài nguyên âm.",
+	"vcmi.lobby.handicap.income" : "Thay đổi thu nhập khác nhau của người chơi theo phần trăm. Được làm tròn lên.",
+	"vcmi.lobby.handicap.growth" : "Thay đổi mức sinh trưởng của quân ở trong thành do người chơi sở hữu. Được làm tròn lên.",
+	"vcmi.lobby.deleteUnsupportedSave" : "{Bản lưu không hợp lệ}\n\nVCMI tìm thấy tệp tin %d không tương thích, có thể là do sự khác biệt trong các phiên bản VCMI.\n\nBạn có muốn xóa chúng không?",
+	"vcmi.lobby.deleteSaveGameTitle" : "Chọn tập tin đã lưu để xóa",
+	"vcmi.lobby.deleteMapTitle" : "Chọn một bản đồ để xóa",
+	"vcmi.lobby.deleteFile" : "Bạn có muốn xóa tập tin này không?",
+	"vcmi.lobby.deleteFolder" : "Bạn có muốn xóa thư mục này không?",
+	"vcmi.lobby.deleteMode" : "Chuyển sang chế độ xóa và trở lại",
+
+	"vcmi.broadcast.failedLoadGame" : "Không tải được trò chơi",
+	"vcmi.broadcast.command" : "Sử dụng lệnh '!help' để xem danh sách các lệnh có sẵn",
+	"vcmi.broadcast.simturn.end" : "Lượt đi cùng lúc đã kết thúc!",
+	"vcmi.broadcast.simturn.endBetween" : "Lượt đi cùng lúc của %s và %s đã kết thúc!",
+	"vcmi.broadcast.serverProblem" : "Máy chủ gặp sự cố",
+	"vcmi.broadcast.gameTerminated" : "trò chơi đã kết thúc",
+	"vcmi.broadcast.gameSavedAs" : "lưu với...",
+	"vcmi.broadcast.noCheater" : "Không có mã gian lận!",
+	"vcmi.broadcast.playerCheater" : "Người chơi %s dùng mã gian lận!",
+	"vcmi.broadcast.statisticFile" : "Các tập tin thống kê được lưu trong thư mục %s",
+	"vcmi.broadcast.help.commands" : "Các lệnh có sẵn cho máy chủ:",
+	"vcmi.broadcast.help.exit" : "'!exit' - kết thúc trò chơi hiện tại",
+	"vcmi.broadcast.help.kick" : "'!kick <player>' - đuổi người chơi ra khỏi trò chơi",
+	"vcmi.broadcast.help.save" : "'!save <filename>' - lưu trò chơi với tên được chỉ định",
+	"vcmi.broadcast.help.statistic" : "'!statistic' - lưu số liệu thống kê của trò chơi",
+	"vcmi.broadcast.help.commandsAll" : "Lệnh cho tất cả người chơi:",
+	"vcmi.broadcast.help.help" : "'!help' - hiện trợ giúp về các lệnh",
+	"vcmi.broadcast.help.cheaters" : "'!cheaters' - hiện danh sách người chơi đã sử dụng mã gian lận",
+	"vcmi.broadcast.help.vote" : "'!vote' - thay đổi các cài đặt trong trò chơi nếu tất cả người chơi vote",
+	"vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - chơi cùng lượt trong số ngày được chỉ định hoặc cho đến khi xâm chiếm",
+	"vcmi.broadcast.vote.force" : "'!vote simturns force X' - bắt buộc chơi cùng lượt trong số ngày được chỉ định, cấm xâm chiếm",
+	"vcmi.broadcast.vote.abort" : "'!vote simturns abort' - hủy lượt đi cùng lúc sau khi lượt này kết thúc",
+	"vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - thêm thời gian đếm ngược cho tất cả người chơi theo số giây được chỉ định",
+	"vcmi.broadcast.vote.noActive" : "Không chấp nhận vote!",
 	"vcmi.broadcast.vote.yes" : "yes",
 	"vcmi.broadcast.vote.no" : "no",
-	"vcmi.broadcast.vote.notRecognized" : "Voting command not recognized!",
-	"vcmi.broadcast.vote.success.untilContacts" : "Voting successful. Simultaneous turns will run for %s more days, or until contact",
-	"vcmi.broadcast.vote.success.contactsBlocked" : "Voting successful. Simultaneous turns will run for %s more days. Contacts are blocked",
-	"vcmi.broadcast.vote.success.nextDay" : "Voting successful. Simultaneous turns will end on next day",
-	"vcmi.broadcast.vote.success.timer" : "Voting successful. Timer for all players has been prolonger for %s seconds",
-	"vcmi.broadcast.vote.aborted" : "Player voted against change. Voting aborted",
-	"vcmi.broadcast.vote.start.untilContacts" : "Started voting to allow simultaneous turns for %s more days",
-	"vcmi.broadcast.vote.start.contactsBlocked" : "Started voting to force simultaneous turns for %s more days",
-	"vcmi.broadcast.vote.start.nextDay" : "Started voting to end simultaneous turns starting from next day",
-	"vcmi.broadcast.vote.start.timer" : "Started voting to prolong timer for all players by %s seconds",
-	"vcmi.broadcast.vote.hint" : "Type '!vote yes' to agree to this change or '!vote no' to vote against it",
+	"vcmi.broadcast.vote.notRecognized" : "Lệnh vote không được chấp nhận!",
+	"vcmi.broadcast.vote.success.untilContacts" : "Đồng ý vote. Chơi cùng lượt thêm %s ngày nữa hoặc cho đến khi xâm chiếm",
+	"vcmi.broadcast.vote.success.contactsBlocked" : "Đồng ý vote. Bắt buộc chơi cùng lượt thêm %s ngày nữa. Cấm xâm chiếm",
+	"vcmi.broadcast.vote.success.nextDay" : "Đồng ý vote. Chơi cùng lượt sẽ kết thúc vào ngày hôm sau",
+	"vcmi.broadcast.vote.success.timer" : "Đồng ý vote. Thêm thời gian %s giây cho tất cả người chơi",
+	"vcmi.broadcast.vote.aborted" : "Người chơi không đồng ý vote. Kết thúc vote!",
+	"vcmi.broadcast.vote.start.untilContacts" : "Vote để chơi cùng lượt thêm %s ngày nữa",
+	"vcmi.broadcast.vote.start.contactsBlocked" : "Vote để bắt buộc chơi cùng lượt thêm %s ngày nữa",
+	"vcmi.broadcast.vote.start.nextDay" : "Vote để kết thúc lượt đi cùng lúc sau ngay hôm nay",
+	"vcmi.broadcast.vote.start.timer" : "Vote thêm thời gian %s giây cho tất cả người chơi",
+	"vcmi.broadcast.vote.hint" : "Nhập vào '!vote yes' để đồng ý vote hoặc '!vote no' để từ chối vote",
 		
-	"vcmi.lobby.login.title" : "VCMI Online Lobby",
-	"vcmi.lobby.login.username" : "Username:",
-	"vcmi.lobby.login.connecting" : "Connecting...",
-	"vcmi.lobby.login.error" : "Connection error: %s",
-	"vcmi.lobby.login.create" : "New Account",
-	"vcmi.lobby.login.login" : "Login",
-	"vcmi.lobby.login.as" : "Login as %s",
-	"vcmi.lobby.login.spectator" : "Spectator",
-	"vcmi.lobby.header.rooms" : "Game Rooms - %d",
-	"vcmi.lobby.header.channels" : "Chat Channels",
-	"vcmi.lobby.header.chat.global" : "Global Game Chat - %s", // %s -> language name
-	"vcmi.lobby.header.chat.match" : "Chat from previous game on %s", // %s -> game start date & time
-	"vcmi.lobby.header.chat.player" : "Private chat with %s", // %s -> nickname of another player
-	"vcmi.lobby.header.history" : "Your Previous Games",
-	"vcmi.lobby.header.players" : "Players Online - %d",
-	"vcmi.lobby.match.solo" : "Singleplayer Game",
-	"vcmi.lobby.match.duel" : "Game with %s", // %s -> nickname of another player
-	"vcmi.lobby.match.multi" : "%d players",
-	"vcmi.lobby.room.create" : "Create New Room",
-	"vcmi.lobby.room.players.limit" : "Players Limit",
-	"vcmi.lobby.room.description.public" : "Any player can join public room.",
-	"vcmi.lobby.room.description.private" : "Only invited players can join private room.",
-	"vcmi.lobby.room.description.new" : "To start the game, select a scenario or set up a random map.",
-	"vcmi.lobby.room.description.load" : "To start the game, use one of your saved games.",
-	"vcmi.lobby.room.description.limit" : "Up to %d players can enter your room, including you.",
-	"vcmi.lobby.invite.header" : "Invite Players",
-	"vcmi.lobby.invite.notification" : "Player has invited you to their game room. You can now join their private room.",
-	"vcmi.lobby.preview.title" : "Join Game Room",
-	"vcmi.lobby.preview.subtitle" : "Game on %s, hosted by %s", //TL Note: 1) name of map or RMG template 2) nickname of game host
-	"vcmi.lobby.preview.version" : "Game version:",
-	"vcmi.lobby.preview.players" : "Players:",
-	"vcmi.lobby.preview.mods" : "Used mods:",
-	"vcmi.lobby.preview.allowed" : "Join the game room?",
-	"vcmi.lobby.preview.error.header" : "Unable to join this room.",
-	"vcmi.lobby.preview.error.playing" : "You need to leave your current game first.",
-	"vcmi.lobby.preview.error.full" : "The room is already full.",
-	"vcmi.lobby.preview.error.busy" : "The room no longer accepts new players.",
-	"vcmi.lobby.preview.error.invite" : "You were not invited to this room.",
-	"vcmi.lobby.preview.error.mods" : "You are using different set of mods.",
-	"vcmi.lobby.preview.error.version" : "You are using different version of VCMI.",
-	"vcmi.lobby.room.new" : "New Game",
-	"vcmi.lobby.room.load" : "Load Game",
-	"vcmi.lobby.room.type" : "Room Type",
-	"vcmi.lobby.room.mode" : "Game Mode",
-	"vcmi.lobby.room.state.public" : "Public",
-	"vcmi.lobby.room.state.private" : "Private",
-	"vcmi.lobby.room.state.busy" : "In Game",
-	"vcmi.lobby.room.state.invited" : "Invited",
-	"vcmi.lobby.mod.state.compatible" : "Compatible",
-	"vcmi.lobby.mod.state.disabled" : "Must be enabled",
-	"vcmi.lobby.mod.state.version" : "Version mismatch",
-	"vcmi.lobby.mod.state.excessive" : "Must be disabled",
-	"vcmi.lobby.mod.state.missing" : "Not installed",
+	"vcmi.lobby.login.title" : "Chơi VCMI Online",
+	"vcmi.lobby.login.username" : "Tên đăng nhập:",
+	"vcmi.lobby.login.connecting" : "Đang kết nối...",
+	"vcmi.lobby.login.error" : "Lỗi kết nối: %s",
+	"vcmi.lobby.login.create" : "Tài khoản mới",
+	"vcmi.lobby.login.login" : "Đăng nhập",
+	"vcmi.lobby.login.as" : "Đăng nhập với %s",
+	"vcmi.lobby.login.spectator" : "Người xem",
+	"vcmi.lobby.header.rooms" : "Số phòng chơi - %d",
+	"vcmi.lobby.header.channels" : "Chọn Ngôn Ngữ",
+	"vcmi.lobby.header.chat.global" : "Trò chuyện trực tuyến - %s", // %s -> language name
+	"vcmi.lobby.header.chat.match" : "Chat từ ván chơi trước %s", // %s -> game start date & time
+	"vcmi.lobby.header.chat.player" : "Chat riêng với %s", // %s -> nickname of another player
+	"vcmi.lobby.header.history" : "Ván Chơi Trước",
+	"vcmi.lobby.header.players" : "Số người chơi - %d",
+	"vcmi.lobby.match.solo" : "Chơi Đơn",
+	"vcmi.lobby.match.duel" : "Chơi với %s", // %s -> nickname of another player
+	"vcmi.lobby.match.multi" : "người chơi %d",
+	"vcmi.lobby.room.create" : "Tạo Phòng Mới",
+	"vcmi.lobby.room.players.limit" : "Số Người Chơi",
+	"vcmi.lobby.room.description.public" : "Người chơi có thể vào phòng công khai.",
+	"vcmi.lobby.room.description.private" : "Người chơi được mời mới có thể vào phòng riêng tư.",
+	"vcmi.lobby.room.description.new" : "Hãy chọn một bản đồ có sẵn hoặc tạo một bản đồ ngẫu nhiên để bắt đầu chơi.",
+	"vcmi.lobby.room.description.load" : "Hãy chọn một bản đồ mà bạn đã lưu để bắt đầu chơi.",
+	"vcmi.lobby.room.description.limit" : "Tối đa %d người chơi có thể vào phòng này, tính cả bạn.",
+	"vcmi.lobby.invite.header" : "Mời người chơi",
+	"vcmi.lobby.invite.notification" : "Người chơi đã mời bạn vào phòng chơi riêng tư của họ.",
+	"vcmi.lobby.preview.title" : "Vào Phòng Chơi",
+	"vcmi.lobby.preview.subtitle" : "Tên %s, chủ phòng %s", //TL Note: 1) name of map or RMG template 2) nickname of game host
+	"vcmi.lobby.preview.version" : "Phiên bản:",
+	"vcmi.lobby.preview.players" : "người chơi:",
+	"vcmi.lobby.preview.mods" : "Sử dụng các mod sau:",
+	"vcmi.lobby.preview.allowed" : "Vào phòng chơi này?",
+	"vcmi.lobby.preview.error.header" : "Không thể vào phòng chơi này.",
+	"vcmi.lobby.preview.error.playing" : "Bạn phải thoát ra khỏi phòng chơi.",
+	"vcmi.lobby.preview.error.full" : "Phòng này đã đầy rồi.",
+	"vcmi.lobby.preview.error.busy" : "Phòng không nhận người chơi mới.",
+	"vcmi.lobby.preview.error.invite" : "Bạn không được mời vào phòng này.",
+	"vcmi.lobby.preview.error.mods" : "Bạn đang sử dụng một bộ mod khác.",
+	"vcmi.lobby.preview.error.version" : "Bạn đang sử dụng phiên bản VCMI khác.",
+	"vcmi.lobby.room.new" : "Chơi Mới",
+	"vcmi.lobby.room.load" : "Tải Lại",
+	"vcmi.lobby.room.type" : "Loại Phòng",
+	"vcmi.lobby.room.mode" : "Chế Độ Chơi",
+	"vcmi.lobby.room.state.public" : "Công khai",
+	"vcmi.lobby.room.state.private" : "Riêng tư",
+	"vcmi.lobby.room.state.busy" : "Đang chơi",
+	"vcmi.lobby.room.state.invited" : "Được mời",
+	"vcmi.lobby.mod.state.compatible" : "Tương thích",
+	"vcmi.lobby.mod.state.disabled" : "Phải được bật",
+	"vcmi.lobby.mod.state.version" : "Không tương thích",
+	"vcmi.lobby.mod.state.excessive" : "Phải bị tắt",
+	"vcmi.lobby.mod.state.missing" : "Chưa cài đặt",
 	"vcmi.lobby.pvp.coin.hover" : "Coin",
 	"vcmi.lobby.pvp.coin.help" : "Flips a coin",
 	"vcmi.lobby.pvp.randomTown.hover" : "Random town",
-	"vcmi.lobby.pvp.randomTown.help" : "Write a random town in the chat",
+	"vcmi.lobby.pvp.randomTown.help" : "Gửi một thành ngẫu nhiên trong khung chat",
 	"vcmi.lobby.pvp.randomTownVs.hover" : "Random town vs.",
-	"vcmi.lobby.pvp.randomTownVs.help" : "Write two random towns in the chat",
+	"vcmi.lobby.pvp.randomTownVs.help" : "Gửi hai thành ngẫu nhiên trong khung chat",
 	"vcmi.lobby.pvp.versus" : "vs.",
 
-	"vcmi.client.errors.invalidMap" : "{Bản đồ hoặc chiến dịch không hợp lệ}\n\nKhông thể bắt đầu trò chơi! Bản đồ hoặc chiến dịch đã chọn có thể không hợp lệ hoặc bị lỗi. Như sau:\n%s",
+	"vcmi.client.errors.invalidMap" : "{Bản đồ hoặc chiến dịch không hợp lệ}\n\nKhông thể bắt đầu trò chơi! Bản đồ hoặc chiến dịch đã chọn có thể không hợp lệ hoặc bị lỗi như sau:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{Thiếu tệp tin dữ liệu}\n\nKhông tìm thấy tệp tin dữ liệu của chiến dịch! Có thể bạn đang sử dụng các tệp tin dữ liệu Heroes 3 bị thiếu hoặc bị lỗi. Hãy thử cài đặt lại trò chơi.",
-	"vcmi.client.errors.modLoadingFailure" : "{Lỗi tải mod}\n\nĐã phát hiện ra lỗi nghiêm trọng khi tải mod! Trò chơi có thể không hoạt động chính xác hoặc bị văng! Hãy cập nhật hoặc tắt hóa các mod sau:\n\n",
+	"vcmi.client.errors.modLoadingFailure" : "{Lỗi tải mod}\n\nĐã phát hiện ra lỗi nghiêm trọng khi tải mod! Trò chơi có thể không hoạt động chính xác hoặc bị văng! Hãy cập nhật hoặc tắt các mod sau:\n\n",
 	"vcmi.server.errors.disconnected" : "{Mạng bị lỗi}\n\nĐã mất kết nối tới máy chủ trò chơi!",
-	"vcmi.server.errors.playerLeft" : "{Người chơi}\n\n%s đã ngắt kết nối khỏi trò chơi!", //%s -> player color
+	"vcmi.server.errors.playerLeft" : "{Người chơi}\n\n%s đã ngắt kết nối khỏi trò chơi!", //%s -> màu của người chơi
 	"vcmi.server.errors.existingProcess" : "Một chương trình máy chủ VCMI khác đang chạy. Hãy đóng nó trước khi bắt đầu một trò chơi mới.",
 	"vcmi.server.errors.modsToEnable"    : "{Các mod sau đây là bắt buộc}",
 	"vcmi.server.errors.modsToDisable"   : "{Bạn phải tắt các mod sau đây}",
@@ -243,7 +243,7 @@
 
 	"vcmi.systemOptions.videoGroup" : "Thiết lập phim ảnh",
 	"vcmi.systemOptions.audioGroup" : "Thiết lập âm thanh",
-	"vcmi.systemOptions.otherGroup" : "Thiết lập khác", // unused right now
+	"vcmi.systemOptions.otherGroup" : "Thiết lập khác", // chưa sử dụng
 	"vcmi.systemOptions.townsGroup" : "Thành phố",
 
 	"vcmi.statisticWindow.statistics" : "Thống Kê",
@@ -375,8 +375,8 @@
 	"vcmi.battleWindow.damageEstimation.shots.1": "Còn %d lần",
 	"vcmi.battleWindow.damageEstimation.damage": "%d sát thương",
 	"vcmi.battleWindow.damageEstimation.damage.1": "%d sát thương",
-	"vcmi.battleWindow.damageEstimation.kills": "%d sẽ bị diệt",
-	"vcmi.battleWindow.damageEstimation.kills.1": "%d sẽ bị diệt",
+	"vcmi.battleWindow.damageEstimation.kills": "%d sẽ bị giết",
+	"vcmi.battleWindow.damageEstimation.kills.1": "%d sẽ bị giết",
 	
 	"vcmi.battleWindow.damageRetaliation.will": "Sẽ phản đòn ",
 	"vcmi.battleWindow.damageRetaliation.may": "Có thể phản đòn ",
@@ -469,10 +469,10 @@
 	"vcmi.optionsTab.simturnsTitle" : "Lượt đi cùng lúc",
 	"vcmi.optionsTab.simturnsMin.hover" : "Tối thiểu là",
 	"vcmi.optionsTab.simturnsMax.hover" : "Tối đa là",
-	"vcmi.optionsTab.simturnsAI.hover" : "(Thử nghiệm) Lượt chơi AI cùng lúc",
+	"vcmi.optionsTab.simturnsAI.hover" : "(Thử nghiệm) Lượt đi cùng lúc của AI",
 	"vcmi.optionsTab.simturnsMin.help" : "Chơi cùng lượt trong số ngày được chỉ định. Cấm các hành vi thù địch giữa những người chơi trong thời gian này.",
 	"vcmi.optionsTab.simturnsMax.help" : "Chơi cùng lượt trong số ngày được chỉ định hoặc cho đến khi thực hiện hành vi thù địch với người chơi khác.",
-	"vcmi.optionsTab.simturnsAI.help" : "{Lượt đi của AI cùng lúc}\nTùy chọn Thử nghiệm. AI sẽ thực hiện các hành động cùng lúc với người chơi khi bật chế độ lượt đi cùng lúc.",
+	"vcmi.optionsTab.simturnsAI.help" : "{Lượt đi cùng lúc của AI}\nTùy chọn thử nghiệm AI sẽ thực hiện các hành động cùng lúc với người chơi khi bật chế độ lượt đi cùng lúc.",
 
 	"vcmi.optionsTab.turnTime.select"     : "Chọn cài đặt giờ cho lượt",
 	"vcmi.optionsTab.turnTime.unlimited"  : "Không giới hạn thời gian",
@@ -639,7 +639,7 @@
 	"core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Đòn chí mạng",
 	"core.bonus.DOUBLE_DAMAGE_CHANCE.description": "Có ${val}% cơ hội gây sát thương gấp đôi khi đánh",
 	"core.bonus.DRAGON_NATURE.name": "Rồng",
-	"core.bonus.DRAGON_NATURE.description": "Quân có thuộc tính Rồng",
+	"core.bonus.DRAGON_NATURE.description": "Quân có thuộc tính của loài Rồng",
 	"core.bonus.EARTH_IMMUNITY.name": "Kháng đất",
 	"core.bonus.EARTH_IMMUNITY.description": "Kháng tất cả phép đất trong trường học phép thuật",
 	"core.bonus.ENCHANTER.name": "Niệm phép",

+ 17 - 8
Mods/vcmi/mod.json

@@ -1,6 +1,14 @@
 {
 	"name" : "VCMI essential files",
 	"description" : "Essential files required for VCMI to run correctly",
+	"version" : "1.5",
+	"author" : "VCMI Team",
+	"contact" : "http://forum.vcmi.eu/index.php",
+	"modType" : "Graphical",
+
+	"factions" : [ "config/towerFactions" ],
+	"creatures" : [ "config/towerCreature" ],
+	"spells" : [ "config/spells" ],
 
 	"chinese" : {
 		"name" : "VCMI essential files",
@@ -120,14 +128,15 @@
 		]
 	},
 
-	"version" : "1.5",
-	"author" : "VCMI Team",
-	"contact" : "http://forum.vcmi.eu/index.php",
-	"modType" : "Graphical",
-
-	"factions" : [ "config/towerFactions" ],
-	"creatures" : [ "config/towerCreature" ],
-	"spells" : [ "config/spells" ],
+	"hungarian" : {
+		"name" : "VCMI - alapvető fájlok",
+		"description" : "Alapvető fájlok a VCMI helyes működéséhez",
+		"author" : "VCMI csapat",
+		"skipValidation" : true,
+		"translations" : [
+			"config/hungarian.json"
+		]
+	},
 
 	"translations" : [
 		"config/english.json"

+ 48 - 0
android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/FileUtil.java

@@ -1,18 +1,25 @@
 package eu.vcmi.vcmi.util;
 
 import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
 import android.net.Uri;
+import android.provider.OpenableColumns;
 
 import androidx.annotation.Nullable;
 import androidx.documentfile.provider.DocumentFile;
 
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.lang.Exception;
 import java.util.List;
 
+import eu.vcmi.vcmi.Const;
 import eu.vcmi.vcmi.Storage;
 
 /**
@@ -104,4 +111,45 @@ public class FileUtil
             target.write(buffer, 0, read);
         }
     }
+
+    @SuppressWarnings(Const.JNI_METHOD_SUPPRESS)
+    private static void copyFileFromUri(String sourceFileUri, String destinationFile, Context context)
+    {
+        try
+        {
+            final InputStream inputStream = new FileInputStream(context.getContentResolver().openFileDescriptor(Uri.parse(sourceFileUri), "r").getFileDescriptor());
+            final OutputStream outputStream = new FileOutputStream(new File(destinationFile));
+
+            copyStream(inputStream, outputStream);
+        }
+        catch (IOException e)
+        {
+            Log.e("copyFileFromUri failed: ", e);
+        }
+    }
+
+    @SuppressWarnings(Const.JNI_METHOD_SUPPRESS)
+    private static String getFilenameFromUri(String sourceFileUri, Context context)
+    {
+        String fileName = "";
+        try
+        {
+            ContentResolver contentResolver = context.getContentResolver();
+            Cursor cursor = contentResolver.query(Uri.parse(sourceFileUri), null, null, null, null);
+
+            if (cursor != null && cursor.moveToFirst()) {
+                int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+                if (nameIndex != -1) {
+                    fileName = cursor.getString(nameIndex);
+                }
+                cursor.close();
+            }
+        }
+        catch (Exception e)
+        {
+            Log.e("getFilenameFromUri failed: ", e);
+        }
+
+        return fileName;
+    }
 }

+ 2 - 1
android/vcmi-app/src/main/res/values-cs/strings.xml

@@ -1,4 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-    <string name="server_name">Server VCMI</string>
+    <string name="server_name">VCMI Server</string>
+    <string name="shortcut_play">Hrát VCMI</string>
 </resources>

+ 10 - 5
client/CMakeLists.txt

@@ -88,6 +88,7 @@ set(vcmiclientcommon_SRCS
 	render/CBitmapHandler.cpp
 	render/CDefFile.cpp
 	render/Canvas.cpp
+	render/CanvasImage.cpp
 	render/ColorFilter.cpp
 	render/Colors.cpp
 	render/Graphics.cpp
@@ -99,14 +100,16 @@ set(vcmiclientcommon_SRCS
 	renderSDL/CursorHardware.cpp
 	renderSDL/CursorSoftware.cpp
 	renderSDL/FontChain.cpp
-	renderSDL/ImageScaled.cpp
+	renderSDL/ScalableImage.cpp
 	renderSDL/RenderHandler.cpp
 	renderSDL/SDLImage.cpp
 	renderSDL/SDLImageLoader.cpp
+	renderSDL/SDLImageScaler.cpp
 	renderSDL/SDLRWwrapper.cpp
 	renderSDL/ScreenHandler.cpp
 	renderSDL/SDL_Extensions.cpp
 
+	globalLobby/GlobalLobbyAddChannelWindow.cpp
 	globalLobby/GlobalLobbyClient.cpp
 	globalLobby/GlobalLobbyInviteWindow.cpp
 	globalLobby/GlobalLobbyLoginWindow.cpp
@@ -290,6 +293,7 @@ set(vcmiclientcommon_HEADERS
 	render/CBitmapHandler.h
 	render/CDefFile.h
 	render/Canvas.h
+	render/CanvasImage.h
 	render/ColorFilter.h
 	render/Colors.h
 	render/EFont.h
@@ -307,10 +311,11 @@ set(vcmiclientcommon_HEADERS
 	renderSDL/CursorHardware.h
 	renderSDL/CursorSoftware.h
 	renderSDL/FontChain.h
-	renderSDL/ImageScaled.h
+	renderSDL/ScalableImage.h
 	renderSDL/RenderHandler.h
 	renderSDL/SDLImage.h
 	renderSDL/SDLImageLoader.h
+	renderSDL/SDLImageScaler.h
 	renderSDL/SDLRWwrapper.h
 	renderSDL/ScreenHandler.h
 	renderSDL/SDL_Extensions.h
@@ -318,6 +323,7 @@ set(vcmiclientcommon_HEADERS
 
 	globalLobby/GlobalLobbyClient.h
 	globalLobby/GlobalLobbyDefines.h
+	globalLobby/GlobalLobbyAddChannelWindow.h
 	globalLobby/GlobalLobbyInviteWindow.h
 	globalLobby/GlobalLobbyLoginWindow.h
 	globalLobby/GlobalLobbyRoomWindow.h
@@ -470,12 +476,11 @@ target_link_libraries(vcmiclientcommon PUBLIC
 		vcmi SDL2::SDL2 SDL2::Image SDL2::Mixer SDL2::TTF
 )
 
-if(ffmpeg_LIBRARIES)
+if(ENABLE_VIDEO)
+	target_compile_definitions(vcmiclientcommon PRIVATE ENABLE_VIDEO)
 	target_link_libraries(vcmiclientcommon PRIVATE
 		${ffmpeg_LIBRARIES}
 	)
-else()
-	target_compile_definitions(vcmiclientcommon PRIVATE DISABLE_VIDEO)
 endif()
 
 target_include_directories(vcmiclientcommon PUBLIC

+ 23 - 4
client/CPlayerInterface.cpp

@@ -97,6 +97,8 @@
 #include "../lib/networkPacks/PacksForServer.h"
 
 #include "../lib/pathfinder/CGPathNode.h"
+#include "../lib/pathfinder/PathfinderCache.h"
+#include "../lib/pathfinder/PathfinderOptions.h"
 
 #include "../lib/serializer/CTypeList.h"
 #include "../lib/serializer/ESerializationVersion.h"
@@ -156,17 +158,29 @@ CPlayerInterface::~CPlayerInterface()
 	if (LOCPLINT == this)
 		LOCPLINT = nullptr;
 }
+
 void CPlayerInterface::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB)
 {
 	cb = CB;
 	env = ENV;
 
+	pathfinderCache = std::make_unique<PathfinderCache>(cb.get(), PathfinderOptions(cb.get()));
 	CCS->musich->loadTerrainMusicThemes();
 	initializeHeroTownList();
 
 	adventureInt.reset(new AdventureMapInterface());
 }
 
+std::shared_ptr<const CPathsInfo> CPlayerInterface::getPathsInfo(const CGHeroInstance * h)
+{
+	return pathfinderCache->getPathsInfo(h);
+}
+
+void CPlayerInterface::invalidatePaths()
+{
+	pathfinderCache->invalidatePaths();
+}
+
 void CPlayerInterface::closeAllDialogs()
 {
 	// remove all active dialogs that do not expect query answer
@@ -182,6 +196,9 @@ void CPlayerInterface::closeAllDialogs()
 		if(infoWindow && infoWindow->ID != QueryID::NONE)
 			break;
 
+		if (topWindow == nullptr)
+			throw std::runtime_error("Invalid or non-existing top window! Total windows: " + std::to_string(GH.windows().count()));
+
 		topWindow->close();
 	}
 }
@@ -464,6 +481,8 @@ void CPlayerInterface::heroSecondarySkillChanged(const CGHeroInstance * hero, in
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	for (auto cuw : GH.windows().findWindows<IMarketHolder>())
 		cuw->updateSecondarySkills();
+
+	localState->verifyPath(hero);
 }
 
 void CPlayerInterface::heroManaPointsChanged(const CGHeroInstance * hero)
@@ -580,6 +599,8 @@ void CPlayerInterface::garrisonsChanged(std::vector<const CArmedInstance *> objs
 
 		if (hero)
 		{
+			localState->verifyPath(hero);
+
 			adventureInt->onHeroChanged(hero);
 			if(hero->inTownGarrison && hero->visitedTown != town)
 				adventureInt->onTownChanged(hero->visitedTown);
@@ -607,7 +628,6 @@ void CPlayerInterface::buildChanged(const CGTownInstance *town, BuildingID build
 			switch(what)
 			{
 			case 1:
-				CCS->soundh->playSound(soundBase::newBuilding);
 				castleInt->addBuilding(buildingID);
 				break;
 			case 2:
@@ -846,7 +866,7 @@ void CPlayerInterface::battleLogMessage(const BattleID & battleID, const std::ve
 	battleInt->displayBattleLog(lines);
 }
 
-void CPlayerInterface::battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport)
+void CPlayerInterface::battleStackMoved(const BattleID & battleID, const CStack * stack, const BattleHexArray & dest, int distance, bool teleport)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
@@ -1150,7 +1170,7 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component
 		if(t)
 		{
 			auto image = GH.renderHandler().loadImage(AnimationPath::builtin("ITPA"), t->getTown()->clientInfo.icons[t->hasFort()][false] + 2, 0, EImageBlitMode::OPAQUE);
-			image->scaleTo(Point(35, 23));
+			image->scaleTo(Point(35, 23), EScalingAlgorithm::NEAREST);
 			images.push_back(image);
 		}
 	}
@@ -1394,7 +1414,6 @@ void CPlayerInterface::newObject( const CGObjectInstance * obj )
 		&& LOCPLINT->castleInt
 		&&  obj->visitablePos() == LOCPLINT->castleInt->town->bestLocation())
 	{
-		CCS->soundh->playSound(soundBase::newBuilding);
 		LOCPLINT->castleInt->addBuilding(BuildingID::SHIP);
 	}
 }

+ 5 - 1
client/CPlayerInterface.h

@@ -27,6 +27,7 @@ class CGObjectInstance;
 class UpgradeInfo;
 class ConditionalWait;
 struct CPathsInfo;
+class PathfinderCache;
 
 VCMI_LIB_NAMESPACE_END
 
@@ -64,6 +65,7 @@ class CPlayerInterface : public CGameInterface, public IUpdateable
 	std::list<std::shared_ptr<CInfoWindow>> dialogs; //queue of dialogs awaiting to be shown (not currently shown!)
 
 	std::unique_ptr<HeroMovementController> movementController;
+	std::unique_ptr<PathfinderCache> pathfinderCache;
 public: // TODO: make private
 	std::unique_ptr<ArtifactsUIController> artifactController;
 	std::shared_ptr<Environment> env;
@@ -154,7 +156,7 @@ protected: // Call-ins from server, should not be called directly, but only via
 	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 battleStackMoved(const BattleID & battleID, const CStack * stack, const BattleHexArray & 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
@@ -198,6 +200,8 @@ public: // public interface for use by client via LOCPLINT access
 	void gamePause(bool pause);
 	void endNetwork();
 	void closeAllDialogs();
+	std::shared_ptr<const CPathsInfo> getPathsInfo(const CGHeroInstance * h);
+	void invalidatePaths() override;
 
 	///returns true if all events are processed internally
 	bool capturedAllEvents();

+ 0 - 41
client/Client.cpp

@@ -222,8 +222,6 @@ void CClient::initMapHandler()
 		CGI->mh = std::make_shared<CMapHandler>(gs->map);
 		logNetwork->trace("Creating mapHandler: %d ms", CSH->th->getDiff());
 	}
-
-	pathCache.clear();
 }
 
 void CClient::initPlayerEnvironments()
@@ -494,24 +492,7 @@ void CClient::startPlayerBattleAction(const BattleID & battleID, PlayerColor col
 	}
 }
 
-void CClient::updatePath(const ObjectInstanceID & id)
-{
-	invalidatePaths();
-	auto hero = getHero(id);
-	updatePath(hero);
-}
-
-void CClient::updatePath(const CGHeroInstance * hero)
-{
-	if(LOCPLINT && hero)
-		LOCPLINT->localState->verifyPath(hero);
-}
 
-void CClient::invalidatePaths()
-{
-	boost::unique_lock<boost::mutex> pathLock(pathCacheMutex);
-	pathCache.clear();
-}
 
 vstd::RNG & CClient::getRandomGenerator()
 {
@@ -520,28 +501,6 @@ vstd::RNG & CClient::getRandomGenerator()
 	throw std::runtime_error("Illegal access to random number generator from client code!");
 }
 
-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))
-	{
-		auto 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
 {

+ 0 - 8
client/Client.h

@@ -149,11 +149,6 @@ public:
 	void battleFinished(const BattleID & battleID);
 	void startPlayerBattleAction(const BattleID & battleID, PlayerColor color);
 
-	void invalidatePaths(); // clears this->pathCache()
-	void updatePath(const ObjectInstanceID & heroID); // invalidatePaths and update displayed hero path 
-	void updatePath(const CGHeroInstance * hero);
-	std::shared_ptr<const CPathsInfo> getPathsInfo(const CGHeroInstance * h);
-
 	friend class CCallback; //handling players actions
 	friend class CBattleCallback; //handling players actions
 
@@ -235,8 +230,5 @@ private:
 #endif
 	std::unique_ptr<events::EventBus> clientEventBus;
 
-	mutable boost::mutex pathCacheMutex;
-	std::map<const CGHeroInstance *, std::shared_ptr<CPathsInfo>> pathCache;
-
 	void reinitScripting();
 };

+ 12 - 37
client/NetPacksClient.cpp

@@ -168,7 +168,6 @@ void ApplyClientNetPackVisitor::visitSetMana(SetMana & pack)
 void ApplyClientNetPackVisitor::visitSetMovePoints(SetMovePoints & pack)
 {
 	const CGHeroInstance *h = cl.getHero(pack.hid);
-	cl.updatePath(h);
 	callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroMovePointsChanged, h);
 }
 
@@ -194,7 +193,7 @@ void ApplyClientNetPackVisitor::visitFoWChange(FoWChange & pack)
 				i.second->tileHidden(pack.tiles);
 		}
 	}
-	cl.invalidatePaths();
+	callAllInterfaces(cl, &CGameInterface::invalidatePaths);
 }
 
 static void dispatchGarrisonChange(CClient & cl, ObjectInstanceID army1, ObjectInstanceID army2)
@@ -235,33 +234,21 @@ void ApplyClientNetPackVisitor::visitSetStackType(SetStackType & pack)
 void ApplyClientNetPackVisitor::visitEraseStack(EraseStack & pack)
 {
 	dispatchGarrisonChange(cl, pack.army, ObjectInstanceID());
-	cl.updatePath(pack.army); //it is possible to remove last non-native unit for current terrain and lose movement penalty
 }
 
 void ApplyClientNetPackVisitor::visitSwapStacks(SwapStacks & pack)
 {
 	dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy);
-
-	if(pack.srcArmy != pack.dstArmy)
-		cl.updatePath(pack.dstArmy); // adding/removing units may change terrain type penalty based on creature native terrains
 }
 
 void ApplyClientNetPackVisitor::visitInsertNewStack(InsertNewStack & pack)
 {
 	dispatchGarrisonChange(cl, pack.army, ObjectInstanceID());
-
-	cl.updatePath(pack.army); // adding/removing units may change terrain type penalty based on creature native terrains
 }
 
 void ApplyClientNetPackVisitor::visitRebalanceStacks(RebalanceStacks & pack)
 {
 	dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy);
-
-	if(pack.srcArmy != pack.dstArmy)
-	{
-		cl.updatePath(pack.srcArmy); // adding/removing units may change terrain type penalty based on creature native terrains
-		cl.updatePath(pack.dstArmy);
-	}
 }
 
 void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & pack)
@@ -272,12 +259,6 @@ void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & p
 			? ObjectInstanceID()
 			: pack.moves[0].dstArmy;
 		dispatchGarrisonChange(cl, pack.moves[0].srcArmy, destArmy);
-
-		if(pack.moves[0].srcArmy != destArmy)
-		{
-			cl.updatePath(destArmy); // adding/removing units may change terrain type penalty based on creature native terrains
-			cl.updatePath(pack.moves[0].srcArmy);
-		}
 	}
 }
 
@@ -303,7 +284,6 @@ void ApplyClientNetPackVisitor::visitPutArtifact(PutArtifact & pack)
 
 void ApplyClientNetPackVisitor::visitEraseArtifact(BulkEraseArtifacts & pack)
 {
-	cl.updatePath(pack.artHolder);
 	for(const auto & slotErase : pack.posPack)
 		callInterfaceIfPresent(cl, cl.getOwner(pack.artHolder), &IGameEventsReceiver::artifactRemoved, ArtifactLocation(pack.artHolder, slotErase));
 }
@@ -323,9 +303,6 @@ void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack)
 				callInterfaceIfPresent(cl, pack.interfaceOwner, &IGameEventsReceiver::askToAssembleArtifact, dstLoc);
 			if(pack.interfaceOwner != dstOwner)
 				callInterfaceIfPresent(cl, dstOwner, &IGameEventsReceiver::artifactMoved, srcLoc, dstLoc);
-
-			cl.updatePath(pack.srcArtHolder); // hero might have equipped/unequipped Angel Wings
-			cl.updatePath(pack.dstArtHolder);
 		}
 	};
 
@@ -354,15 +331,11 @@ void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack)
 void ApplyClientNetPackVisitor::visitAssembledArtifact(AssembledArtifact & pack)
 {
 	callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactAssembled, pack.al);
-
-	cl.updatePath(pack.al.artHolder); // hero might have equipped/unequipped Angel Wings
 }
 
 void ApplyClientNetPackVisitor::visitDisassembledArtifact(DisassembledArtifact & pack)
 {
 	callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactDisassembled, pack.al);
-
-	cl.updatePath(pack.al.artHolder); // hero might have equipped/unequipped Angel Wings
 }
 
 void ApplyClientNetPackVisitor::visitHeroVisit(HeroVisit & pack)
@@ -374,7 +347,7 @@ void ApplyClientNetPackVisitor::visitHeroVisit(HeroVisit & pack)
 
 void ApplyClientNetPackVisitor::visitNewTurn(NewTurn & pack)
 {
-	cl.invalidatePaths();
+	callAllInterfaces(cl, &CGameInterface::invalidatePaths);
 
 	if(pack.newWeekNotification)
 	{
@@ -387,7 +360,8 @@ void ApplyClientNetPackVisitor::visitNewTurn(NewTurn & pack)
 
 void ApplyClientNetPackVisitor::visitGiveBonus(GiveBonus & pack)
 {
-	cl.invalidatePaths();
+	callAllInterfaces(cl, &CGameInterface::invalidatePaths);
+
 	switch(pack.who)
 	{
 	case GiveBonus::ETarget::OBJECT:
@@ -423,7 +397,7 @@ void ApplyClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack)
 		CGI->mh->onObjectFadeIn(obj, pack.initiator);
 		CGI->mh->waitForOngoingAnimations();
 	}
-	cl.invalidatePaths();
+	callAllInterfaces(cl, &CGameInterface::invalidatePaths);
 }
 
 void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack)
@@ -490,7 +464,6 @@ void ApplyClientNetPackVisitor::visitPlayerReinitInterface(PlayerReinitInterface
 
 void ApplyClientNetPackVisitor::visitRemoveBonus(RemoveBonus & pack)
 {
-	cl.invalidatePaths();
 	switch(pack.who)
 	{
 	case GiveBonus::ETarget::OBJECT:
@@ -531,7 +504,8 @@ void ApplyFirstClientNetPackVisitor::visitRemoveObject(RemoveObject & pack)
 
 void ApplyClientNetPackVisitor::visitRemoveObject(RemoveObject & pack)
 {
-	cl.invalidatePaths();
+	callAllInterfaces(cl, &CGameInterface::invalidatePaths);
+
 	for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++)
 		i->second->objectRemovedAfter();
 }
@@ -561,7 +535,7 @@ void ApplyFirstClientNetPackVisitor::visitTryMoveHero(TryMoveHero & pack)
 void ApplyClientNetPackVisitor::visitTryMoveHero(TryMoveHero & pack)
 {
 	const CGHeroInstance *h = cl.getHero(pack.id);
-	cl.invalidatePaths();
+	callAllInterfaces(cl, &CGameInterface::invalidatePaths);
 
 	if(CGI->mh)
 	{
@@ -976,7 +950,8 @@ void ApplyClientNetPackVisitor::visitPlayerMessageClient(PlayerMessageClient & p
 
 void ApplyClientNetPackVisitor::visitAdvmapSpellCast(AdvmapSpellCast & pack)
 {
-	cl.invalidatePaths();
+	callAllInterfaces(cl, &CGameInterface::invalidatePaths);
+
 	auto caster = cl.getHero(pack.casterID);
 	if(caster)
 		//consider notifying other interfaces that see hero?
@@ -1068,7 +1043,7 @@ void ApplyClientNetPackVisitor::visitCenterView(CenterView & pack)
 
 void ApplyClientNetPackVisitor::visitNewObject(NewObject & pack)
 {
-	cl.invalidatePaths();
+	callAllInterfaces(cl, &CGameInterface::invalidatePaths);
 
 	const CGObjectInstance *obj = pack.newObject;
 	if(CGI->mh)
@@ -1101,5 +1076,5 @@ void ApplyClientNetPackVisitor::visitSetAvailableArtifacts(SetAvailableArtifacts
 
 void ApplyClientNetPackVisitor::visitEntitiesChanged(EntitiesChanged & pack)
 {
-	cl.invalidatePaths();
+	callAllInterfaces(cl, &CGameInterface::invalidatePaths);
 }

+ 1 - 1
client/PlayerLocalState.cpp

@@ -54,7 +54,7 @@ bool PlayerLocalState::hasPath(const CGHeroInstance * h) const
 bool PlayerLocalState::setPath(const CGHeroInstance * h, const int3 & destination)
 {
 	CGPath path;
-	if(!owner.cb->getPathsInfo(h)->getPath(path, destination))
+	if(!owner.getPathsInfo(h)->getPath(path, destination))
 	{
 		paths.erase(h); //invalidate previously possible path if selected (before other hero blocked only path / fly spell expired)
 		syncronizeState();

+ 1 - 0
client/PlayerLocalState.h

@@ -17,6 +17,7 @@ class CArmedInstance;
 class JsonNode;
 struct CGPath;
 class int3;
+struct CPathsInfo;
 
 VCMI_LIB_NAMESPACE_END
 

+ 2 - 2
client/adventureMap/AdventureMapInterface.cpp

@@ -546,7 +546,7 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &targetPosition)
 	{
 		isHero = true;
 
-		const CGPathNode *pn = LOCPLINT->cb->getPathsInfo(currentHero)->getPathInfo(targetPosition);
+		const CGPathNode *pn = LOCPLINT->getPathsInfo(currentHero)->getPathInfo(targetPosition);
 		if(currentHero == topBlocking) //clicked selected hero
 		{
 			LOCPLINT->openHeroWindow(currentHero);
@@ -685,7 +685,7 @@ void AdventureMapInterface::onTileHovered(const int3 &targetPosition)
 		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(targetPosition);
+		const CGPathNode * pathNode = LOCPLINT->getPathsInfo(hero)->getPathInfo(targetPosition);
 		assert(pathNode);
 
 		if((GH.isKeyboardAltDown() || settings["gameTweaks"]["forceMovementInfo"].Bool()) && pathNode->reachable()) //overwrite status bar text with movement info

+ 1 - 2
client/adventureMap/CMinimap.cpp

@@ -22,7 +22,6 @@
 #include "../render/Colors.h"
 #include "../render/Canvas.h"
 #include "../render/Graphics.h"
-#include "../renderSDL/SDL_Extensions.h"
 #include "../windows/InfoWindows.h"
 
 #include "../../CCallback.h"
@@ -178,7 +177,7 @@ void CMinimap::mouseDragged(const Point & cursorPosition, const Point & lastUpda
 
 void CMinimap::showAll(Canvas & to)
 {
-	CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), aiShield->pos);
+	CanvasClipRectGuard guard(to, aiShield->pos);
 	CIntObject::showAll(to);
 
 	if(minimap)

+ 24 - 24
client/battle/BattleActionsController.cpp

@@ -358,7 +358,7 @@ const CSpell * BattleActionsController::getHeroSpellToCast( ) const
 	return nullptr;
 }
 
-const CSpell * BattleActionsController::getStackSpellToCast(BattleHex hoveredHex)
+const CSpell * BattleActionsController::getStackSpellToCast(const BattleHex & hoveredHex)
 {
 	if (heroSpellToCast)
 		return nullptr;
@@ -383,14 +383,14 @@ const CSpell * BattleActionsController::getStackSpellToCast(BattleHex hoveredHex
 	return action.spell().toSpell();
 }
 
-const CSpell * BattleActionsController::getCurrentSpell(BattleHex hoveredHex)
+const CSpell * BattleActionsController::getCurrentSpell(const BattleHex & hoveredHex)
 {
 	if (getHeroSpellToCast())
 		return getHeroSpellToCast();
 	return getStackSpellToCast(hoveredHex);
 }
 
-const CStack * BattleActionsController::getStackForHex(BattleHex hoveredHex)
+const CStack * BattleActionsController::getStackForHex(const BattleHex & hoveredHex)
 {
 	const CStack * shere = owner.getBattle()->battleGetStackByPos(hoveredHex, true);
 	if(shere)
@@ -398,7 +398,7 @@ const CStack * BattleActionsController::getStackForHex(BattleHex hoveredHex)
 	return owner.getBattle()->battleGetStackByPos(hoveredHex, false);
 }
 
-void BattleActionsController::actionSetCursor(PossiblePlayerBattleAction action, BattleHex targetHex)
+void BattleActionsController::actionSetCursor(PossiblePlayerBattleAction action, const BattleHex & targetHex)
 {
 	switch (action.get())
 	{
@@ -479,7 +479,7 @@ void BattleActionsController::actionSetCursor(PossiblePlayerBattleAction action,
 	assert(0);
 }
 
-void BattleActionsController::actionSetCursorBlocked(PossiblePlayerBattleAction action, BattleHex targetHex)
+void BattleActionsController::actionSetCursorBlocked(PossiblePlayerBattleAction action, const BattleHex & targetHex)
 {
 	switch (action.get())
 	{
@@ -500,7 +500,7 @@ void BattleActionsController::actionSetCursorBlocked(PossiblePlayerBattleAction
 	assert(0);
 }
 
-std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattleAction action, BattleHex targetHex)
+std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattleAction action, const BattleHex & targetHex)
 {
 	const CStack * targetStack = getStackForHex(targetHex);
 
@@ -522,7 +522,7 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle
 			{
 				const auto * attacker = owner.stacksController->getActiveStack();
 				BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex);
-				int distance = attacker->position.isValid() ? owner.getBattle()->battleGetDistances(attacker, attacker->getPosition())[attackFromHex] : 0;
+				int distance = attacker->position.isValid() ? owner.getBattle()->battleGetDistances(attacker, attacker->getPosition())[attackFromHex.toInt()] : 0;
 				DamageEstimation retaliation;
 				BattleAttackInfo attackInfo(attacker, targetStack, distance, false );
 				DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(attackInfo, &retaliation);
@@ -589,7 +589,7 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle
 	return "";
 }
 
-std::string BattleActionsController::actionGetStatusMessageBlocked(PossiblePlayerBattleAction action, BattleHex targetHex)
+std::string BattleActionsController::actionGetStatusMessageBlocked(PossiblePlayerBattleAction action, const BattleHex & targetHex)
 {
 	switch (action.get())
 	{
@@ -611,7 +611,7 @@ std::string BattleActionsController::actionGetStatusMessageBlocked(PossiblePlaye
 	}
 }
 
-bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, BattleHex targetHex)
+bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, const BattleHex & targetHex)
 {
 	const CStack * targetStack = getStackForHex(targetHex);
 	bool targetStackOwned = targetStack && targetStack->unitOwner() == owner.curInt->playerID;
@@ -706,7 +706,7 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B
 	return false;
 }
 
-void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, BattleHex targetHex)
+void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, const BattleHex & targetHex)
 {
 	const CStack * targetStack = getStackForHex(targetHex);
 
@@ -723,11 +723,11 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, B
 		{
 			if(owner.stacksController->getActiveStack()->doubleWide())
 			{
-				std::vector<BattleHex> acc = owner.getBattle()->battleGetAvailableHexes(owner.stacksController->getActiveStack(), false);
+				BattleHexArray acc = owner.getBattle()->battleGetAvailableHexes(owner.stacksController->getActiveStack(), false);
 				BattleHex shiftedDest = targetHex.cloneInDirection(owner.stacksController->getActiveStack()->destShiftDir(), false);
-				if(vstd::contains(acc, targetHex))
+				if(acc.contains(targetHex))
 					owner.giveCommand(EActionType::WALK, targetHex);
-				else if(vstd::contains(acc, shiftedDest))
+				else if(acc.contains(shiftedDest))
 					owner.giveCommand(EActionType::WALK, shiftedDest);
 			}
 			else
@@ -846,7 +846,7 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, B
 	return;
 }
 
-PossiblePlayerBattleAction BattleActionsController::selectAction(BattleHex targetHex)
+PossiblePlayerBattleAction BattleActionsController::selectAction(const BattleHex & targetHex)
 {
 	assert(owner.stacksController->getActiveStack() != nullptr);
 	assert(!possibleActions.empty());
@@ -870,7 +870,7 @@ PossiblePlayerBattleAction BattleActionsController::selectAction(BattleHex targe
 	return possibleActions.front();
 }
 
-void BattleActionsController::onHexHovered(BattleHex hoveredHex)
+void BattleActionsController::onHexHovered(const BattleHex & hoveredHex)
 {
 	if (owner.openingPlaying())
 	{
@@ -926,7 +926,7 @@ void BattleActionsController::onHoverEnded()
 	currentConsoleMsg.clear();
 }
 
-void BattleActionsController::onHexLeftClicked(BattleHex clickedHex)
+void BattleActionsController::onHexLeftClicked(const BattleHex & clickedHex)
 {
 	if (owner.stacksController->getActiveStack() == nullptr)
 		return;
@@ -983,7 +983,7 @@ spells::Mode BattleActionsController::getCurrentCastMode() const
 
 }
 
-bool BattleActionsController::isCastingPossibleHere(const CSpell * currentSpell, const CStack *targetStack, BattleHex targetHex)
+bool BattleActionsController::isCastingPossibleHere(const CSpell * currentSpell, const CStack *targetStack, const BattleHex & targetHex)
 {
 	assert(currentSpell);
 	if (!currentSpell)
@@ -1006,14 +1006,14 @@ bool BattleActionsController::isCastingPossibleHere(const CSpell * currentSpell,
 	return m->canBeCastAt(target, problem);
 }
 
-bool BattleActionsController::canStackMoveHere(const CStack * stackToMove, BattleHex myNumber) const
+bool BattleActionsController::canStackMoveHere(const CStack * stackToMove, const BattleHex & myNumber) const
 {
-	std::vector<BattleHex> acc = owner.getBattle()->battleGetAvailableHexes(stackToMove, false);
+	BattleHexArray acc = owner.getBattle()->battleGetAvailableHexes(stackToMove, false);
 	BattleHex shiftedDest = myNumber.cloneInDirection(stackToMove->destShiftDir(), false);
 
-	if (vstd::contains(acc, myNumber))
+	if (acc.contains(myNumber))
 		return true;
-	else if (stackToMove->doubleWide() && vstd::contains(acc, shiftedDest))
+	else if (stackToMove->doubleWide() && acc.contains(shiftedDest))
 		return true;
 	else
 		return false;
@@ -1057,7 +1057,7 @@ void BattleActionsController::activateStack()
 	}
 }
 
-void BattleActionsController::onHexRightClicked(BattleHex clickedHex)
+void BattleActionsController::onHexRightClicked(const BattleHex & clickedHex)
 {
 	bool isCurrentStackInSpellcastMode = creatureSpellcastingModeActive();
 
@@ -1095,7 +1095,7 @@ bool BattleActionsController::creatureSpellcastingModeActive() const
 	return !possibleActions.empty() && std::all_of(possibleActions.begin(), possibleActions.end(), spellcastModePredicate);
 }
 
-bool BattleActionsController::currentActionSpellcasting(BattleHex hoveredHex)
+bool BattleActionsController::currentActionSpellcasting(const BattleHex & hoveredHex)
 {
 	if (heroSpellToCast)
 		return true;
@@ -1126,4 +1126,4 @@ void BattleActionsController::pushFrontPossibleAction(PossiblePlayerBattleAction
 void BattleActionsController::resetCurrentStackPossibleActions()
 {
 	possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack());
-}
+}

+ 16 - 16
client/battle/BattleActionsController.h

@@ -44,24 +44,24 @@ class BattleActionsController
 	/// 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
+	bool isCastingPossibleHere (const CSpell * spell, const CStack *shere, const BattleHex & myNumber);
+	bool canStackMoveHere (const CStack *sactive, const 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);
+	bool actionIsLegal(PossiblePlayerBattleAction action, const BattleHex & hoveredHex);
 
-	void actionSetCursor(PossiblePlayerBattleAction action, BattleHex hoveredHex);
-	void actionSetCursorBlocked(PossiblePlayerBattleAction action, BattleHex hoveredHex);
+	void actionSetCursor(PossiblePlayerBattleAction action, const BattleHex & hoveredHex);
+	void actionSetCursorBlocked(PossiblePlayerBattleAction action, const BattleHex & hoveredHex);
 
-	std::string actionGetStatusMessage(PossiblePlayerBattleAction action, BattleHex hoveredHex);
-	std::string actionGetStatusMessageBlocked(PossiblePlayerBattleAction action, BattleHex hoveredHex);
+	std::string actionGetStatusMessage(PossiblePlayerBattleAction action, const BattleHex & hoveredHex);
+	std::string actionGetStatusMessageBlocked(PossiblePlayerBattleAction action, const BattleHex & hoveredHex);
 
-	void actionRealize(PossiblePlayerBattleAction action, BattleHex hoveredHex);
+	void actionRealize(PossiblePlayerBattleAction action, const BattleHex & hoveredHex);
 
-	PossiblePlayerBattleAction selectAction(BattleHex myNumber);
+	PossiblePlayerBattleAction selectAction(const BattleHex & myNumber);
 
-	const CStack * getStackForHex(BattleHex myNumber) ;
+	const CStack * getStackForHex(const BattleHex & myNumber) ;
 
 	/// attempts to initialize spellcasting action for stack
 	/// will silently return if stack is not a spellcaster
@@ -71,7 +71,7 @@ class BattleActionsController
 	const CSpell * getHeroSpellToCast() const;
 
 	/// if current stack is spellcaster, returns spell being cast, or null othervice
-	const CSpell * getStackSpellToCast(BattleHex hoveredHex);
+	const CSpell * getStackSpellToCast(const BattleHex & hoveredHex);
 
 	/// returns true if current stack is a spellcaster
 	bool isActiveStackSpellcaster() const;
@@ -91,7 +91,7 @@ public:
 	/// - 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);
+	bool currentActionSpellcasting(const BattleHex & hoveredHex);
 
 	/// enter targeted spellcasting mode for creature, e.g. via "F" hotkey
 	void enterCreatureCastingMode();
@@ -103,19 +103,19 @@ public:
 	void endCastingSpell();
 
 	/// update cursor and status bar according to new active hex
-	void onHexHovered(BattleHex hoveredHex);
+	void onHexHovered(const 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);
+	void onHexLeftClicked(const BattleHex & clickedHex);
 
 	/// performs action according to selected hex
-	void onHexRightClicked(BattleHex clickedHex);
+	void onHexRightClicked(const BattleHex & clickedHex);
 
 	const spells::Caster * getCurrentSpellcaster() const;
-	const CSpell * getCurrentSpell(BattleHex hoveredHex);
+	const CSpell * getCurrentSpell(const BattleHex & hoveredHex);
 	spells::Mode getCurrentCastMode() const;
 
 	/// methods to work with array of possible actions, needed to control special creatures abilities

+ 6 - 6
client/battle/BattleAnimationClasses.cpp

@@ -174,7 +174,7 @@ AttackAnimation::AttackAnimation(BattleInterface & owner, const CStack *attacker
 	  attackingStack(attacker)
 {
 	assert(attackingStack && "attackingStack is nullptr in CBattleAttack::CBattleAttack !\n");
-	attackingStackPosBeforeReturn = attackingStack->getPosition();
+	attackingStackPosBeforeReturn = attackingStack->getPosition().toInt();
 }
 
 HittedAnimation::HittedAnimation(BattleInterface & owner, const CStack * stack)
@@ -422,7 +422,7 @@ MovementAnimation::~MovementAnimation()
 		CCS->soundh->stopSound(moveSoundHandler);
 }
 
-MovementAnimation::MovementAnimation(BattleInterface & owner, const CStack *stack, std::vector<BattleHex> _destTiles, int _distance)
+MovementAnimation::MovementAnimation(BattleInterface & owner, const CStack *stack, const BattleHexArray & _destTiles, int _distance)
 	: StackMoveAnimation(owner, stack, stack->getPosition(), _destTiles.front()),
 	  destTiles(_destTiles),
 	  currentMoveIndex(0),
@@ -892,17 +892,17 @@ EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath &
 	logAnim->debug("CPointEffectAnimation::init: effect %s", animationName.getName());
 }
 
-EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector<BattleHex> hex, int effects, bool reversed):
+EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, const BattleHexArray & hexes, int effects, bool reversed):
 	EffectAnimation(owner, animationName, effects, 1.0f, reversed)
 {
-	battlehexes = hex;
+	battlehexes = hexes;
 }
 
 EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex, int effects, float transparencyFactor, bool reversed):
 	EffectAnimation(owner, animationName, effects, transparencyFactor, reversed)
 {
 	assert(hex.isValid());
-	battlehexes.push_back(hex);
+	battlehexes.insert(hex);
 }
 
 EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector<Point> pos, int effects, bool reversed):
@@ -921,7 +921,7 @@ EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath &
 	EffectAnimation(owner, animationName, effects, 1.0f, reversed)
 {
 	assert(hex.isValid());
-	battlehexes.push_back(hex);
+	battlehexes.insert(hex);
 	positions.push_back(pos);
 }
 

+ 9 - 9
client/battle/BattleAnimationClasses.h

@@ -9,7 +9,7 @@
  */
 #pragma once
 
-#include "../../lib/battle/BattleHex.h"
+#include "../../lib/battle/BattleHexArray.h"
 #include "../../lib/filesystem/ResourcePath.h"
 #include "BattleConstants.h"
 
@@ -143,7 +143,7 @@ class MovementAnimation : public StackMoveAnimation
 private:
 	int moveSoundHandler; // sound handler used when moving a unit
 
-	std::vector<BattleHex> destTiles; //full path, includes already passed hexes
+	const BattleHexArray & destTiles; //full path, includes already passed hexes
 	ui32 currentMoveIndex; // index of nextHex in destTiles
 
 	double begX, begY; // starting position
@@ -159,7 +159,7 @@ public:
 	bool init() override;
 	void tick(uint32_t msPassed) override;
 
-	MovementAnimation(BattleInterface & owner, const CStack *_stack, std::vector<BattleHex> _destTiles, int _distance);
+	MovementAnimation(BattleInterface & owner, const CStack *_stack, const BattleHexArray & _destTiles, int _distance);
 	~MovementAnimation();
 };
 
@@ -316,7 +316,7 @@ class EffectAnimation : public BattleAnimation
 
 	std::shared_ptr<CAnimation>	animation;
 	std::vector<Point> positions;
-	std::vector<BattleHex> battlehexes;
+	BattleHexArray battlehexes;
 
 	bool alignToBottom() const;
 	bool waitForSound() const;
@@ -339,14 +339,14 @@ public:
 	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects = 0, float transparencyFactor = 1.f, 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);
+	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, float transparencyFactor = 1.0f, 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, BattleHex hex               , int effects = 0, float transparencyFactor = 1.0f, bool reversed = false);
+	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, const BattleHexArray & hexes, int effects = 0, bool reversed = false);
 
-	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, 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;

+ 59 - 69
client/battle/BattleFieldController.cpp

@@ -26,7 +26,6 @@
 #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"
@@ -267,7 +266,7 @@ void BattleFieldController::showBackgroundImageWithHexes(Canvas & canvas)
 void BattleFieldController::redrawBackgroundWithHexes()
 {
 	const CStack *activeStack = owner.stacksController->getActiveStack();
-	std::vector<BattleHex> attackableHexes;
+	BattleHexArray attackableHexes;
 	if(activeStack)
 		occupiableHexes = owner.getBattle()->battleGetAvailableHexes(activeStack, false, true, &attackableHexes);
 
@@ -280,9 +279,9 @@ void BattleFieldController::redrawBackgroundWithHexes()
 	// 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)
+		BattleHexArray hexesToShade = occupiableHexes;
+		hexesToShade.insert(attackableHexes);
+		for(const BattleHex & hex : hexesToShade)
 		{
 			showHighlightedHex(*backgroundWithHexes, cellShade, hex, false);
 		}
@@ -303,7 +302,7 @@ void BattleFieldController::redrawBackgroundWithHexes()
 	}
 }
 
-void BattleFieldController::showHighlightedHex(Canvas & canvas, std::shared_ptr<IImage> highlight, BattleHex hex, bool darkBorder)
+void BattleFieldController::showHighlightedHex(Canvas & canvas, std::shared_ptr<IImage> highlight, const BattleHex & hex, bool darkBorder)
 {
 	Point hexPos = hexPositionLocal(hex).topLeft();
 
@@ -312,48 +311,37 @@ void BattleFieldController::showHighlightedHex(Canvas & canvas, std::shared_ptr<
 		canvas.draw(cellBorder, hexPos);
 }
 
-std::set<BattleHex> BattleFieldController::getHighlightedHexesForActiveStack()
+BattleHexArray BattleFieldController::getHighlightedHexesForActiveStack()
 {
-	std::set<BattleHex> result;
-
 	if(!owner.stacksController->getActiveStack())
-		return result;
+		return BattleHexArray();
 
 	if(!settings["battle"]["stackRange"].Bool())
-		return result;
+		return BattleHexArray();
 
 	auto hoveredHex = getHoveredHex();
 
-	std::set<BattleHex> set = owner.getBattle()->battleGetAttackedHexes(owner.stacksController->getActiveStack(), hoveredHex);
-	for(BattleHex hex : set)
-		result.insert(hex);
-
-	return result;
+	return owner.getBattle()->battleGetAttackedHexes(owner.stacksController->getActiveStack(), hoveredHex);
 }
 
-std::set<BattleHex> BattleFieldController::getMovementRangeForHoveredStack()
+BattleHexArray BattleFieldController::getMovementRangeForHoveredStack()
 {
-	std::set<BattleHex> result;
-
 	if (!owner.stacksController->getActiveStack())
-		return result;
+		return BattleHexArray();
 
 	if (!settings["battle"]["movementHighlightOnHover"].Bool() && !GH.isKeyboardShiftDown())
-		return result;
+		return BattleHexArray();
 
 	auto hoveredStack = getHoveredStack();
 	if(hoveredStack)
-	{
-		std::vector<BattleHex> v = owner.getBattle()->battleGetAvailableHexes(hoveredStack, true, true, nullptr);
-		for(BattleHex hex : v)
-			result.insert(hex);
-	}
-	return result;
+		return owner.getBattle()->battleGetAvailableHexes(hoveredStack, true, true, nullptr);
+	else
+		return BattleHexArray();
 }
 
-std::set<BattleHex> BattleFieldController::getHighlightedHexesForSpellRange()
+BattleHexArray BattleFieldController::getHighlightedHexesForSpellRange()
 {
-	std::set<BattleHex> result;
+	BattleHexArray result;
 	auto hoveredHex = getHoveredHex();
 
 	const spells::Caster *caster = nullptr;
@@ -369,7 +357,7 @@ std::set<BattleHex> BattleFieldController::getHighlightedHexesForSpellRange()
 		spells::BattleCast event(owner.getBattle().get(), caster, mode, spell);
 		auto shadedHexes = spell->battleMechanics(&event)->rangeInHexes(hoveredHex);
 
-		for(BattleHex shadedHex : shadedHexes)
+		for(const BattleHex & shadedHex : shadedHexes)
 		{
 			if((shadedHex.getX() != 0) && (shadedHex.getX() != GameConstants::BFIELD_WIDTH - 1))
 				result.insert(shadedHex);
@@ -378,7 +366,7 @@ std::set<BattleHex> BattleFieldController::getHighlightedHexesForSpellRange()
 	return result;
 }
 
-std::set<BattleHex> BattleFieldController::getHighlightedHexesForMovementTarget()
+BattleHexArray BattleFieldController::getHighlightedHexesForMovementTarget()
 {
 	const CStack * stack = owner.stacksController->getActiveStack();
 	auto hoveredHex = getHoveredHex();
@@ -386,7 +374,7 @@ std::set<BattleHex> BattleFieldController::getHighlightedHexesForMovementTarget(
 	if(!stack)
 		return {};
 
-	std::vector<BattleHex> availableHexes = owner.getBattle()->battleGetAvailableHexes(stack, false, false, nullptr);
+	BattleHexArray availableHexes = owner.getBattle()->battleGetAvailableHexes(stack, false, false, nullptr);
 
 	auto hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true);
 	if(owner.getBattle()->battleCanAttack(stack, hoveredStack, hoveredHex))
@@ -402,7 +390,7 @@ std::set<BattleHex> BattleFieldController::getHighlightedHexesForMovementTarget(
 		}
 	}
 
-	if(vstd::contains(availableHexes, hoveredHex))
+	if(availableHexes.contains(hoveredHex))
 	{
 		if(stack->doubleWide())
 			return {hoveredHex, stack->occupiedHex(hoveredHex)};
@@ -412,7 +400,7 @@ std::set<BattleHex> BattleFieldController::getHighlightedHexesForMovementTarget(
 
 	if(stack->doubleWide())
 	{
-		for(auto const & hex : availableHexes)
+		for(const auto & hex : availableHexes)
 		{
 			if(stack->occupiedHex(hex) == hoveredHex)
 				return {hoveredHex, hex};
@@ -424,9 +412,9 @@ std::set<BattleHex> BattleFieldController::getHighlightedHexesForMovementTarget(
 
 // Range limit highlight helpers
 
-std::vector<BattleHex> BattleFieldController::getRangeHexes(BattleHex sourceHex, uint8_t distance)
+BattleHexArray BattleFieldController::getRangeHexes(const BattleHex & sourceHex, uint8_t distance)
 {
-	std::vector<BattleHex> rangeHexes;
+	BattleHexArray rangeHexes;
 
 	if (!settings["battle"]["rangeLimitHighlightOnHover"].Bool() && !GH.isKeyboardShiftDown())
 		return rangeHexes;
@@ -436,27 +424,27 @@ std::vector<BattleHex> BattleFieldController::getRangeHexes(BattleHex sourceHex,
 	{
 		BattleHex hex(i);
 		if(hex.isAvailable() && BattleHex::getDistance(sourceHex, hex) <= distance)
-			rangeHexes.push_back(hex);
+			rangeHexes.insert(hex);
 	}
 
 	return rangeHexes;
 }
 
-std::vector<BattleHex> BattleFieldController::getRangeLimitHexes(BattleHex hoveredHex, std::vector<BattleHex> rangeHexes, uint8_t distanceToLimit)
+BattleHexArray BattleFieldController::getRangeLimitHexes(const BattleHex & hoveredHex, const BattleHexArray & rangeHexes, uint8_t distanceToLimit)
 {
-	std::vector<BattleHex> rangeLimitHexes;
+	BattleHexArray 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);
+			rangeLimitHexes.insert(hex);
 	}
 
 	return rangeLimitHexes;
 }
 
-bool BattleFieldController::IsHexInRangeLimit(BattleHex hex, std::vector<BattleHex> & rangeLimitHexes, int * hexIndexInRangeLimit)
+bool BattleFieldController::IsHexInRangeLimit(const BattleHex & hex, const BattleHexArray & rangeLimitHexes, int * hexIndexInRangeLimit)
 {
 	bool  hexInRangeLimit = false;
 
@@ -470,18 +458,19 @@ bool BattleFieldController::IsHexInRangeLimit(BattleHex hex, std::vector<BattleH
 	return hexInRangeLimit;
 }
 
-std::vector<std::vector<BattleHex::EDir>> BattleFieldController::getOutsideNeighbourDirectionsForLimitHexes(std::vector<BattleHex> wholeRangeHexes, std::vector<BattleHex> rangeLimitHexes)
+std::vector<std::vector<BattleHex::EDir>> BattleFieldController::getOutsideNeighbourDirectionsForLimitHexes(
+	const BattleHexArray & wholeRangeHexes, const BattleHexArray & rangeLimitHexes)
 {
 	std::vector<std::vector<BattleHex::EDir>> output;
 
 	if(wholeRangeHexes.empty())
 		return output;
 
-	for(auto & hex : rangeLimitHexes)
+	for(const auto & hex : rangeLimitHexes)
 	{
 		// get all neighbours and their directions
 		
-		auto neighbouringTiles = hex.allNeighbouringTiles();
+		const BattleHexArray & neighbouringTiles = hex.getAllNeighbouringTiles();
 
 		std::vector<BattleHex::EDir> outsideNeighbourDirections;
 
@@ -491,9 +480,7 @@ std::vector<std::vector<BattleHex::EDir>> BattleFieldController::getOutsideNeigh
 			if(!neighbouringTiles[direction].isAvailable())
 				continue;
 
-			auto it = std::find(wholeRangeHexes.begin(), wholeRangeHexes.end(), neighbouringTiles[direction]);
-
-			if(it == wholeRangeHexes.end())
+			if(!wholeRangeHexes.contains(neighbouringTiles[direction]))
 				outsideNeighbourDirections.push_back(BattleHex::EDir(direction)); // push direction
 		}
 
@@ -525,9 +512,9 @@ std::vector<std::shared_ptr<IImage>> BattleFieldController::calculateRangeLimitH
 	return output;
 }
 
-void BattleFieldController::calculateRangeLimitAndHighlightImages(uint8_t distance, std::shared_ptr<CAnimation> rangeLimitImages, std::vector<BattleHex> & rangeLimitHexes, std::vector<std::shared_ptr<IImage>> & rangeLimitHexesHighlights)
+void BattleFieldController::calculateRangeLimitAndHighlightImages(uint8_t distance, std::shared_ptr<CAnimation> rangeLimitImages, BattleHexArray & rangeLimitHexes, std::vector<std::shared_ptr<IImage>> & rangeLimitHexesHighlights)
 {
-		std::vector<BattleHex> rangeHexes = getRangeHexes(hoveredHex, distance);
+		BattleHexArray rangeHexes = getRangeHexes(hoveredHex, distance);
 		rangeLimitHexes = getRangeLimitHexes(hoveredHex, rangeHexes, distance);
 		std::vector<std::vector<BattleHex::EDir>> rangeLimitNeighbourDirections = getOutsideNeighbourDirectionsForLimitHexes(rangeHexes, rangeLimitHexes);
 		rangeLimitHexesHighlights = calculateRangeLimitHighlightImages(rangeLimitNeighbourDirections, rangeLimitImages);
@@ -535,18 +522,18 @@ void BattleFieldController::calculateRangeLimitAndHighlightImages(uint8_t distan
 
 void BattleFieldController::showHighlightedHexes(Canvas & canvas)
 {
-	std::vector<BattleHex> rangedFullDamageLimitHexes;
-	std::vector<BattleHex> shootingRangeLimitHexes;
+	BattleHexArray rangedFullDamageLimitHexes;
+	BattleHexArray shootingRangeLimitHexes;
 
 	std::vector<std::shared_ptr<IImage>> rangedFullDamageLimitHexesHighlights;
 	std::vector<std::shared_ptr<IImage>> shootingRangeLimitHexesHighlights;
 
-	std::set<BattleHex> hoveredStackMovementRangeHexes = getMovementRangeForHoveredStack();
-	std::set<BattleHex> hoveredSpellHexes = getHighlightedHexesForSpellRange();
-	std::set<BattleHex> hoveredMoveHexes  = getHighlightedHexesForMovementTarget();
+	BattleHexArray hoveredStackMovementRangeHexes = getMovementRangeForHoveredStack();
+	BattleHexArray hoveredSpellHexes = getHighlightedHexesForSpellRange();
+	BattleHexArray hoveredMoveHexes  = getHighlightedHexesForMovementTarget();
 
 	BattleHex hoveredHex = getHoveredHex();
-	std::set<BattleHex> hoveredMouseHex = hoveredHex.isValid() ? std::set<BattleHex>({ hoveredHex }) : std::set<BattleHex>();
+	BattleHexArray hoveredMouseHex = hoveredHex.isValid() ? BattleHexArray({ hoveredHex }) : BattleHexArray();
 
 	const CStack * hoveredStack = getHoveredStack();
 	if(!hoveredStack && hoveredHex == BattleHex::INVALID)
@@ -573,8 +560,8 @@ void BattleFieldController::showHighlightedHexes(Canvas & canvas)
 
 	for(int hex = 0; hex < GameConstants::BFIELD_SIZE; ++hex)
 	{
-		bool stackMovement = hoveredStackMovementRangeHexes.count(hex);
-		bool mouse = hoveredMouseHexes.count(hex);
+		bool stackMovement = hoveredStackMovementRangeHexes.contains(hex);
+		bool mouse = hoveredMouseHexes.contains(hex);
 
 		// calculate if hex is Ranged Full Damage Limit and its position in highlight array
 		int hexIndexInRangedFullDamageLimit = 0;
@@ -608,7 +595,7 @@ void BattleFieldController::showHighlightedHexes(Canvas & canvas)
 	}
 }
 
-Rect BattleFieldController::hexPositionLocal(BattleHex hex) const
+Rect BattleFieldController::hexPositionLocal(const BattleHex & hex) const
 {
 	int x = 14 + ((hex.getY())%2==0 ? 22 : 0) + 44*hex.getX();
 	int y = 86 + 42 *hex.getY();
@@ -617,7 +604,7 @@ Rect BattleFieldController::hexPositionLocal(BattleHex hex) const
 	return Rect(x, y, w, h);
 }
 
-Rect BattleFieldController::hexPositionAbsolute(BattleHex hex) const
+Rect BattleFieldController::hexPositionAbsolute(const BattleHex & hex) const
 {
 	return hexPositionLocal(hex) + pos.topLeft();
 }
@@ -676,10 +663,10 @@ BattleHex BattleFieldController::getHexAtPosition(Point hoverPos)
 	return BattleHex::INVALID;
 }
 
-BattleHex::EDir BattleFieldController::selectAttackDirection(BattleHex myNumber)
+BattleHex::EDir BattleFieldController::selectAttackDirection(const BattleHex & myNumber)
 {
 	const bool doubleWide = owner.stacksController->getActiveStack()->doubleWide();
-	auto neighbours = myNumber.allNeighbouringTiles();
+	const BattleHexArray & neighbours = myNumber.getAllNeighbouringTiles();
 	//   0 1
 	//  5 x 2
 	//   4 3
@@ -696,18 +683,18 @@ BattleHex::EDir BattleFieldController::selectAttackDirection(BattleHex myNumber)
 		// |    - -   |   - -    |    - -   |   - 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));
+			attackAvailability[i] = occupiableHexes.contains(neighbours[i]) && occupiableHexes.contains(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[i] = occupiableHexes.contains(neighbours[i]) && occupiableHexes.contains(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]);
+		attackAvailability[6] = occupiableHexes.contains(neighbours[0]) && occupiableHexes.contains(neighbours[1]);
+		attackAvailability[7] = occupiableHexes.contains(neighbours[3]) && occupiableHexes.contains(neighbours[4]);
 	}
 	else
 	{
 		for (size_t i = 0; i < 6; ++i)
-			attackAvailability[i] = vstd::contains(occupiableHexes, neighbours[i]);
+			attackAvailability[i] = occupiableHexes.contains(neighbours[i]);
 
 		attackAvailability[6] = false;
 		attackAvailability[7] = false;
@@ -750,7 +737,7 @@ BattleHex::EDir BattleFieldController::selectAttackDirection(BattleHex myNumber)
 	return BattleHex::EDir(nearest);
 }
 
-BattleHex BattleFieldController::fromWhichHexAttack(BattleHex attackTarget)
+BattleHex BattleFieldController::fromWhichHexAttack(const BattleHex & attackTarget)
 {
 	BattleHex::EDir direction = selectAttackDirection(getHoveredHex());
 
@@ -819,6 +806,9 @@ BattleHex BattleFieldController::fromWhichHexAttack(BattleHex attackTarget)
 
 bool BattleFieldController::isTileAttackable(const BattleHex & number) const
 {
+	if(!number.isValid())
+		return false;
+
 	for (auto & elem : occupiableHexes)
 	{
 		if (BattleHex::mutualPosition(elem, number) != -1 || elem == number)
@@ -837,7 +827,7 @@ void BattleFieldController::updateAccessibleHexes()
 
 bool BattleFieldController::stackCountOutsideHex(const BattleHex & number) const
 {
-	return stackCountOutsideHexes[number];
+	return stackCountOutsideHexes[number.toInt()];
 }
 
 void BattleFieldController::showAll(Canvas & to)
@@ -855,7 +845,7 @@ void BattleFieldController::tick(uint32_t msPassed)
 
 void BattleFieldController::show(Canvas & to)
 {
-	CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos);
+	CanvasClipRectGuard guard(to, pos);
 
 	renderBattlefield(to);
 

+ 18 - 18
client/battle/BattleFieldController.h

@@ -9,7 +9,7 @@
  */
 #pragma once
 
-#include "../../lib/battle/BattleHex.h"
+#include "../../lib/battle/BattleHexArray.h"
 #include "../../lib/Point.h"
 #include "../gui/CIntObject.h"
 
@@ -50,39 +50,39 @@ class BattleFieldController : public CIntObject
 	BattleHex hoveredHex;
 
 	/// hexes to which currently active stack can move
-	std::vector<BattleHex> occupiableHexes;
+	BattleHexArray 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);
+	void showHighlightedHex(Canvas & to, std::shared_ptr<IImage> highlight, const BattleHex & hex, bool darkBorder);
 
-	std::set<BattleHex> getHighlightedHexesForActiveStack();
-	std::set<BattleHex> getMovementRangeForHoveredStack();
-	std::set<BattleHex> getHighlightedHexesForSpellRange();
-	std::set<BattleHex> getHighlightedHexesForMovementTarget();
+	BattleHexArray getHighlightedHexesForActiveStack();
+	BattleHexArray getMovementRangeForHoveredStack();
+	BattleHexArray getHighlightedHexesForSpellRange();
+	BattleHexArray getHighlightedHexesForMovementTarget();
 
 	// Range limit highlight helpers
 
 	/// get all hexes within a certain distance of given hex
-	std::vector<BattleHex> getRangeHexes(BattleHex sourceHex, uint8_t distance);
+	BattleHexArray getRangeHexes(const 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);
+	BattleHexArray getRangeLimitHexes(const BattleHex & hoveredHex, const BattleHexArray & 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);
+	bool IsHexInRangeLimit(const BattleHex & hex, const BattleHexArray & rangeLimitHexes, int * hexIndexInRangeLimit);
 
 	/// get an array that has for each hex in range, an array with all directions where an outside neighbour hex exists
-	std::vector<std::vector<BattleHex::EDir>> getOutsideNeighbourDirectionsForLimitHexes(std::vector<BattleHex> rangeHexes, std::vector<BattleHex> rangeLimitHexes);
+	std::vector<std::vector<BattleHex::EDir>> getOutsideNeighbourDirectionsForLimitHexes(const BattleHexArray & rangeHexes, const BattleHexArray & rangeLimitHexes);
 
-	/// calculates what image to use as range limit, depending on the direction of neighbors
+	/// calculates what image to use as range limit, depending on the direction of neighbours
 	/// 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>> & rangeLimitHexesHighlights);
+	void calculateRangeLimitAndHighlightImages(uint8_t distance, std::shared_ptr<CAnimation> rangeLimitImages, BattleHexArray & rangeLimitHexes, std::vector<std::shared_ptr<IImage>> & rangeLimitHexesHighlights);
 
 	void showBackground(Canvas & canvas);
 	void showBackgroundImage(Canvas & canvas);
@@ -94,7 +94,7 @@ class BattleFieldController : public CIntObject
 
 	/// Checks whether selected pixel is transparent, uses local coordinates of a hex
 	bool isPixelInHex(Point const & position);
-	size_t selectBattleCursor(BattleHex myNumber);
+	size_t selectBattleCursor(const 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;
@@ -118,10 +118,10 @@ public:
 	void renderBattlefield(Canvas & canvas);
 
 	/// Returns position of hex relative to owner (BattleInterface)
-	Rect hexPositionLocal(BattleHex hex) const;
+	Rect hexPositionLocal(const BattleHex & hex) const;
 
 	/// Returns position of hex relative to game window
-	Rect hexPositionAbsolute(BattleHex hex) const;
+	Rect hexPositionAbsolute(const BattleHex & hex) const;
 
 	/// Returns ID of currently hovered hex or BattleHex::INVALID if none
 	BattleHex getHoveredHex();
@@ -135,7 +135,7 @@ public:
 	/// 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::EDir selectAttackDirection(const BattleHex & myNumber);
 
-	BattleHex fromWhichHexAttack(BattleHex myNumber);
+	BattleHex fromWhichHexAttack(const BattleHex & myNumber);
 };

+ 6 - 6
client/battle/BattleInterface.cpp

@@ -216,7 +216,7 @@ void BattleInterface::stackActivated(const CStack *stack)
 	stacksController->stackActivated(stack);
 }
 
-void BattleInterface::stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance, bool teleport)
+void BattleInterface::stackMoved(const CStack *stack, const BattleHexArray & destHex, int distance, bool teleport)
 {
 	if (teleport)
 		stacksController->stackTeleported(stack, destHex, distance);
@@ -261,7 +261,7 @@ void BattleInterface::newRound()
 	round++;
 }
 
-void BattleInterface::giveCommand(EActionType action, BattleHex tile, SpellID spell)
+void BattleInterface::giveCommand(EActionType action, const BattleHex & tile, SpellID spell)
 {
 	const CStack * actor = nullptr;
 	if(action != EActionType::HERO_SPELL && action != EActionType::RETREAT && action != EActionType::SURRENDER)
@@ -506,7 +506,7 @@ void BattleInterface::displayBattleLog(const std::vector<MetaString> & battleLog
 	}
 }
 
-void BattleInterface::displaySpellAnimationQueue(const CSpell * spell, const CSpell::TAnimationQueue & q, BattleHex destinationTile, bool isHit)
+void BattleInterface::displaySpellAnimationQueue(const CSpell * spell, const CSpell::TAnimationQueue & q, const BattleHex & destinationTile, bool isHit)
 {
 	for(const CSpell::TAnimation & animation : q)
 	{
@@ -542,19 +542,19 @@ void BattleInterface::displaySpellAnimationQueue(const CSpell * spell, const CSp
 	}
 }
 
-void BattleInterface::displaySpellCast(const CSpell * spell, BattleHex destinationTile)
+void BattleInterface::displaySpellCast(const CSpell * spell, const BattleHex & destinationTile)
 {
 	if(spell)
 		displaySpellAnimationQueue(spell, spell->animationInfo.cast, destinationTile, false);
 }
 
-void BattleInterface::displaySpellEffect(const CSpell * spell, BattleHex destinationTile)
+void BattleInterface::displaySpellEffect(const CSpell * spell, const BattleHex & destinationTile)
 {
 	if(spell)
 		displaySpellAnimationQueue(spell, spell->animationInfo.affect, destinationTile, false);
 }
 
-void BattleInterface::displaySpellHit(const CSpell * spell, BattleHex destinationTile)
+void BattleInterface::displaySpellHit(const CSpell * spell, const BattleHex & destinationTile)
 {
 	if(spell)
 		displaySpellAnimationQueue(spell, spell->animationInfo.hit, destinationTile, true);

+ 7 - 7
client/battle/BattleInterface.h

@@ -10,6 +10,7 @@
 #pragma once
 
 #include "BattleConstants.h"
+#include "../lib/battle/BattleHex.h"
 #include "../gui/CIntObject.h"
 #include "../../lib/spells/CSpellHandler.h" //CSpell::TAnimation
 #include "../ConditionalWait.h"
@@ -27,7 +28,6 @@ class BattleAction;
 class CGTownInstance;
 struct CatapultAttack;
 struct BattleTriggerEffect;
-struct BattleHex;
 struct InfoAboutHero;
 class ObstacleChanges;
 class CPlayerBattleCallback;
@@ -163,7 +163,7 @@ public:
 	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 giveCommand(EActionType action, const 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
@@ -202,7 +202,7 @@ public:
 	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 stackMoved(const CStack *stack, const BattleHexArray & 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();
@@ -215,10 +215,10 @@ public:
 
 	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 displaySpellAnimationQueue(const CSpell * spell, const CSpell::TAnimationQueue & q, const BattleHex & destinationTile, bool isHit);
+	void displaySpellCast(const CSpell * spell, const BattleHex & destinationTile); //displays spell`s cast animation
+	void displaySpellEffect(const CSpell * spell, const BattleHex & destinationTile); //displays spell`s affected animation
+	void displaySpellHit(const CSpell * spell, const BattleHex & destinationTile); //displays spell`s affected animation
 
 	void endAction(const BattleAction & action);
 

+ 1 - 1
client/battle/BattleObstacleController.h

@@ -13,7 +13,7 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-struct BattleHex;
+class BattleHex;
 struct CObstacleInstance;
 class JsonNode;
 class ObstacleChanges;

+ 1 - 1
client/battle/BattleOverlayLogVisualizer.cpp

@@ -29,7 +29,7 @@ BattleOverlayLogVisualizer::BattleOverlayLogVisualizer(
 {
 }
 
-void BattleOverlayLogVisualizer::drawText(BattleHex hex, int lineNumber, const std::string & text)
+void BattleOverlayLogVisualizer::drawText(const BattleHex & hex, int lineNumber, const std::string & text)
 {
 	Point offset = owner.fieldController->hexPositionLocal(hex).topLeft() + Point(20, 20);
 	const auto & font = GH.renderHandler().loadFont(FONT_TINY);

+ 1 - 1
client/battle/BattleOverlayLogVisualizer.h

@@ -24,5 +24,5 @@ private:
 public:
 	BattleOverlayLogVisualizer(BattleRenderer::RendererRef & target, BattleInterface & owner);
 
-	void drawText(BattleHex hex, int lineNumber, const std::string & text) override;
+	void drawText(const BattleHex & hex, int lineNumber, const std::string & text) override;
 };

+ 1 - 1
client/battle/BattleRenderer.cpp

@@ -64,7 +64,7 @@ BattleRenderer::BattleRenderer(BattleInterface & owner):
 {
 }
 
-void BattleRenderer::insert(EBattleFieldLayer layer, BattleHex tile, BattleRenderer::RenderFunctor functor)
+void BattleRenderer::insert(EBattleFieldLayer layer, const BattleHex & tile, BattleRenderer::RenderFunctor functor)
 {
 	objects.push_back({functor, layer, tile});
 }

+ 1 - 1
client/battle/BattleRenderer.h

@@ -48,6 +48,6 @@ private:
 public:
 	BattleRenderer(BattleInterface & owner);
 
-	void insert(EBattleFieldLayer layer, BattleHex tile, RenderFunctor functor);
+	void insert(EBattleFieldLayer layer, const BattleHex & tile, RenderFunctor functor);
 	void execute(RendererRef targetCanvas);
 };

+ 5 - 5
client/battle/BattleSiegeController.cpp

@@ -183,9 +183,9 @@ BattleSiegeController::BattleSiegeController(BattleInterface & owner, const CGTo
 	}
 }
 
-const CCreature *BattleSiegeController::getTurretCreature(BattleHex position) const
+const CCreature *BattleSiegeController::getTurretCreature(const BattleHex & position) const
 {
-	switch (position)
+	switch (position.toInt())
 	{
 		case BattleHex::CASTLE_CENTRAL_TOWER:
 			return town->fortificationsLevel().citadelShooter.toCreature();
@@ -195,14 +195,14 @@ const CCreature *BattleSiegeController::getTurretCreature(BattleHex position) co
 			return town->fortificationsLevel().lowerTowerShooter.toCreature();
 	}
 
-	throw std::runtime_error("Unable to select shooter for tower at " + std::to_string(position.hex));
+	throw std::runtime_error("Unable to select shooter for tower at " + std::to_string(position.toInt()));
 }
 
 Point BattleSiegeController::getTurretCreaturePosition( BattleHex position ) const
 {
 	// Turret positions are read out of the config/wall_pos.txt
 	int posID = 0;
-	switch (position)
+	switch (position.toInt())
 	{
 	case BattleHex::CASTLE_CENTRAL_TOWER: // keep creature
 		posID = EWallVisual::CREATURE_KEEP;
@@ -322,7 +322,7 @@ void BattleSiegeController::collectRenderableObjects(BattleRenderer & renderer)
 	}
 }
 
-bool BattleSiegeController::isAttackableByCatapult(BattleHex hex) const
+bool BattleSiegeController::isAttackableByCatapult(const BattleHex & hex) const
 {
 	if (owner.tacticsMode)
 		return false;

+ 2 - 2
client/battle/BattleSiegeController.h

@@ -102,9 +102,9 @@ public:
 	void collectRenderableObjects(BattleRenderer & renderer);
 
 	/// queries from other battle controllers
-	bool isAttackableByCatapult(BattleHex hex) const;
+	bool isAttackableByCatapult(const BattleHex & hex) const;
 	ImagePath getBattleBackgroundName() const;
-	const CCreature *getTurretCreature(BattleHex turretPosition) const;
+	const CCreature *getTurretCreature(const BattleHex & turretPosition) const;
 	Point getTurretCreaturePosition( BattleHex position ) const;
 
 	const CGTownInstance *getSiegedTown() const;

+ 3 - 3
client/battle/BattleStacksController.cpp

@@ -491,7 +491,7 @@ void BattleStacksController::stacksAreAttacked(std::vector<StackAttackedInfo> at
 	owner.waitForAnimations();
 }
 
-void BattleStacksController::stackTeleported(const CStack *stack, std::vector<BattleHex> destHex, int distance)
+void BattleStacksController::stackTeleported(const CStack *stack, const BattleHexArray & destHex, int distance)
 {
 	assert(destHex.size() > 0);
 	//owner.checkForAnimations(); // NOTE: at this point spellcast animations were added, but not executed
@@ -508,7 +508,7 @@ void BattleStacksController::stackTeleported(const CStack *stack, std::vector<Ba
 	// animations will be executed by spell
 }
 
-void BattleStacksController::stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance)
+void BattleStacksController::stackMoved(const CStack *stack, const BattleHexArray & destHex, int distance)
 {
 	assert(destHex.size() > 0);
 	owner.checkForAnimations();
@@ -733,7 +733,7 @@ bool BattleStacksController::facingRight(const CStack * stack) const
 	return stackFacingRight.at(stack->unitId());
 }
 
-Point BattleStacksController::getStackPositionAtHex(BattleHex hexNum, const CStack * stack) const
+Point BattleStacksController::getStackPositionAtHex(const BattleHex & hexNum, const CStack * stack) const
 {
 	Point ret(-500, -500); //returned value
 	if(stack && stack->initialPosition < 0) //creatures in turrets

+ 5 - 4
client/battle/BattleStacksController.h

@@ -13,7 +13,8 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-struct BattleHex;
+class BattleHex;
+class BattleHexArray;
 class BattleAction;
 class CStack;
 class CSpell;
@@ -109,8 +110,8 @@ public:
 	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 stackMoved(const CStack *stack, const BattleHexArray & destHex, int distance); //stack with id number moved to destHex
+	void stackTeleported(const CStack *stack, const BattleHexArray & 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
 
@@ -142,7 +143,7 @@ public:
 	void tick(uint32_t msPassed);
 
 	/// returns position of animation needed to place stack in specific hex
-	Point getStackPositionAtHex(BattleHex hexNum, const CStack * creature) const;
+	Point getStackPositionAtHex(const BattleHex & hexNum, const CStack * creature) const;
 
 	friend class BattleAnimation; // for exposing pendingAnims/creAnims/creDir to animations
 };

+ 86 - 0
client/globalLobby/GlobalLobbyAddChannelWindow.cpp

@@ -0,0 +1,86 @@
+/*
+ * GlobalLobbyAddChannelWindow.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 "GlobalLobbyAddChannelWindow.h"
+
+#include "GlobalLobbyClient.h"
+
+#include "../CServerHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
+#include "../gui/WindowHandler.h"
+#include "../widgets/Buttons.h"
+#include "../widgets/GraphicalPrimitiveCanvas.h"
+#include "../widgets/Images.h"
+#include "../widgets/ObjectLists.h"
+#include "../widgets/TextControls.h"
+
+#include "../../lib/texts/MetaString.h"
+#include "../../lib/texts/Languages.h"
+
+GlobalLobbyAddChannelWindowCard::GlobalLobbyAddChannelWindowCard(const std::string & languageID)
+	: languageID(languageID)
+{
+	pos.w = 200;
+	pos.h = 40;
+	addUsedEvents(LCLICK);
+
+	OBJECT_CONSTRUCTION;
+	const auto & language = Languages::getLanguageOptions(languageID);
+
+	backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1);
+	labelNameNative = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, language.nameNative);
+
+	if (language.nameNative != language.nameEnglish)
+		labelNameTranslated = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, language.nameEnglish);
+}
+
+void GlobalLobbyAddChannelWindowCard::clickPressed(const Point & cursorPosition)
+{
+	CSH->getGlobalLobby().addChannel(languageID);
+	GH.windows().popWindows(1);
+}
+
+GlobalLobbyAddChannelWindow::GlobalLobbyAddChannelWindow()
+	: CWindowObject(BORDERED)
+{
+	OBJECT_CONSTRUCTION;
+
+	pos.w = 236;
+	pos.h = 420;
+
+	filledBackground = std::make_shared<FilledTexturePlayerColored>(Rect(0, 0, pos.w, pos.h));
+	filledBackground->setPlayerColor(PlayerColor(1));
+	labelTitle = std::make_shared<CLabel>(
+		pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.channel.add").toString()
+		);
+
+	const auto & allLanguages = Languages::getLanguageList();
+	std::vector<std::string> newLanguages;
+	for (const auto & language : allLanguages)
+		if (!vstd::contains(CSH->getGlobalLobby().getActiveChannels(), language.identifier))
+			newLanguages.push_back(language.identifier);
+
+	const auto & createChannelCardCallback = [newLanguages](size_t index) -> std::shared_ptr<CIntObject>
+	{
+		if(index < newLanguages.size())
+			return std::make_shared<GlobalLobbyAddChannelWindowCard>(newLanguages[index]);
+		return std::make_shared<CIntObject>();
+	};
+
+	listBackground = std::make_shared<TransparentFilledRectangle>(Rect(8, 48, 220, 324), ColorRGBA(0, 0, 0, 64), ColorRGBA(64, 80, 128, 255), 1);
+	languageList = std::make_shared<CListBox>(createChannelCardCallback, Point(10, 50), Point(0, 40), 8, newLanguages.size(), 0, 1 | 4, Rect(200, 0, 320, 320));
+	languageList->setRedrawParent(true);
+
+	buttonClose = std::make_shared<CButton>(Point(86, 384), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this]() { close(); }, EShortcut::GLOBAL_RETURN );
+
+	center();
+}

+ 46 - 0
client/globalLobby/GlobalLobbyAddChannelWindow.h

@@ -0,0 +1,46 @@
+/*
+ * GlobalLobbyInviteWindow.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 "GlobalLobbyObserver.h"
+
+#include "../windows/CWindowObject.h"
+
+class CLabel;
+class FilledTexturePlayerColored;
+class TransparentFilledRectangle;
+class CListBox;
+class CButton;
+struct GlobalLobbyAccount;
+
+class GlobalLobbyAddChannelWindow final : public CWindowObject
+{
+	std::shared_ptr<FilledTexturePlayerColored> filledBackground;
+	std::shared_ptr<CLabel> labelTitle;
+	std::shared_ptr<CListBox> languageList;
+	std::shared_ptr<TransparentFilledRectangle> listBackground;
+	std::shared_ptr<CButton> buttonClose;
+
+public:
+	GlobalLobbyAddChannelWindow();
+};
+
+class GlobalLobbyAddChannelWindowCard : public CIntObject
+{
+	std::string languageID;
+
+	std::shared_ptr<TransparentFilledRectangle> backgroundOverlay;
+	std::shared_ptr<CLabel> labelNameNative;
+	std::shared_ptr<CLabel> labelNameTranslated;
+
+	void clickPressed(const Point & cursorPosition) override;
+public:
+	GlobalLobbyAddChannelWindowCard(const std::string & languageID);
+};

+ 53 - 4
client/globalLobby/GlobalLobbyClient.cpp

@@ -32,9 +32,54 @@
 
 GlobalLobbyClient::GlobalLobbyClient()
 {
-	activeChannels.emplace_back("english");
-	if (CGI->generaltexth->getPreferredLanguage() != "english")
-		activeChannels.emplace_back(CGI->generaltexth->getPreferredLanguage());
+	auto customChannels = settings["lobby"]["languageRooms"].convertTo<std::vector<std::string>>();
+
+	if (customChannels.empty())
+	{
+		activeChannels.emplace_back("english");
+		if (CGI->generaltexth->getPreferredLanguage() != "english")
+			activeChannels.emplace_back(CGI->generaltexth->getPreferredLanguage());
+	}
+	else
+	{
+		activeChannels = customChannels;
+	}
+}
+
+void GlobalLobbyClient::addChannel(const std::string & channel)
+{
+	activeChannels.emplace_back(channel);
+
+	auto lobbyWindowPtr = lobbyWindow.lock();
+	if(lobbyWindowPtr)
+		lobbyWindowPtr->refreshActiveChannels();
+
+	JsonNode toSend;
+	toSend["type"].String() = "requestChatHistory";
+	toSend["channelType"].String() = "global";
+	toSend["channelName"].String() = channel;
+	CSH->getGlobalLobby().sendMessage(toSend);
+
+	Settings languageRooms = settings.write["lobby"]["languageRooms"];
+
+	languageRooms->Vector().clear();
+	for (const auto & lang : activeChannels)
+		languageRooms->Vector().push_back(JsonNode(lang));
+}
+
+void GlobalLobbyClient::closeChannel(const std::string & channel)
+{
+	vstd::erase(activeChannels, channel);
+
+	auto lobbyWindowPtr = lobbyWindow.lock();
+	if(lobbyWindowPtr)
+		lobbyWindowPtr->refreshActiveChannels();
+
+	Settings languageRooms = settings.write["lobby"]["languageRooms"];
+
+	languageRooms->Vector().clear();
+	for (const auto & lang : activeChannels)
+		languageRooms->Vector().push_back(JsonNode(lang));
 }
 
 GlobalLobbyClient::~GlobalLobbyClient() = default;
@@ -171,7 +216,7 @@ void GlobalLobbyClient::receiveChatMessage(const JsonNode & json)
 		lobbyWindowPtr->onGameChatMessage(message.displayName, message.messageText, message.timeFormatted, channelType, channelName);
 		lobbyWindowPtr->refreshChatText();
 
-		if(channelType == "player" || lobbyWindowPtr->isChannelOpen(channelType, channelName))
+		if(channelType == "player" || (lobbyWindowPtr->isChannelOpen(channelType, channelName) && lobbyWindowPtr->isActive()))
 			CCS->soundh->playSound(AudioPath::builtin("CHAT"));
 	}
 }
@@ -347,6 +392,10 @@ void GlobalLobbyClient::sendClientLogin()
 	toSend["accountCookie"].String() = getAccountCookie();
 	toSend["language"].String() = CGI->generaltexth->getPreferredLanguage();
 	toSend["version"].String() = VCMI_VERSION_STRING;
+
+	for (const auto & language : activeChannels)
+		toSend["languageRooms"].Vector().push_back(JsonNode(language));
+
 	sendMessage(toSend);
 }
 

+ 2 - 0
client/globalLobby/GlobalLobbyClient.h

@@ -92,6 +92,8 @@ public:
 	void sendClientRegister(const std::string & accountName);
 	void sendClientLogin();
 	void sendOpenRoom(const std::string & mode, int playerLimit);
+	void addChannel(const std::string & channel);
+	void closeChannel(const std::string & channel);
 
 	void sendProxyConnectionLogin(const NetworkConnectionPtr & netConnection);
 	void resetMatchState();

+ 21 - 1
client/globalLobby/GlobalLobbyWidget.cpp

@@ -11,9 +11,10 @@
 #include "StdInc.h"
 #include "GlobalLobbyWidget.h"
 
+#include "GlobalLobbyAddChannelWindow.h"
 #include "GlobalLobbyClient.h"
-#include "GlobalLobbyWindow.h"
 #include "GlobalLobbyRoomWindow.h"
+#include "GlobalLobbyWindow.h"
 
 #include "../CGameInfo.h"
 #include "../CServerHandler.h"
@@ -72,6 +73,18 @@ GlobalLobbyWidget::CreateFunc GlobalLobbyWidget::getItemListConstructorFunc(cons
 
 		if(index < channels.size())
 			return std::make_shared<GlobalLobbyChannelCard>(this->window, channels[index]);
+
+		if(index == channels.size())
+		{
+			const auto buttonCallback = [](){
+				GH.windows().createAndPushWindow<GlobalLobbyAddChannelWindow>();
+			};
+
+			auto result = std::make_shared<CButton>(Point(0,0), AnimationPath::builtin("lobbyAddChannel"), CButton::tooltip(), buttonCallback);
+			result->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("lobby/addChannel")));
+			return result;
+		}
+
 		return std::make_shared<CIntObject>();
 	};
 
@@ -255,6 +268,13 @@ GlobalLobbyChannelCard::GlobalLobbyChannelCard(GlobalLobbyWindow * window, const
 {
 	OBJECT_CONSTRUCTION;
 	labelName = std::make_shared<CLabel>(5, 20, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, Languages::getLanguageOptions(channelName).nameNative);
+
+	if (CSH->getGlobalLobby().getActiveChannels().size() > 1)
+	{
+		pos.w = 110;
+		buttonClose = std::make_shared<CButton>(Point(113, 7), AnimationPath::builtin("lobbyCloseChannel"), CButton::tooltip(), [channelName](){CSH->getGlobalLobby().closeChannel(channelName);});
+		buttonClose->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("lobby/closeChannel")));
+	}
 }
 
 GlobalLobbyMatchCard::GlobalLobbyMatchCard(GlobalLobbyWindow * window, const GlobalLobbyRoom & matchDescription)

+ 1 - 1
client/globalLobby/GlobalLobbyWidget.h

@@ -77,7 +77,6 @@ class GlobalLobbyRoomCard : public CIntObject
 	std::shared_ptr<CLabel> labelRoomSize;
 	std::shared_ptr<CLabel> labelRoomStatus;
 	std::shared_ptr<CLabel> labelDescription;
-	std::shared_ptr<CButton> buttonJoin;
 	std::shared_ptr<CPicture> iconRoomSize;
 
 	void clickPressed(const Point & cursorPosition) override;
@@ -88,6 +87,7 @@ public:
 class GlobalLobbyChannelCard : public GlobalLobbyChannelCardBase
 {
 	std::shared_ptr<CLabel> labelName;
+	std::shared_ptr<CButton> buttonClose;
 
 public:
 	GlobalLobbyChannelCard(GlobalLobbyWindow * window, const std::string & channelName);

+ 16 - 0
client/globalLobby/GlobalLobbyWindow.cpp

@@ -39,6 +39,7 @@ GlobalLobbyWindow::GlobalLobbyWindow()
 	doOpenChannel("global", "english", Languages::getLanguageOptions("english").nameNative);
 
 	widget->getChannelListHeader()->setText(MetaString::createFromTextID("vcmi.lobby.header.channels").toString());
+	widget->getChannelList()->resize(CSH->getGlobalLobby().getActiveChannels().size()+1);
 }
 
 bool GlobalLobbyWindow::isChannelOpen(const std::string & testChannelType, const std::string & testChannelName) const
@@ -182,6 +183,21 @@ void GlobalLobbyWindow::onMatchesHistory(const std::vector<GlobalLobbyRoom> & hi
 	widget->getMatchListHeader()->setText(text.toString());
 }
 
+void GlobalLobbyWindow::refreshActiveChannels()
+{
+	const auto & activeChannels = CSH->getGlobalLobby().getActiveChannels();
+
+	if (activeChannels.size()+1 == widget->getChannelList()->size())
+		widget->getChannelList()->reset();
+	else
+		widget->getChannelList()->resize(activeChannels.size()+1);
+
+	if (currentChannelType == "global" && !vstd::contains(activeChannels, currentChannelName) && !activeChannels.empty())
+	{
+		doOpenChannel("global", activeChannels.front(), Languages::getLanguageOptions(activeChannels.front()).nameNative);
+	}
+}
+
 void GlobalLobbyWindow::onInviteReceived(const std::string & invitedRoomID)
 {
 	widget->getRoomList()->reset();

+ 1 - 0
client/globalLobby/GlobalLobbyWindow.h

@@ -45,6 +45,7 @@ public:
 
 	void onGameChatMessage(const std::string & sender, const std::string & message, const std::string & when, const std::string & channelType, const std::string & channelName);
 	void refreshChatText();
+	void refreshActiveChannels();
 	void onActiveAccounts(const std::vector<GlobalLobbyAccount> & accounts) override;
 	void onActiveGameRooms(const std::vector<GlobalLobbyRoom> & rooms) override;
 	void onMatchesHistory(const std::vector<GlobalLobbyRoom> & history);

+ 4 - 1
client/gui/CIntObject.cpp

@@ -343,6 +343,9 @@ WindowBase::WindowBase(int used_, Point pos_)
 void WindowBase::close()
 {
 	if(!GH.windows().isTopWindow(this))
-		throw std::runtime_error("Only top interface can be closed");
+	{
+		auto topWindow = GH.windows().topWindow<IShowActivatable>().get();
+		throw std::runtime_error(std::string("Only top interface can be closed! Top window is ") + typeid(*this).name() + " but attempted to close " + typeid(*topWindow).name());
+	}
 	GH.windows().popWindows(1);
 }

+ 13 - 3
client/gui/EventDispatcher.cpp

@@ -201,10 +201,20 @@ void EventDispatcher::dispatchShowPopup(const Point & position, int tolerance)
 
 void EventDispatcher::dispatchClosePopup(const Point & position)
 {
-	if (GH.windows().isTopWindowPopup())
-		GH.windows().popWindows(1);
+	bool popupOpen = GH.windows().isTopWindowPopup(); // popup can already be closed for mouse dragging with RMB
+
+	auto hlp = rclickable;
 
-	assert(!GH.windows().isTopWindowPopup());
+	for(auto & i : hlp)
+	{
+		if(!vstd::contains(rclickable, i))
+			continue;
+
+		i->closePopupWindow(!popupOpen);
+	}
+
+	if(popupOpen)
+		GH.windows().popWindows(1);
 }
 
 void EventDispatcher::handleLeftButtonClick(const Point & position, int tolerance, bool isPressed)

+ 1 - 0
client/gui/EventsReceiver.h

@@ -50,6 +50,7 @@ public:
 	virtual void clickReleased(const Point & cursorPosition, bool lastActivated);
 	virtual void clickCancel(const Point & cursorPosition) {}
 	virtual void showPopupWindow(const Point & cursorPosition) {}
+	virtual void closePopupWindow(bool alreadyClosed) {}
 	virtual void clickDouble(const Point & cursorPosition) {}
 	virtual void notFocusedClick() {};
 

Vissa filer visades inte eftersom för många filer har ändrats