ソースを参照

Merge branch 'develop' into patch-8

Ivan Savenko 10 ヶ月 前
コミット
75e2339cec
100 ファイル変更8633 行追加2834 行削除
  1. 1 5
      AI/BattleAI/BattleAI.cpp
  2. 0 1
      AI/BattleAI/BattleAI.h
  3. 35 8
      AI/BattleAI/BattleEvaluator.cpp
  4. 1 0
      AI/BattleAI/BattleEvaluator.h
  5. 31 2
      AI/BattleAI/BattleExchangeVariant.cpp
  6. 2 1
      AI/BattleAI/BattleExchangeVariant.h
  7. 58 3
      AI/Nullkiller/Analyzers/ArmyManager.cpp
  8. 1 1
      AI/Nullkiller/Analyzers/ObjectClusterizer.cpp
  9. 1 1
      AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp
  10. 15 3
      AI/Nullkiller/Behaviors/DefenceBehavior.cpp
  11. 1 0
      AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp
  12. 6 1
      AI/Nullkiller/Engine/Nullkiller.cpp
  13. 39 17
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  14. 2 0
      AI/Nullkiller/Engine/PriorityEvaluator.h
  15. 30 1
      AI/Nullkiller/Goals/BuyArmy.cpp
  16. 6 4
      AI/Nullkiller/Pathfinding/Actors.cpp
  17. 45 36
      CI/NSIS.template.in
  18. 12 1
      ChangeLog.md
  19. 41 0
      Mods/vcmi/Content/config/chinese.json
  20. 1 1
      Mods/vcmi/Content/config/czech.json
  21. 1 1
      Mods/vcmi/Content/config/english.json
  22. 39 1
      Mods/vcmi/Content/config/german.json
  23. 76 8
      Mods/vcmi/Content/config/polish.json
  24. 30 30
      Mods/vcmi/Content/config/portuguese.json
  25. 25 3
      Mods/vcmi/Content/config/swedish.json
  26. 157 4
      Mods/vcmi/Content/config/ukrainian.json
  27. 2 2
      android/vcmi-app/build.gradle
  28. 2 1
      client/NetPacksClient.cpp
  29. 2 1
      client/battle/BattleInterfaceClasses.cpp
  30. 13 6
      client/eventsSDL/InputSourceKeyboard.cpp
  31. 5 1
      client/eventsSDL/InputSourceKeyboard.h
  32. 12 3
      client/media/CVideoHandler.cpp
  33. 3 0
      client/media/CVideoHandler.h
  34. 35 27
      client/render/CBitmapHandler.cpp
  35. 5 5
      client/render/IImage.h
  36. 1 1
      client/renderSDL/CBitmapFont.cpp
  37. 3 1
      client/renderSDL/ImageScaled.cpp
  38. 18 4
      client/renderSDL/SDLImage.cpp
  39. 5 5
      client/renderSDL/SDLImage.h
  40. 8 3
      client/renderSDL/SDL_Extensions.cpp
  41. 2 1
      client/renderSDL/SDL_Extensions.h
  42. 1 1
      client/widgets/VideoWidget.cpp
  43. 7 4
      client/windows/CCreatureWindow.cpp
  44. 0 5
      client/windows/settings/SettingsMainWindow.cpp
  45. 0 1
      client/windows/settings/SettingsMainWindow.h
  46. 17 0
      client/xBRZ/xbrz.cpp
  47. 1 0
      client/xBRZ/xbrz.h
  48. 1 1
      cmake_modules/VersionDefinition.cmake
  49. 1 1
      config/commanders.json
  50. 7 1
      debian/changelog
  51. 3 5
      docs/Readme.md
  52. 3 0
      launcher/CMakeLists.txt
  53. 2 1
      launcher/eu.vcmi.VCMI.metainfo.xml
  54. 25 16
      launcher/firstLaunch/firstlaunch_moc.cpp
  55. 0 2
      launcher/firstLaunch/firstlaunch_moc.h
  56. 12 10
      launcher/firstLaunch/firstlaunch_moc.ui
  57. 2 0
      launcher/innoextract.cpp
  58. 105 17
      launcher/mainwindow_moc.cpp
  59. 16 1
      launcher/mainwindow_moc.h
  60. 37 77
      launcher/mainwindow_moc.ui
  61. 49 45
      launcher/modManager/chroniclesextractor.cpp
  62. 11 10
      launcher/modManager/chroniclesextractor.h
  63. 153 129
      launcher/modManager/cmodlistview_moc.cpp
  64. 30 21
      launcher/modManager/cmodlistview_moc.h
  65. 9 36
      launcher/modManager/cmodlistview_moc.ui
  66. 7 11
      launcher/modManager/modstatecontroller.cpp
  67. 1 3
      launcher/modManager/modstatecontroller.h
  68. 23 26
      launcher/modManager/modstateitemmodel_moc.cpp
  69. 1 1
      launcher/modManager/modstateitemmodel_moc.h
  70. 33 4
      launcher/modManager/modstatemodel.cpp
  71. 9 1
      launcher/modManager/modstatemodel.h
  72. 7 17
      launcher/settingsView/csettingsview_moc.cpp
  73. 5 5
      launcher/settingsView/csettingsview_moc.ui
  74. 431 0
      launcher/startGame/StartGameTab.cpp
  75. 83 0
      launcher/startGame/StartGameTab.h
  76. 857 0
      launcher/startGame/StartGameTab.ui
  77. 592 137
      launcher/translation/chinese.ts
  78. 554 236
      launcher/translation/czech.ts
  79. 487 210
      launcher/translation/english.ts
  80. 532 149
      launcher/translation/french.ts
  81. 349 177
      launcher/translation/german.ts
  82. 317 151
      launcher/translation/polish.ts
  83. 551 241
      launcher/translation/portuguese.ts
  84. 503 197
      launcher/translation/russian.ts
  85. 504 182
      launcher/translation/spanish.ts
  86. 541 120
      launcher/translation/swedish.ts
  87. 330 179
      launcher/translation/ukrainian.ts
  88. 501 185
      launcher/translation/vietnamese.ts
  89. 1 1
      launcher/updatedialog_moc.cpp
  90. 9 3
      lib/CCreatureHandler.cpp
  91. 1 1
      lib/CCreatureHandler.h
  92. 7 2
      lib/gameState/CGameState.cpp
  93. 2 1
      lib/mapObjects/CGHeroInstance.cpp
  94. 1 2
      lib/modding/ModDescription.cpp
  95. 95 2
      lib/modding/ModManager.cpp
  96. 16 0
      lib/modding/ModManager.h
  97. 4 0
      lib/network/NetworkConnection.cpp
  98. 4 0
      lib/network/NetworkDefines.h
  99. 11 12
      lib/networkPacks/NetPacksLib.cpp
  100. BIN
      mapeditor/icons/document-open-recent.png

+ 1 - 5
AI/BattleAI/BattleAI.cpp

@@ -167,14 +167,12 @@ void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
 
 
 		result = evaluator.selectStackAction(stack);
 		result = evaluator.selectStackAction(stack);
 
 
-		if(autobattlePreferences.enableSpellsUsage && !skipCastUntilNextBattle && evaluator.canCastSpell())
+		if(autobattlePreferences.enableSpellsUsage && evaluator.canCastSpell())
 		{
 		{
 			auto spelCasted = evaluator.attemptCastingSpell(stack);
 			auto spelCasted = evaluator.attemptCastingSpell(stack);
 
 
 			if(spelCasted)
 			if(spelCasted)
 				return;
 				return;
-			
-			skipCastUntilNextBattle = true;
 		}
 		}
 
 
 		logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start));
 		logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start));
@@ -256,8 +254,6 @@ void CBattleAI::battleStart(const BattleID & battleID, const CCreatureSet *army1
 {
 {
 	LOG_TRACE(logAi);
 	LOG_TRACE(logAi);
 	side = Side;
 	side = Side;
-
-	skipCastUntilNextBattle = false;
 }
 }
 
 
 void CBattleAI::print(const std::string &text) const
 void CBattleAI::print(const std::string &text) const

+ 0 - 1
AI/BattleAI/BattleAI.h

@@ -62,7 +62,6 @@ class CBattleAI : public CBattleGameInterface
 	bool wasWaitingForRealize;
 	bool wasWaitingForRealize;
 	bool wasUnlockingGs;
 	bool wasUnlockingGs;
 	int movesSkippedByDefense;
 	int movesSkippedByDefense;
-	bool skipCastUntilNextBattle;
 
 
 public:
 public:
 	CBattleAI();
 	CBattleAI();

+ 35 - 8
AI/BattleAI/BattleEvaluator.cpp

@@ -119,6 +119,14 @@ std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const
 	return result;
 	return result;
 }
 }
 
 
+bool BattleEvaluator::hasWorkingTowers() const
+{
+	bool keepIntact = cb->getBattle(battleID)->battleGetWallState(EWallPart::KEEP) != EWallState::NONE && cb->getBattle(battleID)->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED;
+	bool upperIntact = cb->getBattle(battleID)->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::NONE && cb->getBattle(battleID)->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED;
+	bool bottomIntact = cb->getBattle(battleID)->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::NONE && cb->getBattle(battleID)->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED;
+	return keepIntact || upperIntact || bottomIntact;
+}
+
 std::optional<PossibleSpellcast> BattleEvaluator::findBestCreatureSpell(const CStack *stack)
 std::optional<PossibleSpellcast> BattleEvaluator::findBestCreatureSpell(const CStack *stack)
 {
 {
 	//TODO: faerie dragon type spell should be selected by server
 	//TODO: faerie dragon type spell should be selected by server
@@ -161,6 +169,14 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 
 
 	auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, *targets, damageCache, hb);
 	auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, *targets, damageCache, hb);
 	float score = EvaluationResult::INEFFECTIVE_SCORE;
 	float score = EvaluationResult::INEFFECTIVE_SCORE;
+	auto enemyMellee = hb->getUnitsIf([this](const battle::Unit* u) -> bool
+		{
+			return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u);
+		});
+	bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
+		&& !stack->canShoot()
+		&& hasWorkingTowers()
+		&& !enemyMellee.empty();
 
 
 	if(targets->possibleAttacks.empty() && bestSpellcast.has_value())
 	if(targets->possibleAttacks.empty() && bestSpellcast.has_value())
 	{
 	{
@@ -174,7 +190,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 		logAi->trace("Evaluating attack for %s", stack->getDescription());
 		logAi->trace("Evaluating attack for %s", stack->getDescription());
 #endif
 #endif
 
 
-		auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb);
+		auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb, siegeDefense);
 		auto & bestAttack = evaluationResult.bestAttack;
 		auto & bestAttack = evaluationResult.bestAttack;
 
 
 		cachedAttack.ap = bestAttack;
 		cachedAttack.ap = bestAttack;
@@ -227,15 +243,10 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 						return BattleAction::makeDefend(stack);
 						return BattleAction::makeDefend(stack);
 					}
 					}
 
 
-					auto enemyMellee = hb->getUnitsIf([this](const battle::Unit * u) -> bool
-						{
-							return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u);
-						});
-
-					bool isTargetOutsideFort = bestAttack.dest.getY() < GameConstants::BFIELD_WIDTH - 4;
+					bool isTargetOutsideFort = !hb->battleIsInsideWalls(bestAttack.from);
 					bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
 					bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
 						&& !bestAttack.attack.shooting
 						&& !bestAttack.attack.shooting
-						&& hb->battleGetFortifications().hasMoat
+						&& hasWorkingTowers()
 						&& !enemyMellee.empty()
 						&& !enemyMellee.empty()
 						&& isTargetOutsideFort;
 						&& isTargetOutsideFort;
 
 
@@ -349,6 +360,22 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
 	auto reachability = cb->getBattle(battleID)->getReachability(stack);
 	auto reachability = cb->getBattle(battleID)->getReachability(stack);
 	auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false);
 	auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false);
 
 
+	auto enemyMellee = hb->getUnitsIf([this](const battle::Unit* u) -> bool
+		{
+			return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u);
+		});
+
+	bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
+		&& hasWorkingTowers()
+		&& !enemyMellee.empty();
+
+	if (siegeDefense)
+	{
+		vstd::erase_if(avHexes, [&](const BattleHex& hex) {
+			return !cb->getBattle(battleID)->battleIsInsideWalls(hex);
+		});
+	}
+
 	if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked
 	if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked
 	{
 	{
 		return BattleAction::makeDefend(stack);
 		return BattleAction::makeDefend(stack);

+ 1 - 0
AI/BattleAI/BattleEvaluator.h

@@ -53,6 +53,7 @@ public:
 	std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack * stack);
 	std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack * stack);
 	BattleAction goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes, const PotentialTargets & targets);
 	BattleAction goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes, const PotentialTargets & targets);
 	std::vector<BattleHex> getBrokenWallMoatHexes() const;
 	std::vector<BattleHex> getBrokenWallMoatHexes() const;
+	bool hasWorkingTowers() const;
 	void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only
 	void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only
 	void print(const std::string & text) const;
 	void print(const std::string & text) const;
 	BattleAction moveOrAttack(const CStack * stack, BattleHex hex, const PotentialTargets & targets);
 	BattleAction moveOrAttack(const CStack * stack, BattleHex hex, const PotentialTargets & targets);

+ 31 - 2
AI/BattleAI/BattleExchangeVariant.cpp

@@ -9,6 +9,7 @@
  */
  */
 #include "StdInc.h"
 #include "StdInc.h"
 #include "BattleExchangeVariant.h"
 #include "BattleExchangeVariant.h"
+#include "BattleEvaluator.h"
 #include "../../lib/CStack.h"
 #include "../../lib/CStack.h"
 
 
 AttackerValue::AttackerValue()
 AttackerValue::AttackerValue()
@@ -213,7 +214,8 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
 	const battle::Unit * activeStack,
 	const battle::Unit * activeStack,
 	PotentialTargets & targets,
 	PotentialTargets & targets,
 	DamageCache & damageCache,
 	DamageCache & damageCache,
-	std::shared_ptr<HypotheticBattle> hb)
+	std::shared_ptr<HypotheticBattle> hb,
+	bool siegeDefense)
 {
 {
 	EvaluationResult result(targets.bestAction());
 	EvaluationResult result(targets.bestAction());
 
 
@@ -231,6 +233,9 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
 
 
 		for(auto & ap : targets.possibleAttacks)
 		for(auto & ap : targets.possibleAttacks)
 		{
 		{
+			if (siegeDefense && !hb->battleIsInsideWalls(ap.from))
+				continue;
+
 			float score = evaluateExchange(ap, 0, targets, damageCache, hbWaited);
 			float score = evaluateExchange(ap, 0, targets, damageCache, hbWaited);
 
 
 			if(score > result.score)
 			if(score > result.score)
@@ -263,6 +268,9 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
 
 
 	for(auto & ap : targets.possibleAttacks)
 	for(auto & ap : targets.possibleAttacks)
 	{
 	{
+		if (siegeDefense && !hb->battleIsInsideWalls(ap.from))
+			continue;
+
 		float score = evaluateExchange(ap, 0, targets, damageCache, hb);
 		float score = evaluateExchange(ap, 0, targets, damageCache, hb);
 		bool sameScoreButWaited = vstd::isAlmostEqual(score, result.score) && result.wait;
 		bool sameScoreButWaited = vstd::isAlmostEqual(score, result.score) && result.wait;
 
 
@@ -350,11 +358,32 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
 		if(distance <= speed)
 		if(distance <= speed)
 			continue;
 			continue;
 
 
+		float penaltyMultiplier = 1.0f; // Default multiplier, no penalty
+		float closestAllyDistance = std::numeric_limits<float>::max();
+
+		for (const battle::Unit* ally : hb->battleAliveUnits()) {
+			if (ally == activeStack) 
+				continue;
+			if (ally->unitSide() != activeStack->unitSide()) 
+				continue;
+
+			float allyDistance = dists.distToNearestNeighbour(ally, enemy);
+			if (allyDistance < closestAllyDistance)
+			{
+				closestAllyDistance = allyDistance;
+			}
+		}
+
+		// If an ally is closer to the enemy, compute the penaltyMultiplier
+		if (closestAllyDistance < distance) {
+			penaltyMultiplier = closestAllyDistance / distance; // Ratio of distances
+		}
+
 		auto turnsToRich = (distance - 1) / speed + 1;
 		auto turnsToRich = (distance - 1) / speed + 1;
 		auto hexes = enemy->getSurroundingHexes();
 		auto hexes = enemy->getSurroundingHexes();
 		auto enemySpeed = enemy->getMovementRange();
 		auto enemySpeed = enemy->getMovementRange();
 		auto speedRatio = speed / static_cast<float>(enemySpeed);
 		auto speedRatio = speed / static_cast<float>(enemySpeed);
-		auto multiplier = speedRatio > 1 ? 1 : speedRatio;
+		auto multiplier = (speedRatio > 1 ? 1 : speedRatio) * penaltyMultiplier;
 
 
 		for(auto & hex : hexes)
 		for(auto & hex : hexes)
 		{
 		{

+ 2 - 1
AI/BattleAI/BattleExchangeVariant.h

@@ -159,7 +159,8 @@ public:
 		const battle::Unit * activeStack,
 		const battle::Unit * activeStack,
 		PotentialTargets & targets,
 		PotentialTargets & targets,
 		DamageCache & damageCache,
 		DamageCache & damageCache,
-		std::shared_ptr<HypotheticBattle> hb);
+		std::shared_ptr<HypotheticBattle> hb,
+		bool siegeDefense = false);
 
 
 	float evaluateExchange(
 	float evaluateExchange(
 		const AttackPossibility & ap,
 		const AttackPossibility & ap,

+ 58 - 3
AI/Nullkiller/Analyzers/ArmyManager.cpp

@@ -309,6 +309,8 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
 		? dynamic_cast<const CGTownInstance *>(dwelling)
 		? dynamic_cast<const CGTownInstance *>(dwelling)
 		: nullptr;
 		: nullptr;
 
 
+	std::set<SlotID> alreadyDisbanded;
+
 	for(int i = dwelling->creatures.size() - 1; i >= 0; i--)
 	for(int i = dwelling->creatures.size() - 1; i >= 0; i--)
 	{
 	{
 		auto ci = infoFromDC(dwelling->creatures[i]);
 		auto ci = infoFromDC(dwelling->creatures[i]);
@@ -322,18 +324,71 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
 
 
 		if(!ci.count) continue;
 		if(!ci.count) continue;
 
 
+		// Calculate the market value of the new stack
+		TResources newStackValue = ci.creID.toCreature()->getFullRecruitCost() * ci.count;
+
 		SlotID dst = hero->getSlotFor(ci.creID);
 		SlotID dst = hero->getSlotFor(ci.creID);
+
+		// Keep track of the least valuable slot in the hero's army
+		SlotID leastValuableSlot;
+		TResources leastValuableStackValue;
+		leastValuableStackValue[6] = std::numeric_limits<int>::max();
+		bool shouldDisband = false;
 		if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack
 		if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack
 		{
 		{
-			if(!freeHeroSlots) //no more place for stacks
-				continue;
+			if(!freeHeroSlots) // No free slots; consider replacing
+			{
+				// Check for the least valuable existing stack
+				for (auto& slot : hero->Slots())
+				{
+					if (alreadyDisbanded.find(slot.first) != alreadyDisbanded.end())
+						continue;
+
+					if(slot.second->getCreatureID() != CreatureID::NONE)
+					{
+						TResources currentStackValue = slot.second->getCreatureID().toCreature()->getFullRecruitCost() * slot.second->getCount();
+
+						if (town && slot.second->getCreatureID().toCreature()->getFactionID() == town->getFactionID())
+							continue;
+
+						if(currentStackValue.marketValue() < leastValuableStackValue.marketValue())
+						{
+							leastValuableStackValue = currentStackValue;
+							leastValuableSlot = slot.first;
+						}
+					}
+				}
+
+				// Decide whether to replace the least valuable stack
+				if(newStackValue.marketValue() <= leastValuableStackValue.marketValue())
+				{
+					continue; // Skip if the new stack isn't worth replacing
+				}
+				else
+				{
+					shouldDisband = true;
+				}
+			}
 			else
 			else
+			{
 				freeHeroSlots--; //new slot will be occupied
 				freeHeroSlots--; //new slot will be occupied
+			}
 		}
 		}
 
 
 		vstd::amin(ci.count, availableRes / ci.creID.toCreature()->getFullRecruitCost()); //max count we can afford
 		vstd::amin(ci.count, availableRes / ci.creID.toCreature()->getFullRecruitCost()); //max count we can afford
 
 
-		if(!ci.count) continue;
+		int disbandMalus = 0;
+		
+		if (shouldDisband)
+		{
+			disbandMalus = leastValuableStackValue / ci.creID.toCreature()->getFullRecruitCost();
+			alreadyDisbanded.insert(leastValuableSlot);
+		}
+
+		ci.count -= disbandMalus;
+
+		if(ci.count <= 0)
+			continue;
 
 
 		ci.level = i; //this is important for Dungeon Summoning Portal
 		ci.level = i; //this is important for Dungeon Summoning Portal
 		creaturesInDwellings.push_back(ci);
 		creaturesInDwellings.push_back(ci);

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

@@ -505,7 +505,7 @@ void ObjectClusterizer::clusterizeObject(
 		else if (priority <= 0)
 		else if (priority <= 0)
 			continue;
 			continue;
 
 
-		bool interestingObject = path.turn() <= 2 || priority > 0.5f;
+		bool interestingObject = path.turn() <= 2 || priority > (ai->settings->isUseFuzzy() ? 0.5f : 0);
 
 
 		if(interestingObject)
 		if(interestingObject)
 		{
 		{

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

@@ -64,7 +64,7 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
 
 
 				if(reinforcement)
 				if(reinforcement)
 				{
 				{
-					tasks.push_back(Goals::sptr(Goals::BuyArmy(town, reinforcement).setpriority(5)));
+					tasks.push_back(Goals::sptr(Goals::BuyArmy(town, reinforcement).setpriority(reinforcement)));
 				}
 				}
 			}
 			}
 		}
 		}

+ 15 - 3
AI/Nullkiller/Behaviors/DefenceBehavior.cpp

@@ -41,9 +41,6 @@ Goals::TGoalVec DefenceBehavior::decompose(const Nullkiller * ai) const
 	for(auto town : ai->cb->getTownsInfo())
 	for(auto town : ai->cb->getTownsInfo())
 	{
 	{
 		evaluateDefence(tasks, town, ai);
 		evaluateDefence(tasks, town, ai);
-		//Let's do only one defence-task per pass since otherwise it can try to hire the same hero twice
-		if (!tasks.empty())
-			break;
 	}
 	}
 
 
 	return tasks;
 	return tasks;
@@ -422,6 +419,21 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM
 			if(hero->getTotalStrength() < threat.danger)
 			if(hero->getTotalStrength() < threat.danger)
 				continue;
 				continue;
 
 
+			bool heroAlreadyHiredInOtherTown = false;
+			for (const auto& task : tasks) 
+			{
+				if (auto recruitGoal = dynamic_cast<Goals::RecruitHero*>(task.get())) 
+				{
+					if (recruitGoal->getHero() == hero)
+					{
+						heroAlreadyHiredInOtherTown = true;
+						break;
+					}
+				}
+			}
+			if (heroAlreadyHiredInOtherTown)
+				continue;
+
 			auto myHeroes = ai->cb->getHeroesInfo();
 			auto myHeroes = ai->cb->getHeroesInfo();
 
 
 #if NKAI_TRACE_LEVEL >= 1
 #if NKAI_TRACE_LEVEL >= 1

+ 1 - 0
AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp

@@ -124,6 +124,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
 	{
 	{
 		if (ai->cb->getHeroesInfo().size() == 0
 		if (ai->cb->getHeroesInfo().size() == 0
 			|| treasureSourcesCount > ai->cb->getHeroesInfo().size() * 5
 			|| treasureSourcesCount > ai->cb->getHeroesInfo().size() * 5
+			|| bestHeroToHire->getArmyCost() > GameConstants::HERO_GOLD_COST / 2.0
 			|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol)
 			|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol)
 			|| (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh()))
 			|| (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh()))
 		{
 		{

+ 6 - 1
AI/Nullkiller/Engine/Nullkiller.cpp

@@ -397,7 +397,12 @@ void Nullkiller::makeTurn()
 				if(!executeTask(bestTask))
 				if(!executeTask(bestTask))
 					return;
 					return;
 
 
-				updateAiState(i, true);
+				bool fastUpdate = true;
+
+				if (bestTask->getHero() != nullptr)
+					fastUpdate = false;
+
+				updateAiState(i, fastUpdate);
 			}
 			}
 			else
 			else
 			{
 			{

+ 39 - 17
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -1006,6 +1006,9 @@ public:
 		Goals::ExecuteHeroChain & chain = dynamic_cast<Goals::ExecuteHeroChain &>(*task);
 		Goals::ExecuteHeroChain & chain = dynamic_cast<Goals::ExecuteHeroChain &>(*task);
 		const AIPath & path = chain.getPath();
 		const AIPath & path = chain.getPath();
 
 
+		if (vstd::isAlmostZero(path.movementCost()))
+			return;
+
 		vstd::amax(evaluationContext.danger, path.getTotalDanger());
 		vstd::amax(evaluationContext.danger, path.getTotalDanger());
 		evaluationContext.movementCost += path.movementCost();
 		evaluationContext.movementCost += path.movementCost();
 		evaluationContext.closestWayRatio = chain.closestWayRatio;
 		evaluationContext.closestWayRatio = chain.closestWayRatio;
@@ -1019,12 +1022,20 @@ public:
 				evaluationContext.involvesSailing = true;
 				evaluationContext.involvesSailing = true;
 		}
 		}
 
 
+		float highestCostForSingleHero = 0;
 		for(auto pair : costsPerHero)
 		for(auto pair : costsPerHero)
 		{
 		{
 			auto role = evaluationContext.evaluator.ai->heroManager->getHeroRole(pair.first);
 			auto role = evaluationContext.evaluator.ai->heroManager->getHeroRole(pair.first);
-
 			evaluationContext.movementCostByRole[role] += pair.second;
 			evaluationContext.movementCostByRole[role] += pair.second;
+			if (pair.second > highestCostForSingleHero)
+				highestCostForSingleHero = pair.second;
+		}
+		if (highestCostForSingleHero > 1 && costsPerHero.size() > 1)
+		{
+			//Chains that involve more than 1 hero doing something for more than a turn are too expensive in my book. They often involved heroes doing nothing just standing there waiting to fulfill their part of the chain.
+			return;
 		}
 		}
+		evaluationContext.movementCost *= costsPerHero.size(); //further deincentivise chaining as it often involves bringing back the army afterwards
 
 
 		auto hero = task->hero;
 		auto hero = task->hero;
 		bool checkGold = evaluationContext.danger == 0;
 		bool checkGold = evaluationContext.danger == 0;
@@ -1046,13 +1057,13 @@ public:
 			evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
 			evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
 			if (target->ID == Obj::HERO)
 			if (target->ID == Obj::HERO)
 				evaluationContext.isHero = true;
 				evaluationContext.isHero = true;
-			if (target->getOwner() != PlayerColor::NEUTRAL && ai->cb->getPlayerRelations(ai->playerID, target->getOwner()) == PlayerRelations::ENEMIES)
+			if (target->getOwner().isValidPlayer() && ai->cb->getPlayerRelations(ai->playerID, target->getOwner()) == PlayerRelations::ENEMIES)
 				evaluationContext.isEnemy = true;
 				evaluationContext.isEnemy = true;
 			evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
 			evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
-			evaluationContext.armyInvolvement += army->getArmyCost();
 			if(evaluationContext.danger > 0)
 			if(evaluationContext.danger > 0)
 				evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength();
 				evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength();
 		}
 		}
+		evaluationContext.armyInvolvement += army->getArmyCost();
 
 
 		vstd::amax(evaluationContext.armyLossPersentage, (float)path.getTotalArmyLoss() / (float)army->getArmyStrength());
 		vstd::amax(evaluationContext.armyLossPersentage, (float)path.getTotalArmyLoss() / (float)army->getArmyStrength());
 		addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
 		addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
@@ -1353,17 +1364,18 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 		const float maxWillingToLose = amIInDanger ? 1 : ai->settings->getMaxArmyLossTarget();
 		const float maxWillingToLose = amIInDanger ? 1 : ai->settings->getMaxArmyLossTarget();
 
 
 		bool arriveNextWeek = false;
 		bool arriveNextWeek = false;
-		if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7)
+		if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7 && priorityTier < PriorityTier::FAR_KILL)
 			arriveNextWeek = true;
 			arriveNextWeek = true;
 
 
 #if NKAI_TRACE_LEVEL >= 2
 #if NKAI_TRACE_LEVEL >= 2
-		logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, explorePriority: %d isDefend: %d",
+		logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, army-involvement: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, explorePriority: %d isDefend: %d isEnemy: %d arriveNextWeek: %d",
 			priorityTier,
 			priorityTier,
 			task->toString(),
 			task->toString(),
 			evaluationContext.armyLossPersentage,
 			evaluationContext.armyLossPersentage,
 			(int)evaluationContext.turn,
 			(int)evaluationContext.turn,
 			evaluationContext.movementCostByRole[HeroRole::MAIN],
 			evaluationContext.movementCostByRole[HeroRole::MAIN],
 			evaluationContext.movementCostByRole[HeroRole::SCOUT],
 			evaluationContext.movementCostByRole[HeroRole::SCOUT],
+			evaluationContext.armyInvolvement,
 			goldRewardPerTurn,
 			goldRewardPerTurn,
 			evaluationContext.goldCost,
 			evaluationContext.goldCost,
 			evaluationContext.armyReward,
 			evaluationContext.armyReward,
@@ -1378,7 +1390,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 			evaluationContext.closestWayRatio,
 			evaluationContext.closestWayRatio,
 			evaluationContext.enemyHeroDangerRatio,
 			evaluationContext.enemyHeroDangerRatio,
 			evaluationContext.explorePriority,
 			evaluationContext.explorePriority,
-			evaluationContext.isDefend);
+			evaluationContext.isDefend,
+			evaluationContext.isEnemy,
+			arriveNextWeek);
 #endif
 #endif
 
 
 		switch (priorityTier)
 		switch (priorityTier)
@@ -1387,13 +1401,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 			{
 			{
 				if (evaluationContext.turn > 0)
 				if (evaluationContext.turn > 0)
 					return 0;
 					return 0;
+				if (evaluationContext.movementCost >= 1)
+					return 0;
 				if(evaluationContext.conquestValue > 0)
 				if(evaluationContext.conquestValue > 0)
-					score = 1000;
+					score = evaluationContext.armyInvolvement;
 				if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
 				if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
 					return 0;
 					return 0;
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 					return 0;
 					return 0;
-				score *= evaluationContext.closestWayRatio;
 				if (evaluationContext.movementCost > 0)
 				if (evaluationContext.movementCost > 0)
 					score /= evaluationContext.movementCost;
 					score /= evaluationContext.movementCost;
 				break;
 				break;
@@ -1404,17 +1419,18 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 					score = evaluationContext.armyInvolvement;
 					score = evaluationContext.armyInvolvement;
 				if (evaluationContext.isEnemy && maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 				if (evaluationContext.isEnemy && maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 					return 0;
 					return 0;
-				score *= evaluationContext.closestWayRatio;
 				break;
 				break;
 			}
 			}
 			case PriorityTier::KILL: //Take towns / kill heroes that are further away
 			case PriorityTier::KILL: //Take towns / kill heroes that are further away
+				//FALL_THROUGH
+			case PriorityTier::FAR_KILL:
 			{
 			{
 				if (evaluationContext.turn > 0 && evaluationContext.isHero)
 				if (evaluationContext.turn > 0 && evaluationContext.isHero)
 					return 0;
 					return 0;
 				if (arriveNextWeek && evaluationContext.isEnemy)
 				if (arriveNextWeek && evaluationContext.isEnemy)
 					return 0;
 					return 0;
 				if (evaluationContext.conquestValue > 0)
 				if (evaluationContext.conquestValue > 0)
-					score = 1000;
+					score = evaluationContext.armyInvolvement;
 				if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
 				if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
 					return 0;
 					return 0;
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
@@ -1432,8 +1448,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 					return 0;
 					return 0;
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 					return 0;
 					return 0;
+				if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0)
+					return 0;
 				score = 1000;
 				score = 1000;
-				score *= evaluationContext.closestWayRatio;
 				if (evaluationContext.movementCost > 0)
 				if (evaluationContext.movementCost > 0)
 					score /= evaluationContext.movementCost;
 					score /= evaluationContext.movementCost;
 				break;
 				break;
@@ -1446,13 +1463,16 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 					return 0;
 					return 0;
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 					return 0;
 					return 0;
+				if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0)
+					return 0;
 				score = 1000;
 				score = 1000;
-				score *= evaluationContext.closestWayRatio;
 				if (evaluationContext.movementCost > 0)
 				if (evaluationContext.movementCost > 0)
 					score /= evaluationContext.movementCost;
 					score /= evaluationContext.movementCost;
 				break;
 				break;
 			}
 			}
 			case PriorityTier::HUNTER_GATHER: //Collect guarded stuff
 			case PriorityTier::HUNTER_GATHER: //Collect guarded stuff
+				//FALL_THROUGH
+			case PriorityTier::FAR_HUNTER_GATHER:
 			{
 			{
 				if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend)
 				if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend)
 					return 0;
 					return 0;
@@ -1468,6 +1488,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 					return 0;
 					return 0;
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 					return 0;
 					return 0;
+				if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0)
+					return 0;
 				score += evaluationContext.strategicalValue * 1000;
 				score += evaluationContext.strategicalValue * 1000;
 				score += evaluationContext.goldReward;
 				score += evaluationContext.goldReward;
 				score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05;
 				score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05;
@@ -1478,7 +1500,6 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 				if (score > 0)
 				if (score > 0)
 				{
 				{
 					score = 1000;
 					score = 1000;
-					score *= evaluationContext.closestWayRatio;
 					if (evaluationContext.movementCost > 0)
 					if (evaluationContext.movementCost > 0)
 						score /= evaluationContext.movementCost;
 						score /= evaluationContext.movementCost;
 				}
 				}
@@ -1492,8 +1513,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 					return 0;
 					return 0;
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 					return 0;
 					return 0;
+				if (evaluationContext.closestWayRatio < 1.0)
+					return 0;
 				score = 1000;
 				score = 1000;
-				score *= evaluationContext.closestWayRatio;
 				if (evaluationContext.movementCost > 0)
 				if (evaluationContext.movementCost > 0)
 					score /= evaluationContext.movementCost;
 					score /= evaluationContext.movementCost;
 				break;
 				break;
@@ -1503,8 +1525,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 				if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange)
 				if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange)
 					return 0;
 					return 0;
 				if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade)
 				if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade)
-					score = 1000;
-				score *= evaluationContext.closestWayRatio;
+					score = evaluationContext.armyInvolvement;
 				score /= (evaluationContext.turn + 1);
 				score /= (evaluationContext.turn + 1);
 				break;
 				break;
 			}
 			}
@@ -1563,13 +1584,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 	}
 	}
 
 
 #if NKAI_TRACE_LEVEL >= 2
 #if NKAI_TRACE_LEVEL >= 2
-	logAi->trace("priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, result %f",
+	logAi->trace("priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, army-involvement: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, result %f",
 		priorityTier,
 		priorityTier,
 		task->toString(),
 		task->toString(),
 		evaluationContext.armyLossPersentage,
 		evaluationContext.armyLossPersentage,
 		(int)evaluationContext.turn,
 		(int)evaluationContext.turn,
 		evaluationContext.movementCostByRole[HeroRole::MAIN],
 		evaluationContext.movementCostByRole[HeroRole::MAIN],
 		evaluationContext.movementCostByRole[HeroRole::SCOUT],
 		evaluationContext.movementCostByRole[HeroRole::SCOUT],
+		evaluationContext.armyInvolvement,
 		goldRewardPerTurn,
 		goldRewardPerTurn,
 		evaluationContext.goldCost,
 		evaluationContext.goldCost,
 		evaluationContext.armyReward,
 		evaluationContext.armyReward,

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

@@ -118,6 +118,8 @@ public:
 		HIGH_PRIO_EXPLORE,
 		HIGH_PRIO_EXPLORE,
 		HUNTER_GATHER,
 		HUNTER_GATHER,
 		LOW_PRIO_EXPLORE,
 		LOW_PRIO_EXPLORE,
+		FAR_KILL,
+		FAR_HUNTER_GATHER,
 		DEFEND
 		DEFEND
 	};
 	};
 
 

+ 30 - 1
AI/Nullkiller/Goals/BuyArmy.cpp

@@ -58,7 +58,36 @@ void BuyArmy::accept(AIGateway * ai)
 
 
 		if(ci.count)
 		if(ci.count)
 		{
 		{
-			cb->recruitCreatures(town, town->getUpperArmy(), ci.creID, ci.count, ci.level);
+			if (town->getUpperArmy()->stacksCount() == GameConstants::ARMY_SIZE)
+			{
+				SlotID lowestValueSlot;
+				int lowestValue = std::numeric_limits<int>::max();
+				for (auto slot : town->getUpperArmy()->Slots())
+				{
+					if (slot.second->getCreatureID() != CreatureID::NONE)
+					{
+						int currentStackMarketValue =
+							slot.second->getCreatureID().toCreature()->getFullRecruitCost().marketValue() * slot.second->getCount();
+
+						if (slot.second->getCreatureID().toCreature()->getFactionID() == town->getFactionID())
+							continue;
+
+						if (currentStackMarketValue < lowestValue)
+						{
+							lowestValue = currentStackMarketValue;
+							lowestValueSlot = slot.first;
+						}
+					}
+				}
+				if (lowestValueSlot.validSlot())
+				{
+					cb->dismissCreature(town->getUpperArmy(), lowestValueSlot);
+				}
+			}
+			if (town->getUpperArmy()->stacksCount() < GameConstants::ARMY_SIZE || town->getUpperArmy()->getSlotFor(ci.creID).validSlot()) //It is possible we don't scrap despite we wanted to due to not scrapping stacks that fit our faction
+			{
+				cb->recruitCreatures(town, town->getUpperArmy(), ci.creID, ci.count, ci.level);
+			}
 			valueBought += ci.count * ci.creID.toCreature()->getAIValue();
 			valueBought += ci.count * ci.creID.toCreature()->getAIValue();
 		}
 		}
 	}
 	}

+ 6 - 4
AI/Nullkiller/Pathfinding/Actors.cpp

@@ -374,10 +374,12 @@ HeroExchangeArmy * HeroExchangeMap::tryUpgrade(
 		for(auto & creatureToBuy : buyArmy)
 		for(auto & creatureToBuy : buyArmy)
 		{
 		{
 			auto targetSlot = target->getSlotFor(creatureToBuy.creID.toCreature());
 			auto targetSlot = target->getSlotFor(creatureToBuy.creID.toCreature());
-
-			target->addToSlot(targetSlot, creatureToBuy.creID, creatureToBuy.count);
-			target->armyCost += creatureToBuy.creID.toCreature()->getFullRecruitCost() * creatureToBuy.count;
-			target->requireBuyArmy = true;
+			if (targetSlot.validSlot())
+			{
+				target->addToSlot(targetSlot, creatureToBuy.creID, creatureToBuy.count);
+				target->armyCost += creatureToBuy.creID.toCreature()->getFullRecruitCost() * creatureToBuy.count;
+				target->requireBuyArmy = true;
+			}
 		}
 		}
 	}
 	}
 
 

+ 45 - 36
CI/NSIS.template.in

@@ -567,56 +567,62 @@ FunctionEnd
 ;Languages
 ;Languages
 
 
   !insertmacro MUI_LANGUAGE "English" ;first language is the default language
   !insertmacro MUI_LANGUAGE "English" ;first language is the default language
-  !insertmacro MUI_LANGUAGE "Albanian"
-  !insertmacro MUI_LANGUAGE "Arabic"
-  !insertmacro MUI_LANGUAGE "Basque"
-  !insertmacro MUI_LANGUAGE "Belarusian"
-  !insertmacro MUI_LANGUAGE "Bosnian"
-  !insertmacro MUI_LANGUAGE "Breton"
-  !insertmacro MUI_LANGUAGE "Bulgarian"
-  !insertmacro MUI_LANGUAGE "Croatian"
   !insertmacro MUI_LANGUAGE "Czech"
   !insertmacro MUI_LANGUAGE "Czech"
-  !insertmacro MUI_LANGUAGE "Danish"
-  !insertmacro MUI_LANGUAGE "Dutch"
-  !insertmacro MUI_LANGUAGE "Estonian"
-  !insertmacro MUI_LANGUAGE "Farsi"
+  !insertmacro MUI_LANGUAGE "SimpChinese"
   !insertmacro MUI_LANGUAGE "Finnish"
   !insertmacro MUI_LANGUAGE "Finnish"
   !insertmacro MUI_LANGUAGE "French"
   !insertmacro MUI_LANGUAGE "French"
   !insertmacro MUI_LANGUAGE "German"
   !insertmacro MUI_LANGUAGE "German"
-  !insertmacro MUI_LANGUAGE "Greek"
-  !insertmacro MUI_LANGUAGE "Hebrew"
   !insertmacro MUI_LANGUAGE "Hungarian"
   !insertmacro MUI_LANGUAGE "Hungarian"
-  !insertmacro MUI_LANGUAGE "Icelandic"
-  !insertmacro MUI_LANGUAGE "Indonesian"
-  !insertmacro MUI_LANGUAGE "Irish"
   !insertmacro MUI_LANGUAGE "Italian"
   !insertmacro MUI_LANGUAGE "Italian"
-  !insertmacro MUI_LANGUAGE "Japanese"
   !insertmacro MUI_LANGUAGE "Korean"
   !insertmacro MUI_LANGUAGE "Korean"
-  !insertmacro MUI_LANGUAGE "Kurdish"
-  !insertmacro MUI_LANGUAGE "Latvian"
-  !insertmacro MUI_LANGUAGE "Lithuanian"
-  !insertmacro MUI_LANGUAGE "Luxembourgish"
-  !insertmacro MUI_LANGUAGE "Macedonian"
-  !insertmacro MUI_LANGUAGE "Malay"
-  !insertmacro MUI_LANGUAGE "Mongolian"
-  !insertmacro MUI_LANGUAGE "Norwegian"
   !insertmacro MUI_LANGUAGE "Polish"
   !insertmacro MUI_LANGUAGE "Polish"
   !insertmacro MUI_LANGUAGE "Portuguese"
   !insertmacro MUI_LANGUAGE "Portuguese"
-  !insertmacro MUI_LANGUAGE "PortugueseBR"
-  !insertmacro MUI_LANGUAGE "Romanian"
   !insertmacro MUI_LANGUAGE "Russian"
   !insertmacro MUI_LANGUAGE "Russian"
-  !insertmacro MUI_LANGUAGE "Serbian"
-  !insertmacro MUI_LANGUAGE "SerbianLatin"
-  !insertmacro MUI_LANGUAGE "SimpChinese"
-  !insertmacro MUI_LANGUAGE "Slovak"
-  !insertmacro MUI_LANGUAGE "Slovenian"
   !insertmacro MUI_LANGUAGE "Spanish"
   !insertmacro MUI_LANGUAGE "Spanish"
   !insertmacro MUI_LANGUAGE "Swedish"
   !insertmacro MUI_LANGUAGE "Swedish"
-  !insertmacro MUI_LANGUAGE "Thai"
-  !insertmacro MUI_LANGUAGE "TradChinese"
   !insertmacro MUI_LANGUAGE "Turkish"
   !insertmacro MUI_LANGUAGE "Turkish"
   !insertmacro MUI_LANGUAGE "Ukrainian"
   !insertmacro MUI_LANGUAGE "Ukrainian"
-  !insertmacro MUI_LANGUAGE "Welsh"
+  !insertmacro MUI_LANGUAGE "Vietnamese"
+  
+  ;!insertmacro MUI_LANGUAGE "Albanian"
+  ;!insertmacro MUI_LANGUAGE "Arabic"
+  ;!insertmacro MUI_LANGUAGE "Basque"
+  ;!insertmacro MUI_LANGUAGE "Belarusian"
+  ;!insertmacro MUI_LANGUAGE "Bosnian"
+  ;!insertmacro MUI_LANGUAGE "Breton"
+  ;!insertmacro MUI_LANGUAGE "Bulgarian"
+  ;!insertmacro MUI_LANGUAGE "Croatian"
+  ;!insertmacro MUI_LANGUAGE "Danish"
+  ;!insertmacro MUI_LANGUAGE "Dutch"
+  ;!insertmacro MUI_LANGUAGE "Estonian"
+  ;!insertmacro MUI_LANGUAGE "Farsi"
+  ;!insertmacro MUI_LANGUAGE "Greek"
+  ;!insertmacro MUI_LANGUAGE "Hebrew"
+  ;!insertmacro MUI_LANGUAGE "Icelandic"
+  ;!insertmacro MUI_LANGUAGE "Indonesian"
+  ;!insertmacro MUI_LANGUAGE "Irish"
+  ;!insertmacro MUI_LANGUAGE "Japanese"
+  ;!insertmacro MUI_LANGUAGE "Kurdish"
+  ;!insertmacro MUI_LANGUAGE "Latvian"
+  ;!insertmacro MUI_LANGUAGE "Lithuanian"
+  ;!insertmacro MUI_LANGUAGE "Luxembourgish"
+  ;!insertmacro MUI_LANGUAGE "Macedonian"
+  ;!insertmacro MUI_LANGUAGE "Malay"
+  ;!insertmacro MUI_LANGUAGE "Mongolian"
+  ;!insertmacro MUI_LANGUAGE "Norwegian"
+  ;!insertmacro MUI_LANGUAGE "PortugueseBR"
+  ;!insertmacro MUI_LANGUAGE "Romanian"
+  ;!insertmacro MUI_LANGUAGE "Serbian"
+  ;!insertmacro MUI_LANGUAGE "SerbianLatin"
+  ;!insertmacro MUI_LANGUAGE "Slovak"
+  ;!insertmacro MUI_LANGUAGE "Slovenian"
+  ;!insertmacro MUI_LANGUAGE "Thai"
+  ;!insertmacro MUI_LANGUAGE "TradChinese"
+  ;!insertmacro MUI_LANGUAGE "Welsh"
+
+
+; Language Selection Dialog
+  !define MUI_LANGDLL_DISPLAY
 
 
 
 
 ;--------------------------------
 ;--------------------------------
@@ -899,6 +905,9 @@ SectionEnd
 ; "Program Files" for AllUsers, "My Documents" for JustMe...
 ; "Program Files" for AllUsers, "My Documents" for JustMe...
 
 
 Function .onInit
 Function .onInit
+
+  !insertmacro MUI_LANGDLL_DISPLAY
+
   StrCmp "@CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL@" "ON" 0 inst
   StrCmp "@CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL@" "ON" 0 inst
 
 
   ReadRegStr $0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" "UninstallString"
   ReadRegStr $0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" "UninstallString"

+ 12 - 1
ChangeLog.md

@@ -1,10 +1,11 @@
 # VCMI Project Changelog
 # VCMI Project Changelog
 
 
-## 1.5.7 -> 1.6.0 (in development)
+## 1.5.7 -> 1.6.0
 
 
 ### Major changes
 ### Major changes
 
 
 * Greatly improved decision-making of NullkillerAI
 * Greatly improved decision-making of NullkillerAI
+* Implemented support for multiple mod presets allowing player to quickly switch between them in Launcher
 * Implemented handicap system, with options to reduce income and growth in addition to starting resources restriction
 * Implemented handicap system, with options to reduce income and growth in addition to starting resources restriction
 * Game will now show statistics after scenario completion, such as resources or army strength over time
 * Game will now show statistics after scenario completion, such as resources or army strength over time
 * Implemented spell quick selection panel in combat
 * Implemented spell quick selection panel in combat
@@ -66,6 +67,7 @@
 * Mutare and Mutare Drake are now Overlord and not Warlock
 * Mutare and Mutare Drake are now Overlord and not Warlock
 * Elixir of Life no longer affects siege machines
 * Elixir of Life no longer affects siege machines
 * Banned skills known by hero now have minimal chance (1) instead of 0 to appear on levelup
 * Banned skills known by hero now have minimal chance (1) instead of 0 to appear on levelup
+* The Transport Artifact victory condition fulfilled by the enemy AI will no longer trigger a victory for human players if "standard victory" is enabled on the map
 
 
 ### Video / Audio
 ### Video / Audio
 
 
@@ -178,6 +180,12 @@
 
 
 ### Launcher
 ### Launcher
 
 
+* Implemented support for multiple mod presets allowing player to quickly switch between them
+* Added new Start Game page to Launcher which is now used when starting the game
+* Added option to create empty mod preset to quickly disable all mods
+* Added button to update all installed mods to Start Game page
+* Added diagnostics to detect common issues with Heroes III data files
+* Added built-in help descriptions for functionalities such as data files import to better explain them to players
 * It is now always possible to disable or uninstall a mod. Any mods that depend on this mod will be automatically disabled
 * It is now always possible to disable or uninstall a mod. Any mods that depend on this mod will be automatically disabled
 * It is now always possible to update a mod, even if there are mods that depend on this mod.
 * It is now always possible to update a mod, even if there are mods that depend on this mod.
 * It is now possible to enable mod that conflicts with already active mod. Conflicting mods will be automatically disabled
 * It is now possible to enable mod that conflicts with already active mod. Conflicting mods will be automatically disabled
@@ -188,6 +196,8 @@
 * Launcher will now correctly show conflicts on both sides - if mod A is marked as conflicting with B, then information on this conflict will be shown in description of both mod A and mod B (instead of only in mod B)
 * Launcher will now correctly show conflicts on both sides - if mod A is marked as conflicting with B, then information on this conflict will be shown in description of both mod A and mod B (instead of only in mod B)
 * Added Swedish translation
 * Added Swedish translation
 * Added better diagnostics for gog installer extraction errors
 * Added better diagnostics for gog installer extraction errors
+* It is no longer possible to start installation or update for a mod that is already being downloaded
+* Fixed detection of existing Heroes III Complete or Shadow of Death data files during import
 
 
 ### Map Editor
 ### Map Editor
 
 
@@ -205,6 +215,7 @@
 * Fixed duplicated list of spells in Mage Guild in copy-pasted towns
 * Fixed duplicated list of spells in Mage Guild in copy-pasted towns
 * Removed separate versioning of map editor. Map editor now has same version as VCMI
 * Removed separate versioning of map editor. Map editor now has same version as VCMI
 * Timed events interfaces now counts days from 1, instead of from 0
 * Timed events interfaces now counts days from 1, instead of from 0
+* Added Recent Files to File Menu and Toolbar
 * Fixed crash on attempting to save map with random dwelling
 * Fixed crash on attempting to save map with random dwelling
 
 
 ### Modding
 ### Modding

+ 41 - 0
Mods/vcmi/Content/config/chinese.json

@@ -121,6 +121,44 @@
 	"vcmi.lobby.deleteFolder" : "你确定要删除下列文件夹?",
 	"vcmi.lobby.deleteFolder" : "你确定要删除下列文件夹?",
 	"vcmi.lobby.deleteMode" : "切换删除模式并返回",
 	"vcmi.lobby.deleteMode" : "切换删除模式并返回",
 
 
+	"vcmi.broadcast.failedLoadGame" : "加载游戏失败",
+	"vcmi.broadcast.command" : "输入'!help'来列举可用命令",
+	"vcmi.broadcast.simturn.end" : "同步回合已结束",
+	"vcmi.broadcast.simturn.endBetween" : "在玩家 %s和%s之间的同步回合已结束",
+	"vcmi.broadcast.serverProblem" : "服务器遇到了一个问题",
+	"vcmi.broadcast.gameTerminated" : "游戏已终止",
+	"vcmi.broadcast.gameSavedAs" : "游戏另存为",
+	"vcmi.broadcast.noCheater" : "没有注册作弊者!",
+	"vcmi.broadcast.playerCheater" : "玩家%s是作弊者!",
+	"vcmi.broadcast.statisticFile" : "统计文件可以在目录%s中找到",
+	"vcmi.broadcast.help.commands" : "主机可用命令:",
+	"vcmi.broadcast.help.exit" : "'!exit' - 立即结束当前游戏",
+	"vcmi.broadcast.help.kick" : "'!kick <玩家>' - 从游戏中踢除特定玩家",
+	"vcmi.broadcast.help.save" : "'!save <文件名>' - 以指定文件名保存游戏",
+	"vcmi.broadcast.help.statistic" : "'!statistic' - 将游戏统计信息保存为csv文件",
+	"vcmi.broadcast.help.commandsAll" : "所有玩家可用命令:",
+	"vcmi.broadcast.help.help" : "'!help' - 显示此帮助",
+	"vcmi.broadcast.help.cheaters" : "'!cheaters' - 列出在游戏中使用作弊命令的玩家",
+	"vcmi.broadcast.help.vote" : "'!vote' - 如果所有玩家投票通过,允许更改一些游戏设置",
+	"vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - 允许进行指定天数的同步回合,发生接触解除",
+	"vcmi.broadcast.vote.force" : "'!vote simturns force X' - 强制进行指定天数的同步回合,阻止玩家接触",
+	"vcmi.broadcast.vote.abort" : "'!vote simturns abort' - 本回合结束后中止同步回合",
+	"vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - 将所有玩家的基础计时器延长指定的秒数",
+	"vcmi.broadcast.vote.noActive" : "没有正在进行的投票!",
+	"vcmi.broadcast.vote.yes" : "是",
+	"vcmi.broadcast.vote.no" : "否",
+	"vcmi.broadcast.vote.notRecognized" : "投票命令无法识别!",
+	"vcmi.broadcast.vote.success.untilContacts" : "投票成功,同步回合将继续进行%s天,发生接触解除",
+	"vcmi.broadcast.vote.success.contactsBlocked" : "投票成功,同步回合将继续进行%s天,阻止玩家接触",
+	"vcmi.broadcast.vote.success.nextDay" : "投票成功,将于第二天结束同步回合",
+	"vcmi.broadcast.vote.success.timer" : "投票成功,所有玩家的计时器已延长 %s 秒。",
+	"vcmi.broadcast.vote.aborted" : "玩家投票反对更改,投票已中止。",
+	"vcmi.broadcast.vote.start.untilContacts" : "开始投票,允许同步回合再进行 %s 天",
+	"vcmi.broadcast.vote.start.contactsBlocked" : "开始投票, 允许同步回合再强制进行 %s 天",
+	"vcmi.broadcast.vote.start.nextDay" : "开始投票,从第二天起结束同布回合",
+	"vcmi.broadcast.vote.start.timer" : "开始投票,将所有玩家的计时器延长 %s 秒。",
+	"vcmi.broadcast.vote.hint" : "输入'!vote yes'来同意这项改动或输入'!vote no'来投票反对它。",
+
 	"vcmi.lobby.login.title" : "VCMI大厅",
 	"vcmi.lobby.login.title" : "VCMI大厅",
 	"vcmi.lobby.login.username" : "用户名:",
 	"vcmi.lobby.login.username" : "用户名:",
 	"vcmi.lobby.login.connecting" : "连接中...",
 	"vcmi.lobby.login.connecting" : "连接中...",
@@ -128,6 +166,7 @@
 	"vcmi.lobby.login.create" : "新账号",
 	"vcmi.lobby.login.create" : "新账号",
 	"vcmi.lobby.login.login" : "登录",
 	"vcmi.lobby.login.login" : "登录",
 	"vcmi.lobby.login.as" : "以 %s 身份登录",
 	"vcmi.lobby.login.as" : "以 %s 身份登录",
+	"vcmi.lobby.login.spectator" : "旁观者",
 	"vcmi.lobby.header.rooms" : "游戏房间 - %d",
 	"vcmi.lobby.header.rooms" : "游戏房间 - %d",
 	"vcmi.lobby.header.channels" : "聊天频道",
 	"vcmi.lobby.header.channels" : "聊天频道",
 	"vcmi.lobby.header.chat.global" : "全局游戏聊天 - %s", // %s -> language name
 	"vcmi.lobby.header.chat.global" : "全局游戏聊天 - %s", // %s -> language name
@@ -189,6 +228,8 @@
 	"vcmi.server.errors.modsToEnable"    : "{需要启用的mod列表}",
 	"vcmi.server.errors.modsToEnable"    : "{需要启用的mod列表}",
 	"vcmi.server.errors.modsToDisable"   : "{需要禁用的mod列表}",
 	"vcmi.server.errors.modsToDisable"   : "{需要禁用的mod列表}",
 	"vcmi.server.errors.unknownEntity" : "加载保存失败! 在保存的游戏中发现未知实体'%s'! 保存可能与当前安装的mod版本不兼容!",
 	"vcmi.server.errors.unknownEntity" : "加载保存失败! 在保存的游戏中发现未知实体'%s'! 保存可能与当前安装的mod版本不兼容!",
+	"vcmi.server.errors.wrongIdentified"   : "你被识别为玩家%s,但预期是玩家%s。",
+	"vcmi.server.errors.notAllowed"   : "你无权执行此操作!",
 
 
 	"vcmi.dimensionDoor.seaToLandError" : "无法在陆地与海洋之间使用异次元之门传送。",
 	"vcmi.dimensionDoor.seaToLandError" : "无法在陆地与海洋之间使用异次元之门传送。",
 
 

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

@@ -53,7 +53,7 @@
 	"vcmi.quickExchange.moveAllUnits" : "Přesunout všechny jednotky",
 	"vcmi.quickExchange.moveAllUnits" : "Přesunout všechny jednotky",
 	"vcmi.quickExchange.swapAllUnits" : "Vyměnit armády",
 	"vcmi.quickExchange.swapAllUnits" : "Vyměnit armády",
 	"vcmi.quickExchange.moveAllArtifacts" : "Přesunout všechny artefakty",
 	"vcmi.quickExchange.moveAllArtifacts" : "Přesunout všechny artefakty",
-	"vcmi.quickExchange.swapAllArtifacts" : "Vyměnit artefakt",
+	"vcmi.quickExchange.swapAllArtifacts" : "Vyměnit artefakty",
 
 
 	"vcmi.radialWheel.mergeSameUnit" : "Sloučit stejné jednotky",
 	"vcmi.radialWheel.mergeSameUnit" : "Sloučit stejné jednotky",
 	"vcmi.radialWheel.fillSingleUnit" : "Vyplnit jednou jednotkou",
 	"vcmi.radialWheel.fillSingleUnit" : "Vyplnit jednou jednotkou",

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

@@ -53,7 +53,7 @@
 	"vcmi.quickExchange.moveAllUnits" : "Move All Units",
 	"vcmi.quickExchange.moveAllUnits" : "Move All Units",
 	"vcmi.quickExchange.swapAllUnits" : "Swap Armies",
 	"vcmi.quickExchange.swapAllUnits" : "Swap Armies",
 	"vcmi.quickExchange.moveAllArtifacts" : "Move All Artifacts",
 	"vcmi.quickExchange.moveAllArtifacts" : "Move All Artifacts",
-	"vcmi.quickExchange.swapAllArtifacts" : "Swap Artifact",
+	"vcmi.quickExchange.swapAllArtifacts" : "Swap Artifacts",
 	
 	
 	"vcmi.radialWheel.mergeSameUnit" : "Merge same creatures",
 	"vcmi.radialWheel.mergeSameUnit" : "Merge same creatures",
 	"vcmi.radialWheel.fillSingleUnit" : "Fill with single creatures",
 	"vcmi.radialWheel.fillSingleUnit" : "Fill with single creatures",

+ 39 - 1
Mods/vcmi/Content/config/german.json

@@ -121,7 +121,43 @@
 	"vcmi.lobby.deleteFolder" : "Möchtet Ihr folgenden Ordner löschen?",
 	"vcmi.lobby.deleteFolder" : "Möchtet Ihr folgenden Ordner löschen?",
 	"vcmi.lobby.deleteMode" : "In den Löschmodus wechseln und zurück",
 	"vcmi.lobby.deleteMode" : "In den Löschmodus wechseln und zurück",
 
 
+	"vcmi.broadcast.failedLoadGame" : "Spiel konnte nicht geladen werden",
 	"vcmi.broadcast.command" : "Benutze '!help' um alle verfügbaren Befehle aufzulisten",
 	"vcmi.broadcast.command" : "Benutze '!help' um alle verfügbaren Befehle aufzulisten",
+	"vcmi.broadcast.simturn.end" : "Simultane Züge wurden beendet",
+	"vcmi.broadcast.simturn.endBetween" : "Simultane Züge zwischen den Spielern %s und %s wurden beendet",
+	"vcmi.broadcast.serverProblem" : "Server hat ein Problem festgestellt",
+	"vcmi.broadcast.gameTerminated" : "Spiel wurde abgebrochen",
+	"vcmi.broadcast.gameSavedAs" : "Spiel gespeichert als",
+	"vcmi.broadcast.noCheater" : "Keine Betrüger registriert!",
+	"vcmi.broadcast.playerCheater" : "Spieler %s ist ein Betrüger!",
+	"vcmi.broadcast.statisticFile" : "Die Statistikdateien befinden sich im Verzeichnis %s",
+	"vcmi.broadcast.help.commands" : "Verfügbare Befehle für den Host:",
+	"vcmi.broadcast.help.exit" : "'!exit' - beendet sofort das aktuelle Spiel",
+	"vcmi.broadcast.help.kick" : "'!kick <player>' - den angegebenen Spieler aus dem Spiel werfen",
+	"vcmi.broadcast.help.save" : "'!save <filename>' - Spiel unter dem angegebenen Dateinamen speichern",
+	"vcmi.broadcast.help.statistic" : "'!statistic' - Spielstatistiken als csv-Datei speichern",
+	"vcmi.broadcast.help.commandsAll" : "Verfügbare Befehle für alle Spieler:",
+	"vcmi.broadcast.help.help" : "'!help' - diese Hilfe anzeigen",
+	"vcmi.broadcast.help.cheaters" : "'!cheaters' - Liste der Spieler, die während des Spiels einen Cheat-Befehl eingegeben haben",
+	"vcmi.broadcast.help.vote" : "'!vote' - erlaubt es, einige Spieleinstellungen zu ändern, wenn alle Spieler dafür stimmen",
+	"vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - erlaubt simultane Züge für eine bestimmte Anzahl von Tagen oder bis zum Kontakt",
+	"vcmi.broadcast.vote.force" : "'!vote simturns force X' - erzwingt simultane Züge für die angegebene Anzahl von Tagen und blockiert Spielerkontakte",
+	"vcmi.broadcast.vote.abort" : "'!vote simturns abort' - Simultane Züge abbrechen, sobald dieser Zug endet",
+	"vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - verlängert den Basis-Timer für alle Spieler um die angegebene Anzahl von Sekunden",
+	"vcmi.broadcast.vote.noActive" : "Keine aktive Abstimmung!",
+	"vcmi.broadcast.vote.yes" : "ja",
+	"vcmi.broadcast.vote.no" : "nein",
+	"vcmi.broadcast.vote.notRecognized" : "Abstimmungsbefehl nicht erkannt!",
+	"vcmi.broadcast.vote.success.untilContacts" : "Abstimmung erfolgreich. Simultane Züge laufen für %s weitere Tage, oder bis zum Kontakt",
+	"vcmi.broadcast.vote.success.contactsBlocked" : "Abstimmung erfolgreich. Simultane Züge werden für %s weitere Tage laufen. Kontakte sind blockiert",
+	"vcmi.broadcast.vote.success.nextDay" : "Abstimmung erfolgreich. Simultane Züge werden am nächsten Tag beendet",
+	"vcmi.broadcast.vote.success.timer" : "Abstimmung erfolgreich. Der Timer für alle Spieler wurde um %s Sekunden verlängert.",
+	"vcmi.broadcast.vote.aborted" : "Spieler haben gegen die Änderung gestimmt. Abstimmung abgebrochen",
+	"vcmi.broadcast.vote.start.untilContacts" : "Abstimmung gestartet, um simultane Züge für weitere %s Tage zu erlauben",
+	"vcmi.broadcast.vote.start.contactsBlocked" : "Abstimmung über die Erzwingung simultaner Züge für weitere %s-Tage eingeleitet",
+	"vcmi.broadcast.vote.start.nextDay" : "Beginn der Abstimmung zur Beendigung der simultanen Züge ab dem nächsten Tag",
+	"vcmi.broadcast.vote.start.timer" : "Abstimmung gestartet, um den Timer für alle Spieler um %s Sekunden zu verlängern",
+	"vcmi.broadcast.vote.hint" : "Gib '!vote yes' ein, um dieser Änderung zuzustimmen oder '!vote no', um dagegen zu stimmen",
 		
 		
 	"vcmi.lobby.login.title" : "VCMI Online Lobby",
 	"vcmi.lobby.login.title" : "VCMI Online Lobby",
 	"vcmi.lobby.login.username" : "Benutzername:",
 	"vcmi.lobby.login.username" : "Benutzername:",
@@ -130,6 +166,7 @@
 	"vcmi.lobby.login.create" : "Neuer Account",
 	"vcmi.lobby.login.create" : "Neuer Account",
 	"vcmi.lobby.login.login" : "Login",
 	"vcmi.lobby.login.login" : "Login",
 	"vcmi.lobby.login.as" : "Login als %s",
 	"vcmi.lobby.login.as" : "Login als %s",
+	"vcmi.lobby.login.spectator" : "Beobachter",
 	"vcmi.lobby.header.rooms" : "Spielräume - %d",
 	"vcmi.lobby.header.rooms" : "Spielräume - %d",
 	"vcmi.lobby.header.channels" : "Chat Kanäle",
 	"vcmi.lobby.header.channels" : "Chat Kanäle",
 	"vcmi.lobby.header.chat.global" : "Globaler Spiele-Chat - %s", // %s -> language name
 	"vcmi.lobby.header.chat.global" : "Globaler Spiele-Chat - %s", // %s -> language name
@@ -190,8 +227,9 @@
 	"vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst",
 	"vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst",
 	"vcmi.server.errors.modsToEnable"    : "{Erforderliche Mods um das Spiel zu laden}",
 	"vcmi.server.errors.modsToEnable"    : "{Erforderliche Mods um das Spiel zu laden}",
 	"vcmi.server.errors.modsToDisable"   : "{Folgende Mods müssen deaktiviert werden}",
 	"vcmi.server.errors.modsToDisable"   : "{Folgende Mods müssen deaktiviert werden}",
-	"vcmi.server.errors.modDependencyLoop" : "Mod {'%s'} konnte nicht geladen werden.!\n Möglicherweise befindet sie sich in einer (weichen) Abhängigkeitsschleife.",
 	"vcmi.server.errors.unknownEntity" : "Spielstand konnte nicht geladen werden! Unbekannte Entität '%s' im gespeicherten Spiel gefunden! Der Spielstand ist möglicherweise nicht mit der aktuell installierten Version der Mods kompatibel!",
 	"vcmi.server.errors.unknownEntity" : "Spielstand konnte nicht geladen werden! Unbekannte Entität '%s' im gespeicherten Spiel gefunden! Der Spielstand ist möglicherweise nicht mit der aktuell installierten Version der Mods kompatibel!",
+	"vcmi.server.errors.wrongIdentified"   : "Ihr wurdet als Spieler %s identifiziert, während %s erwartet wurde",
+	"vcmi.server.errors.notAllowed"   : "Ihr dürft diese Aktion nicht durchführen!",
 	
 	
 	"vcmi.dimensionDoor.seaToLandError" : "Es ist nicht möglich, mit einer Dimensionstür vom Meer zum Land oder umgekehrt zu teleportieren.",
 	"vcmi.dimensionDoor.seaToLandError" : "Es ist nicht möglich, mit einer Dimensionstür vom Meer zum Land oder umgekehrt zu teleportieren.",
 
 

+ 76 - 8
Mods/vcmi/Content/config/polish.json

@@ -12,9 +12,9 @@
 	"vcmi.adventureMap.monsterThreat.levels.9"  : "Przytłaczający",
 	"vcmi.adventureMap.monsterThreat.levels.9"  : "Przytłaczający",
 	"vcmi.adventureMap.monsterThreat.levels.10" : "Śmiertelny",
 	"vcmi.adventureMap.monsterThreat.levels.10" : "Śmiertelny",
 	"vcmi.adventureMap.monsterThreat.levels.11" : "Nie do pokonania",
 	"vcmi.adventureMap.monsterThreat.levels.11" : "Nie do pokonania",
-	"vcmi.adventureMap.monsterLevel"            : "\n\nJednostka %ATTACK_TYPE %LEVEL poziomu z miasta %TOWN",
-	"vcmi.adventureMap.monsterMeleeType"        : "Walcząca wręcz",
-	"vcmi.adventureMap.monsterRangedType"       : "Dystansowa",
+	"vcmi.adventureMap.monsterLevel"            : "\n\nJednostka %ATTACK_TYPE %LEVEL-go poziomu z miasta %TOWN",
+	"vcmi.adventureMap.monsterMeleeType"        : "walcząca wręcz",
+	"vcmi.adventureMap.monsterRangedType"       : "dystansowa",
 	"vcmi.adventureMap.search.hover"            : "Wyszukiwarka obiektów",
 	"vcmi.adventureMap.search.hover"            : "Wyszukiwarka obiektów",
 	"vcmi.adventureMap.search.help"             : "Wybierz obiekt który chcesz znaleźć na mapie.",
 	"vcmi.adventureMap.search.help"             : "Wybierz obiekt który chcesz znaleźć na mapie.",
 
 
@@ -34,6 +34,44 @@
 	"vcmi.bonusSource.hero" : "Bohater",
 	"vcmi.bonusSource.hero" : "Bohater",
 	"vcmi.bonusSource.commander" : "Dowódca",
 	"vcmi.bonusSource.commander" : "Dowódca",
 	"vcmi.bonusSource.other" : "Inne",
 	"vcmi.bonusSource.other" : "Inne",
+
+	"vcmi.broadcast.command" : "Wpisz '!help' aby zobaczyć listę dostępnych komend.",
+	"vcmi.broadcast.failedLoadGame" : "Nie udało się wczytać gry",
+	"vcmi.broadcast.gameSavedAs" : "gra zapisana jako",
+	"vcmi.broadcast.gameTerminated" : "gra została zamknięta",
+	"vcmi.broadcast.help.cheaters" : "'!cheaters' - wyświetla listę graczy, którzy użyli 'kodów' w trakcie gry",
+	"vcmi.broadcast.help.commands" : "Dostępne komendy dla hosta:",
+	"vcmi.broadcast.help.commandsAll" : "Dostępne komendy dla wszystkich graczy:",
+	"vcmi.broadcast.help.exit" : "'!exit' - natychmiast kończy bieżącą grę",
+	"vcmi.broadcast.help.help" : "'!help' - wyświetla tę pomoc",
+	"vcmi.broadcast.help.kick" : "'!kick <player>' - wyrzuca określonego gracza z gry",
+	"vcmi.broadcast.help.save" : "'!save <filename>' - zapisz grę pod określoną nazwą",
+	"vcmi.broadcast.help.statistic" : "'!statistic' - zapisz statystyki gry do pliku csv",
+	"vcmi.broadcast.help.vote" : "'!vote' - pozwala na zmianę ustawień gry jeśli wszyscy gracze zagłosują 'za'",
+	"vcmi.broadcast.noCheater" : "Nie zarejestrowano oszustw!",
+	"vcmi.broadcast.playerCheater" : "Gracz %s to oszust!",
+	"vcmi.broadcast.serverProblem" : "Serwer napotkał problem",
+	"vcmi.broadcast.simturn.end" : "Tury symultaniczne zostały zakończone",
+	"vcmi.broadcast.simturn.endBetween" : "Tury symultaniczne pomiędzy graczami %s i %s dobiegły końca",
+	"vcmi.broadcast.statisticFile" : "Pliki statystyk są dostępne w folderze %s",
+	"vcmi.broadcast.vote.abort" : "'!vote simturns abort' - przerwanie tur symultanicznych po zakończeniu tej tury",
+	"vcmi.broadcast.vote.aborted" : "Gracz zagłosował przeciwko. Głosowanie anulowane.",
+	"vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - tury symultaniczne przez określoną ilość dni, albo do pierwszego kontaktu",
+	"vcmi.broadcast.vote.force" : "'!vote simturns force X' - tury symultaniczne przez określoną ilość dni, niezależnie od kontaktu",
+	"vcmi.broadcast.vote.hint" : "Wpisz '!vote yes' żeby zagłosować na tak lub '!vote no' - przeciwko tej zmianie",
+	"vcmi.broadcast.vote.no" : "nie",
+	"vcmi.broadcast.vote.noActive" : "Żadne głosowanie nie jest aktywne!",
+	"vcmi.broadcast.vote.notRecognized" : "Nieznana komenda do głosowania!",
+	"vcmi.broadcast.vote.start.contactsBlocked" : "Rozpoczęto głosowanie: tury symultaniczne przez %s kolejnych dni",
+	"vcmi.broadcast.vote.start.nextDay" : "Rozpoczęto głosowanie: zakończenie tur symultanicznych od następnego dnia",
+	"vcmi.broadcast.vote.start.timer" : "Rozpoczęto głosowanie: wydłużenie czasu dla wszystkich graczy o %s sekund",
+	"vcmi.broadcast.vote.start.untilContacts" : "Rozpoczęto głosowanie: przedłużenie trwania tur symultanicznych o kolejne %s dni",
+	"vcmi.broadcast.vote.success.contactsBlocked" : "Głosowanie zakończone pomyślnie. Tury symultaniczne będą trwały jeszcze przez kolejne %s dni, niezależnie od kontaktu.",
+	"vcmi.broadcast.vote.success.nextDay" : "Głosowanie zakończone pomyślnie. Tury symultaniczne zostaną wyłączone w kolejnym dniu",
+	"vcmi.broadcast.vote.success.timer" : "Głosowanie zakończone pomyślnie. Czas dla wszystkich graczy został wydłużony o %s sekund",
+	"vcmi.broadcast.vote.success.untilContacts" : "Głosowanie zakończone pomyślnie. Tury symultaniczne będą trwały jeszcze przez kolejne %s dni lub do pierwszego kontaktu.",
+	"vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - wydłużenie bazowego czasu dla wszystkich graczy o X sekund",
+	"vcmi.broadcast.vote.yes" : "tak",
 	
 	
 	"vcmi.capitalColors.0" : "Czerwony",
 	"vcmi.capitalColors.0" : "Czerwony",
 	"vcmi.capitalColors.1" : "Niebieski",
 	"vcmi.capitalColors.1" : "Niebieski",
@@ -174,6 +212,12 @@
 	"vcmi.lobby.pvp.randomTownVs.hover" : "Wylosuj 2 miasta",
 	"vcmi.lobby.pvp.randomTownVs.hover" : "Wylosuj 2 miasta",
 	"vcmi.lobby.pvp.randomTownVs.help" : "Wyświetli nazwę 2 wylosowanych miast na czacie, które nie zostały zablokowane na liście",
 	"vcmi.lobby.pvp.randomTownVs.help" : "Wyświetli nazwę 2 wylosowanych miast na czacie, które nie zostały zablokowane na liście",
 	"vcmi.lobby.pvp.versus" : "vs.",
 	"vcmi.lobby.pvp.versus" : "vs.",
+	"vcmi.lobby.deleteFile" : "Czy chcesz usunąć ten plik ?",
+	"vcmi.lobby.deleteFolder" : "Czy chcesz usunąć ten folder ?",
+	"vcmi.lobby.deleteMapTitle" : "Wskaż tytuł, który chcesz usunąć",
+	"vcmi.lobby.deleteMode" : "Przełącza tryb na usuwanie i spowrotem",
+	"vcmi.lobby.deleteSaveGameTitle" : "Wskaż zapis gry do usunięcia",
+	"vcmi.lobby.deleteUnsupportedSave" : "{Znaleziono niekompatybilne zapisy gry}\n\nVCMI wykrył %d zapisów gry, które nie są już wspierane. Prawdopodobnie ze względu na różne wersje gry.\n\nCzy chcesz je usunąć ?",
 
 
 	"vcmi.client.errors.invalidMap" : "{Błędna mapa lub kampania}\n\nNie udało się stworzyć gry! Wybrana mapa lub kampania jest niepoprawna lub uszkodzona. Powód:\n%s",
 	"vcmi.client.errors.invalidMap" : "{Błędna mapa lub kampania}\n\nNie udało się stworzyć gry! Wybrana mapa lub kampania jest niepoprawna lub uszkodzona. Powód:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{Brakujące pliki gry}\n\nPliki kampanii nie zostały znalezione! Możliwe że używasz niekompletnych lub uszkodzonych plików Heroes 3. Spróbuj ponownej instalacji plików gry.",
 	"vcmi.client.errors.missingCampaigns" : "{Brakujące pliki gry}\n\nPliki kampanii nie zostały znalezione! Możliwe że używasz niekompletnych lub uszkodzonych plików Heroes 3. Spróbuj ponownej instalacji plików gry.",
@@ -183,8 +227,10 @@
 	"vcmi.server.errors.modsToEnable"    : "{Następujące mody są wymagane do wczytania gry}",
 	"vcmi.server.errors.modsToEnable"    : "{Następujące mody są wymagane do wczytania gry}",
 	"vcmi.server.errors.modsToDisable"   : "{Następujące mody muszą zostać wyłączone}",
 	"vcmi.server.errors.modsToDisable"   : "{Następujące mody muszą zostać wyłączone}",
 	"vcmi.server.errors.modDependencyLoop" : "Nie udało się wczytać moda {'%s'}!\n Być może znajduje się w pętli zależności",
 	"vcmi.server.errors.modDependencyLoop" : "Nie udało się wczytać moda {'%s'}!\n Być może znajduje się w pętli zależności",
+	"vcmi.server.errors.notAllowed" : "To działanie nie jest dozwolone!",
 	"vcmi.server.errors.unknownEntity" : "Nie udało się wczytać zapisu! Nieznany element '%s' znaleziony w pliku zapisu! Zapis może nie być zgodny z aktualnie zainstalowaną wersją modów!",
 	"vcmi.server.errors.unknownEntity" : "Nie udało się wczytać zapisu! Nieznany element '%s' znaleziony w pliku zapisu! Zapis może nie być zgodny z aktualnie zainstalowaną wersją modów!",
-
+	"vcmi.server.errors.wrongIdentified" : "Zostałeś zidentyfikowany jako gracz %s natomiast powinieneś być %s",
+	
 	"vcmi.dimensionDoor.seaToLandError" : "Nie jest możliwa teleportacja przez drzwi wymiarów z wód na ląd i na odwrót.",
 	"vcmi.dimensionDoor.seaToLandError" : "Nie jest możliwa teleportacja przez drzwi wymiarów z wód na ląd i na odwrót.",
 
 
 	"vcmi.settingsMainWindow.generalTab.hover"   : "Ogólne",
 	"vcmi.settingsMainWindow.generalTab.hover"   : "Ogólne",
@@ -228,7 +274,7 @@
 	"vcmi.statisticWindow.param.obeliskVisited" : "Lb. obelisków",
 	"vcmi.statisticWindow.param.obeliskVisited" : "Lb. obelisków",
 	"vcmi.statisticWindow.icon.townCaptured" : "Miasto zdobyte",
 	"vcmi.statisticWindow.icon.townCaptured" : "Miasto zdobyte",
 	"vcmi.statisticWindow.icon.strongestHeroDefeated" : "Najsilniejszy bohater przeciwnika pokonany",
 	"vcmi.statisticWindow.icon.strongestHeroDefeated" : "Najsilniejszy bohater przeciwnika pokonany",
-	"vcmi.statisticWindow.icon.grailFound" : "Gral znaleziony",
+	"vcmi.statisticWindow.icon.grailFound" : "Graal znaleziony",
 	"vcmi.statisticWindow.icon.defeated" : "Pokonany",
 	"vcmi.statisticWindow.icon.defeated" : "Pokonany",
 
 
 	"vcmi.systemOptions.fullscreenBorderless.hover" : "Pełny ekran (bez ramek)",
 	"vcmi.systemOptions.fullscreenBorderless.hover" : "Pełny ekran (bez ramek)",
@@ -491,11 +537,33 @@
 	"vcmi.stackExperience.rank.4" : "Udowodniony",
 	"vcmi.stackExperience.rank.4" : "Udowodniony",
 	"vcmi.stackExperience.rank.5" : "Weteran",
 	"vcmi.stackExperience.rank.5" : "Weteran",
 	"vcmi.stackExperience.rank.6" : "Adept",
 	"vcmi.stackExperience.rank.6" : "Adept",
-	"vcmi.stackExperience.rank.7" : "Expert",
+	"vcmi.stackExperience.rank.7" : "Ekspert",
 	"vcmi.stackExperience.rank.8" : "Elitarny",
 	"vcmi.stackExperience.rank.8" : "Elitarny",
-	"vcmi.stackExperience.rank.9" : "Master",
+	"vcmi.stackExperience.rank.9" : "Mistrz",
 	"vcmi.stackExperience.rank.10" : "As",
 	"vcmi.stackExperience.rank.10" : "As",
 
 
+	"spell.core.castleMoat.name" : "Fosa",
+	"spell.core.castleMoatTrigger.name" : "Fosa",
+	"spell.core.catapultShot.name" : "Strzał z katapulty",
+	"spell.core.cyclopsShot.name" : "Strzał oblężniczy",
+	"spell.core.dungeonMoat.name" : "Wrzący olej",
+	"spell.core.dungeonMoatTrigger.name" : "Wrzący olej",
+	"spell.core.fireWallTrigger.name" : "Ściana Ognia",
+	"spell.core.firstAid.name" : "Pierwsza pomoc",
+	"spell.core.fortressMoat.name" : "Wrząca smoła",
+	"spell.core.fortressMoatTrigger.name" : "Wrząca smoła",
+	"spell.core.infernoMoat.name" : "Lawa",
+	"spell.core.infernoMoatTrigger.name" : "Lawa",
+	"spell.core.landMineTrigger.name" : "Mina",
+	"spell.core.necropolisMoat.name" : "Fosa z kości",
+	"spell.core.necropolisMoatTrigger.name" : "Fosa z kości",
+	"spell.core.rampartMoat.name" : "Ciernisko",
+	"spell.core.rampartMoatTrigger.name" : "Ciernisko",
+	"spell.core.strongholdMoat.name" : "Palisada obronna",
+	"spell.core.strongholdMoatTrigger.name" : "Palisada obronna",
+	"spell.core.summonDemons.name" : "Przyzwanie Demonów",
+	"spell.core.towerMoat.name" : "Pole minowe",
+
 	// Strings for HotA Seer Hut / Quest Guards
 	// Strings for HotA Seer Hut / Quest Guards
 	"core.seerhut.quest.heroClass.complete.0" : "Ah, ty jesteś %s.  Oto prezent dla ciebie.  Czy go przyjmiesz?",
 	"core.seerhut.quest.heroClass.complete.0" : "Ah, ty jesteś %s.  Oto prezent dla ciebie.  Czy go przyjmiesz?",
 	"core.seerhut.quest.heroClass.complete.1" : "Ah, ty jesteś %s.  Oto prezent dla ciebie.  Czy go przyjmiesz?",
 	"core.seerhut.quest.heroClass.complete.1" : "Ah, ty jesteś %s.  Oto prezent dla ciebie.  Czy go przyjmiesz?",
@@ -657,7 +725,7 @@
 	"core.bonus.NO_MORALE.description": "Odporność na efekty morale",
 	"core.bonus.NO_MORALE.description": "Odporność na efekty morale",
 	"core.bonus.NO_WALL_PENALTY.name": "Bez przeszkód",
 	"core.bonus.NO_WALL_PENALTY.name": "Bez przeszkód",
 	"core.bonus.NO_WALL_PENALTY.description": "Pełne obrażenia podczas oblężenia",
 	"core.bonus.NO_WALL_PENALTY.description": "Pełne obrażenia podczas oblężenia",
-	"core.bonus.NON_LIVING.name": "Nie żyjący",
+	"core.bonus.NON_LIVING.name": "Nieżyjący",
 	"core.bonus.NON_LIVING.description": "Niewrażliwość na wiele efektów",
 	"core.bonus.NON_LIVING.description": "Niewrażliwość na wiele efektów",
 	"core.bonus.RANDOM_SPELLCASTER.name": "Losowy czarodziej",
 	"core.bonus.RANDOM_SPELLCASTER.name": "Losowy czarodziej",
 	"core.bonus.RANDOM_SPELLCASTER.description": "Może rzucić losowy czar",
 	"core.bonus.RANDOM_SPELLCASTER.description": "Może rzucić losowy czar",

+ 30 - 30
Mods/vcmi/Content/config/portuguese.json

@@ -55,10 +55,10 @@
 	"vcmi.quickExchange.moveAllArtifacts" : "Mover Todos os Artefatos",
 	"vcmi.quickExchange.moveAllArtifacts" : "Mover Todos os Artefatos",
 	"vcmi.quickExchange.swapAllArtifacts" : "Trocar Artefato",
 	"vcmi.quickExchange.swapAllArtifacts" : "Trocar Artefato",
 	
 	
-	"vcmi.radialWheel.mergeSameUnit" : "Mesclar criaturas iguais",
-	"vcmi.radialWheel.fillSingleUnit" : "Preencher com criaturas únicas",
-	"vcmi.radialWheel.splitSingleUnit" : "Dividir uma criatura única",
-	"vcmi.radialWheel.splitUnitEqually" : "Dividir criaturas igualmente",
+	"vcmi.radialWheel.mergeSameUnit" : "Mesclar criaturas do mesmo tipo",
+	"vcmi.radialWheel.fillSingleUnit" : "Preencher com criaturas individuais",
+	"vcmi.radialWheel.splitSingleUnit" : "Separar uma criatura",
+	"vcmi.radialWheel.splitUnitEqually" : "Distribuir criaturas igualmente",
 	"vcmi.radialWheel.moveUnit" : "Mover criaturas para outro exército",
 	"vcmi.radialWheel.moveUnit" : "Mover criaturas para outro exército",
 	"vcmi.radialWheel.splitUnit" : "Dividir criatura para outro espaço",
 	"vcmi.radialWheel.splitUnit" : "Dividir criatura para outro espaço",
 	
 	
@@ -74,8 +74,8 @@
 	"vcmi.radialWheel.moveDown" : "Mover para baixo",
 	"vcmi.radialWheel.moveDown" : "Mover para baixo",
 	"vcmi.radialWheel.moveBottom" : "Mover para o fundo",
 	"vcmi.radialWheel.moveBottom" : "Mover para o fundo",
 	
 	
-	"vcmi.randomMap.description" : "Mapa criado pelo Gerador de Mapas Aleatórios.\nO modelo foi %s, tamanho %dx%d, níveis %d, jogadores %d, computadores %d, água %s, monstros %s, mapa VCMI",
-	"vcmi.randomMap.description.isHuman" : ", %s é humano",
+	"vcmi.randomMap.description" : "Mapa criado pelo Gerador de Mapas Aleatórios.\nO modelo foi %s, tamanho %dx%d, níveis %d, jogadores %d, computadores %d, água %s, monstro %s, mapa VCMI",
+	"vcmi.randomMap.description.isHuman" : ", %s é um jogador humano",
 	"vcmi.randomMap.description.townChoice" : ", a escolha de cidade de %s é %s",
 	"vcmi.randomMap.description.townChoice" : ", a escolha de cidade de %s é %s",
 	"vcmi.randomMap.description.water.none" : "nenhuma",
 	"vcmi.randomMap.description.water.none" : "nenhuma",
 	"vcmi.randomMap.description.water.normal" : "normal",
 	"vcmi.randomMap.description.water.normal" : "normal",
@@ -107,11 +107,11 @@
 	"vcmi.lobby.mapPreview" : "Prévia do mapa",
 	"vcmi.lobby.mapPreview" : "Prévia do mapa",
 	"vcmi.lobby.noPreview" : "sem prévia",
 	"vcmi.lobby.noPreview" : "sem prévia",
 	"vcmi.lobby.noUnderground" : "sem subterrâneo",
 	"vcmi.lobby.noUnderground" : "sem subterrâneo",
-	"vcmi.lobby.sortDate" : "Ordenar mapas por data de alteração",
+	"vcmi.lobby.sortDate" : "Ordenar mapas por data de modificação",
 	"vcmi.lobby.backToLobby" : "Voltar para a sala de espera",
 	"vcmi.lobby.backToLobby" : "Voltar para a sala de espera",
 	"vcmi.lobby.author" : "Autor",
 	"vcmi.lobby.author" : "Autor",
 	"vcmi.lobby.handicap" : "Desvant.",
 	"vcmi.lobby.handicap" : "Desvant.",
-	"vcmi.lobby.handicap.resource" : "Fornece aos jogadores recursos apropriados para começar, além dos recursos iniciais normais. Valores negativos são permitidos, mas são limitados a 0 no total (o jogador nunca começa com recursos negativos).",
+	"vcmi.lobby.handicap.resource" : "Fornece aos jogadores recursos adequados para começar, além dos recursos iniciais normais. Valores negativos são permitidos, mas são limitados a 0 no total (o jogador nunca começa com recursos negativos).",
 	"vcmi.lobby.handicap.income" : "Altera as várias rendas do jogador em porcentagem. Arredondado para cima.",
 	"vcmi.lobby.handicap.income" : "Altera as várias rendas do jogador em porcentagem. Arredondado para cima.",
 	"vcmi.lobby.handicap.growth" : "Altera a taxa de produção das criaturas nas cidades possuídas pelo jogador. Arredondado para cima.",
 	"vcmi.lobby.handicap.growth" : "Altera a taxa de produção das criaturas nas cidades possuídas pelo jogador. Arredondado para cima.",
 	"vcmi.lobby.deleteUnsupportedSave" : "{Jogos salvos incompatíveis encontrados}\n\nO VCMI encontrou %d jogos salvos que não são mais compatíveis, possivelmente devido a diferenças nas versões do VCMI.\n\nVocê deseja excluí-los?",
 	"vcmi.lobby.deleteUnsupportedSave" : "{Jogos salvos incompatíveis encontrados}\n\nO VCMI encontrou %d jogos salvos que não são mais compatíveis, possivelmente devido a diferenças nas versões do VCMI.\n\nVocê deseja excluí-los?",
@@ -123,16 +123,16 @@
 
 
 	"vcmi.broadcast.failedLoadGame" : "Falha ao carregar o jogo",
 	"vcmi.broadcast.failedLoadGame" : "Falha ao carregar o jogo",
 	"vcmi.broadcast.command" : "Use '!help' para listar os comandos disponíveis",
 	"vcmi.broadcast.command" : "Use '!help' para listar os comandos disponíveis",
-	"vcmi.broadcast.simturn.end" : "Os turnos simultâneos terminaram",
-	"vcmi.broadcast.simturn.endBetween" : "Os turnos simultâneos entre os jogadores %s e %s terminaram",
+	"vcmi.broadcast.simturn.end" : "Turnos simultâneos encerrados",
+	"vcmi.broadcast.simturn.endBetween" : "Os turnos simultâneos entre os jogadores %s e %s encerraram",
 	"vcmi.broadcast.serverProblem" : "O servidor encontrou um problema",
 	"vcmi.broadcast.serverProblem" : "O servidor encontrou um problema",
 	"vcmi.broadcast.gameTerminated" : "o jogo foi encerrado",
 	"vcmi.broadcast.gameTerminated" : "o jogo foi encerrado",
 	"vcmi.broadcast.gameSavedAs" : "jogo salvo como",
 	"vcmi.broadcast.gameSavedAs" : "jogo salvo como",
-	"vcmi.broadcast.noCheater" : "Nenhum trapaçeiro registrado!",
-	"vcmi.broadcast.playerCheater" : "O jogador %s é um trapaçeiro!",
+	"vcmi.broadcast.noCheater" : "Nenhum trapaceiro registrado!",
+	"vcmi.broadcast.playerCheater" : "O jogador %s é um trapaceiro!",
 	"vcmi.broadcast.statisticFile" : "Os arquivos de estatísticas podem ser encontrados no diretório %s",
 	"vcmi.broadcast.statisticFile" : "Os arquivos de estatísticas podem ser encontrados no diretório %s",
 	"vcmi.broadcast.help.commands" : "Comandos disponíveis para o anfitrião:",
 	"vcmi.broadcast.help.commands" : "Comandos disponíveis para o anfitrião:",
-	"vcmi.broadcast.help.exit" : "'!exit' - termina imediatamente o jogo atual",
+	"vcmi.broadcast.help.exit" : "'!exit' - encerra imediatamente o jogo atual",
 	"vcmi.broadcast.help.kick" : "'!kick <player>' - expulsa o jogador especificado do jogo",
 	"vcmi.broadcast.help.kick" : "'!kick <player>' - expulsa o jogador especificado do jogo",
 	"vcmi.broadcast.help.save" : "'!save <filename>' - salva o jogo com o nome de arquivo especificado",
 	"vcmi.broadcast.help.save" : "'!save <filename>' - salva o jogo com o nome de arquivo especificado",
 	"vcmi.broadcast.help.statistic" : "'!statistic' - salva as estatísticas do jogo como arquivo csv",
 	"vcmi.broadcast.help.statistic" : "'!statistic' - salva as estatísticas do jogo como arquivo csv",
@@ -143,20 +143,20 @@
 	"vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - permite turnos simultâneos por um número determinado de dias, ou até o contato",
 	"vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - permite turnos simultâneos por um número determinado de dias, ou até o contato",
 	"vcmi.broadcast.vote.force" : "'!vote simturns force X' - força turnos simultâneos por um número determinado de dias, bloqueando os contatos dos jogadores",
 	"vcmi.broadcast.vote.force" : "'!vote simturns force X' - força turnos simultâneos por um número determinado de dias, bloqueando os contatos dos jogadores",
 	"vcmi.broadcast.vote.abort" : "'!vote simturns abort' - aborta os turnos simultâneos assim que este turno terminar",
 	"vcmi.broadcast.vote.abort" : "'!vote simturns abort' - aborta os turnos simultâneos assim que este turno terminar",
-	"vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - prolonga o temporizador base para todos os jogadores por um número determinado de segundos",
+	"vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - prolonga o cronômetro base para todos os jogadores por um número determinado de segundos",
 	"vcmi.broadcast.vote.noActive" : "Nenhuma votação ativa!",
 	"vcmi.broadcast.vote.noActive" : "Nenhuma votação ativa!",
 	"vcmi.broadcast.vote.yes" : "sim",
 	"vcmi.broadcast.vote.yes" : "sim",
 	"vcmi.broadcast.vote.no" : "não",
 	"vcmi.broadcast.vote.no" : "não",
 	"vcmi.broadcast.vote.notRecognized" : "Comando de votação não reconhecido!",
 	"vcmi.broadcast.vote.notRecognized" : "Comando de votação não reconhecido!",
-	"vcmi.broadcast.vote.success.untilContacts" : "Votação bem-sucedida. Os turnos simultâneos ocorrerão por mais %s dias, ou até o contato",
-	"vcmi.broadcast.vote.success.contactsBlocked" : "Votação bem-sucedida. Os turnos simultâneos ocorrerão por mais %s dias. Os contatos estão bloqueados",
-	"vcmi.broadcast.vote.success.nextDay" : "Votação bem-sucedida. Os turnos simultâneos terminarão no próximo dia",
-	"vcmi.broadcast.vote.success.timer" : "Votação bem-sucedida. O temporizador para todos os jogadores foi prolongado por %s segundos",
+	"vcmi.broadcast.vote.success.untilContacts" : "Votação concluída com sucesso. Os turnos simultâneos ocorrerão por mais %s dias, ou até o contato",
+	"vcmi.broadcast.vote.success.contactsBlocked" : "Votação concluída com sucesso. Os turnos simultâneos ocorrerão por mais %s dias. Os contatos estão bloqueados",
+	"vcmi.broadcast.vote.success.nextDay" : "Votação concluída com sucesso. Os turnos simultâneos serão encerrados no próximo dia",
+	"vcmi.broadcast.vote.success.timer" : "Votação concluída com sucesso. O cronômetro para todos os jogadores foi prolongado em %s segundos",
 	"vcmi.broadcast.vote.aborted" : "O jogador votou contra a mudança. Votação abortada",
 	"vcmi.broadcast.vote.aborted" : "O jogador votou contra a mudança. Votação abortada",
-	"vcmi.broadcast.vote.start.untilContacts" : "Iniciada votação para permitir turnos simultâneos por mais %s dias",
-	"vcmi.broadcast.vote.start.contactsBlocked" : "Iniciada votação para forçar turnos simultâneos por mais %s dias",
-	"vcmi.broadcast.vote.start.nextDay" : "Iniciada votação para terminar os turnos simultâneos a partir do próximo dia",
-	"vcmi.broadcast.vote.start.timer" : "Iniciada votação para prolongar o temporizador para todos os jogadores por %s segundos",
+	"vcmi.broadcast.vote.start.untilContacts" : "Votação iniciada para permitir turnos simultâneos por mais %s dias",
+	"vcmi.broadcast.vote.start.contactsBlocked" : "Votação iniciada para forçar turnos simultâneos por mais %s dias",
+	"vcmi.broadcast.vote.start.nextDay" : "Votação iniciada para terminar os turnos simultâneos a partir do próximo dia",
+	"vcmi.broadcast.vote.start.timer" : "Votação iniciada para prolongar o cronômetro para todos os jogadores em %s segundos",
 	"vcmi.broadcast.vote.hint" : "Digite '!vote yes' para concordar com esta mudança ou '!vote no' para votar contra",
 	"vcmi.broadcast.vote.hint" : "Digite '!vote yes' para concordar com esta mudança ou '!vote no' para votar contra",
 		
 		
 	"vcmi.lobby.login.title" : "Sala de Espera Online do VCMI",
 	"vcmi.lobby.login.title" : "Sala de Espera Online do VCMI",
@@ -294,7 +294,7 @@
 	"vcmi.systemOptions.longTouchMenu.hover"     : "Selecionar Intervalo de Toque Longo",
 	"vcmi.systemOptions.longTouchMenu.hover"     : "Selecionar Intervalo de Toque Longo",
 	"vcmi.systemOptions.longTouchMenu.help"      : "Muda a duração do intervalo de toque longo.",
 	"vcmi.systemOptions.longTouchMenu.help"      : "Muda a duração do intervalo de toque longo.",
 	"vcmi.systemOptions.longTouchMenu.entry"     : "%d milissegundos",
 	"vcmi.systemOptions.longTouchMenu.entry"     : "%d milissegundos",
-	"vcmi.systemOptions.framerateButton.hover"  : "Mostrar FPS",
+	"vcmi.systemOptions.framerateButton.hover"  : "Mostrar QPS",
 	"vcmi.systemOptions.framerateButton.help"   : "{Mostra os Quadros Por Segundo}\n\nAtiva ou desativa a visibilidade do contador de Quadros Por Segundo no canto da janela do jogo.",
 	"vcmi.systemOptions.framerateButton.help"   : "{Mostra os Quadros Por Segundo}\n\nAtiva ou desativa a visibilidade do contador de Quadros Por Segundo no canto da janela do jogo.",
 	"vcmi.systemOptions.hapticFeedbackButton.hover"  : "Resposta Tátil",
 	"vcmi.systemOptions.hapticFeedbackButton.hover"  : "Resposta Tátil",
 	"vcmi.systemOptions.hapticFeedbackButton.help"   : "{Resposta Tátil}\n\nAtiva ou desativa a resposta tátil nos toques na tela.",
 	"vcmi.systemOptions.hapticFeedbackButton.help"   : "{Resposta Tátil}\n\nAtiva ou desativa a resposta tátil nos toques na tela.",
@@ -307,8 +307,8 @@
 
 
 	"vcmi.adventureOptions.infoBarPick.hover" : "Mensagens no Painel de Informações",
 	"vcmi.adventureOptions.infoBarPick.hover" : "Mensagens no Painel de Informações",
 	"vcmi.adventureOptions.infoBarPick.help" : "{Mostra as Mensagens no Painel de Informações}\n\nSempre que possível, as mensagens do jogo provenientes de objetos no mapa serão mostradas no painel de informações, em vez de aparecerem em uma janela separada.",
 	"vcmi.adventureOptions.infoBarPick.help" : "{Mostra as Mensagens no Painel de Informações}\n\nSempre que possível, as mensagens do jogo provenientes de objetos no mapa serão mostradas no painel de informações, em vez de aparecerem em uma janela separada.",
-	"vcmi.adventureOptions.numericQuantities.hover" : "Quantidades Numéricas de Criaturas",
-	"vcmi.adventureOptions.numericQuantities.help" : "{Quantidades Numéricas de Criaturas}\n\nMostra as quantidades aproximadas de criaturas inimigas no formato numérico A-B.",
+	"vcmi.adventureOptions.numericQuantities.hover" : "Quantidades de Criaturas",
+	"vcmi.adventureOptions.numericQuantities.help" : "{Quantidades de Criaturas}\n\nMostra as quantidades aproximadas de criaturas inimigas no formato numérico A-B.",
 	"vcmi.adventureOptions.forceMovementInfo.hover" : "Sempre Mostrar o Custo de Movimento",
 	"vcmi.adventureOptions.forceMovementInfo.hover" : "Sempre Mostrar o Custo de Movimento",
 	"vcmi.adventureOptions.forceMovementInfo.help" : "{Sempre Mostrar o Custo de Movimento}\n\nSempre mostra os dados de pontos de movimento na barra de status (em vez de apenas visualizá-los enquanto você mantém pressionada a tecla ALT).",
 	"vcmi.adventureOptions.forceMovementInfo.help" : "{Sempre Mostrar o Custo de Movimento}\n\nSempre mostra os dados de pontos de movimento na barra de status (em vez de apenas visualizá-los enquanto você mantém pressionada a tecla ALT).",
 	"vcmi.adventureOptions.showGrid.hover" : "Mostrar Grade",
 	"vcmi.adventureOptions.showGrid.hover" : "Mostrar Grade",
@@ -321,8 +321,8 @@
 	"vcmi.adventureOptions.leftButtonDrag.help" : "{Arrastar Mapa com o Botão Esquerdo}\n\nQuando ativado, mover o mouse com o botão esquerdo pressionado irá arrastar a visualização do mapa de aventura.",
 	"vcmi.adventureOptions.leftButtonDrag.help" : "{Arrastar Mapa com o Botão Esquerdo}\n\nQuando ativado, mover o mouse com o botão esquerdo pressionado irá arrastar a visualização do mapa de aventura.",
 	"vcmi.adventureOptions.rightButtonDrag.hover" : "Botão Dir. Arrasta",
 	"vcmi.adventureOptions.rightButtonDrag.hover" : "Botão Dir. Arrasta",
 	"vcmi.adventureOptions.rightButtonDrag.help" : "{Arrastar Mapa com o Botão Direito}\n\nQuando ativado, mover o mouse com o botão direito pressionado irá arrastar a visualização do mapa de aventura.",
 	"vcmi.adventureOptions.rightButtonDrag.help" : "{Arrastar Mapa com o Botão Direito}\n\nQuando ativado, mover o mouse com o botão direito pressionado irá arrastar a visualização do mapa de aventura.",
-	"vcmi.adventureOptions.smoothDragging.hover" : "Arrastar Suavemente o Mapa",
-	"vcmi.adventureOptions.smoothDragging.help" : "{Arrasta o Mapa Suavemente}\n\nQuando ativado, o arrasto do mapa tem um efeito de movimento moderno.",
+	"vcmi.adventureOptions.smoothDragging.hover" : "Arrasto Suave do Mapa",
+	"vcmi.adventureOptions.smoothDragging.help" : "{Arrasto Suave do Mapa}\n\nQuando ativado, o arrasto do mapa tem um efeito de movimento moderno.",
 	"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Omitir Efeitos de Desvanecimento",
 	"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Omitir Efeitos de Desvanecimento",
 	"vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Omite os Efeitos de Desvanecimento}\n\nQuando ativado, omite o desvanecimento de objetos e efeitos semelhantes (coleta de recursos, embarque em navios etc.). Torna a interface do usuário mais reativa em alguns casos, em detrimento da estética. Especialmente útil em jogos PvP. Para obter velocidade de movimento máxima, o pulo está ativo independentemente desta configuração.",
 	"vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Omite os Efeitos de Desvanecimento}\n\nQuando ativado, omite o desvanecimento de objetos e efeitos semelhantes (coleta de recursos, embarque em navios etc.). Torna a interface do usuário mais reativa em alguns casos, em detrimento da estética. Especialmente útil em jogos PvP. Para obter velocidade de movimento máxima, o pulo está ativo independentemente desta configuração.",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
@@ -354,7 +354,7 @@
 	"vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Mostrar Limites de Alcance de Atiradores",
 	"vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Mostrar Limites de Alcance de Atiradores",
 	"vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Mostra os Limites de Alcance dos Atiradores ao Passar o Mouse}\n\nMostra os limites de alcance do atirador quando você passa o mouse sobre ele.",
 	"vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Mostra os Limites de Alcance dos Atiradores ao Passar o Mouse}\n\nMostra os limites de alcance do atirador quando você passa o mouse sobre ele.",
 	"vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Mostrar Janelas de Estatísticas de Heróis",
 	"vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Mostrar Janelas de Estatísticas de Heróis",
-	"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Mostra as Janelas de Estatísticas de Heróis}\n\nAlterna permanentemente as janelas de estatísticas dos heróis que mostram estatísticas primárias e pontos de feitiço.",
+	"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Mostra as Janelas de Estatísticas de Heróis}\n\nAlterna permanentemente as janelas de estatísticas dos heróis que mostram estatísticas primárias e pontos de mana.",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Pular Música de Introdução",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Pular Música de Introdução",
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Pula a Música de Introdução}\n\nPermite ações durante a música de introdução que toca no início de cada batalha.",
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Pula a Música de Introdução}\n\nPermite ações durante a música de introdução que toca no início de cada batalha.",
 	"vcmi.battleOptions.endWithAutocombat.hover": "Terminar a batalha",
 	"vcmi.battleOptions.endWithAutocombat.hover": "Terminar a batalha",
@@ -674,7 +674,7 @@
 	"core.bonus.HEALER.name" : "Curandeiro",
 	"core.bonus.HEALER.name" : "Curandeiro",
 	"core.bonus.HEALER.description" : "Cura unidades aliadas",
 	"core.bonus.HEALER.description" : "Cura unidades aliadas",
 	"core.bonus.HP_REGENERATION.name" : "Regeneração",
 	"core.bonus.HP_REGENERATION.name" : "Regeneração",
-	"core.bonus.HP_REGENERATION.description" : "Cura ${val} pontos de vida a cada rodada",
+	"core.bonus.HP_REGENERATION.description" : "Cura ${val} pontos de vida a cada turno",
 	"core.bonus.JOUSTING.name" : "Carga do Campeão",
 	"core.bonus.JOUSTING.name" : "Carga do Campeão",
 	"core.bonus.JOUSTING.description" : "+${val}% de dano para cada hexágono percorrido",
 	"core.bonus.JOUSTING.description" : "+${val}% de dano para cada hexágono percorrido",
 	"core.bonus.KING.name" : "Rei",
 	"core.bonus.KING.name" : "Rei",
@@ -705,7 +705,7 @@
 	"core.bonus.NO_WALL_PENALTY.description" : "Causa dano total\ndurante cerco",
 	"core.bonus.NO_WALL_PENALTY.description" : "Causa dano total\ndurante cerco",
 	"core.bonus.NON_LIVING.name" : "Não Vivo",
 	"core.bonus.NON_LIVING.name" : "Não Vivo",
 	"core.bonus.NON_LIVING.description" : "Imune a muitos efeitos",
 	"core.bonus.NON_LIVING.description" : "Imune a muitos efeitos",
-	"core.bonus.RANDOM_SPELLCASTER.name" : "Lançador de Feitiços Aleatório",
+	"core.bonus.RANDOM_SPELLCASTER.name" : "Conjurador Aleatório",
 	"core.bonus.RANDOM_SPELLCASTER.description" : "Pode lançar um feitiço aleatório",
 	"core.bonus.RANDOM_SPELLCASTER.description" : "Pode lançar um feitiço aleatório",
 	"core.bonus.RANGED_RETALIATION.name" : "Contra-ataques à Distância",
 	"core.bonus.RANGED_RETALIATION.name" : "Contra-ataques à Distância",
 	"core.bonus.RANGED_RETALIATION.description" : "Realiza contra-ataques à distância",
 	"core.bonus.RANGED_RETALIATION.description" : "Realiza contra-ataques à distância",

+ 25 - 3
Mods/vcmi/Content/config/swedish.json

@@ -41,7 +41,7 @@
 	"vcmi.capitalColors.3" : "Grön",
 	"vcmi.capitalColors.3" : "Grön",
 	"vcmi.capitalColors.4" : "Orange",
 	"vcmi.capitalColors.4" : "Orange",
 	"vcmi.capitalColors.5" : "Lila",
 	"vcmi.capitalColors.5" : "Lila",
-	"vcmi.capitalColors.6" : "Grönblå",
+	"vcmi.capitalColors.6" : "Turkos",
 	"vcmi.capitalColors.7" : "Rosa",
 	"vcmi.capitalColors.7" : "Rosa",
 
 
 	"vcmi.heroOverview.startingArmy"    : "Startarmé",
 	"vcmi.heroOverview.startingArmy"    : "Startarmé",
@@ -70,7 +70,7 @@
 	"vcmi.radialWheel.heroDismiss"       : "Avfärda hjälten",
 	"vcmi.radialWheel.heroDismiss"       : "Avfärda hjälten",
 
 
 	"vcmi.radialWheel.moveTop"    : "Flytta längst upp",
 	"vcmi.radialWheel.moveTop"    : "Flytta längst upp",
-	"vcmi.radialWheel.moveUp"     : "Flytta upp",
+	"vcmi.radialWheel.moveUp"     : "Flytta uppåt",
 	"vcmi.radialWheel.moveDown"   : "Flytta nedåt",
 	"vcmi.radialWheel.moveDown"   : "Flytta nedåt",
 	"vcmi.radialWheel.moveBottom" : "Flytta längst ner",
 	"vcmi.radialWheel.moveBottom" : "Flytta längst ner",
 
 
@@ -762,5 +762,27 @@
 	"core.bonus.MECHANICAL.name"                         : "Mekanisk",
 	"core.bonus.MECHANICAL.name"                         : "Mekanisk",
 	"core.bonus.MECHANICAL.description"                  : "Immun mot många effekter, reparerbar.",
 	"core.bonus.MECHANICAL.description"                  : "Immun mot många effekter, reparerbar.",
 	"core.bonus.PRISM_HEX_ATTACK_BREATH.name"            : "Prism-andedräkt",
 	"core.bonus.PRISM_HEX_ATTACK_BREATH.name"            : "Prism-andedräkt",
-	"core.bonus.PRISM_HEX_ATTACK_BREATH.description"     : "Treriktad andedräkt."
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.description"     : "Treriktad andedräkt.",
+
+	"spell.core.castleMoat.name"            : "Vallgrav",
+	"spell.core.castleMoatTrigger.name"     : "Vallgrav",
+	"spell.core.catapultShot.name"          : "Katapultskott",
+	"spell.core.cyclopsShot.name"           : "Katapultskott",
+	"spell.core.dungeonMoat.name"           : "Kokande olja",
+	"spell.core.dungeonMoatTrigger.name"    : "Kokande olja",
+	"spell.core.fireWallTrigger.name"       : "Eldvägg",
+	"spell.core.firstAid.name"              : "Första hjälpen",
+	"spell.core.fortressMoat.name"          : "Kokande tjära",
+	"spell.core.fortressMoatTrigger.name"   : "Kokande tjära",
+	"spell.core.infernoMoat.name"           : "Lava",
+	"spell.core.infernoMoatTrigger.name"    : "Lava",
+	"spell.core.landMineTrigger.name"       : "Landmina",
+	"spell.core.necropolisMoat.name"        : "Vallgrav med ben",
+	"spell.core.necropolisMoatTrigger.name" : "Vallgrav med ben",
+	"spell.core.rampartMoat.name"           : "Törnbuske",
+	"spell.core.rampartMoatTrigger.name"    : "Törnbuske",
+	"spell.core.strongholdMoat.name"        : "Spetsiga pålar",
+	"spell.core.strongholdMoatTrigger.name" : "Spetsiga pålar",
+	"spell.core.summonDemons.name"          : "Sammankalla demoner",
+	"spell.core.towerMoat.name"             : "Landmina"
 }
 }

+ 157 - 4
Mods/vcmi/Content/config/ukrainian.json

@@ -12,6 +12,11 @@
 	"vcmi.adventureMap.monsterThreat.levels.9"  : "Нездоланна",
 	"vcmi.adventureMap.monsterThreat.levels.9"  : "Нездоланна",
 	"vcmi.adventureMap.monsterThreat.levels.10" : "Смертельна",
 	"vcmi.adventureMap.monsterThreat.levels.10" : "Смертельна",
 	"vcmi.adventureMap.monsterThreat.levels.11" : "Неможлива",
 	"vcmi.adventureMap.monsterThreat.levels.11" : "Неможлива",
+	"vcmi.adventureMap.monsterLevel"            : "\n\n%TOWN, істота%ATTACK_TYPE, %LEVELго рівня",
+	"vcmi.adventureMap.monsterMeleeType"        : " ближнього бою",
+	"vcmi.adventureMap.monsterRangedType"       : "-стрілок",
+	"vcmi.adventureMap.search.hover"            : "Шукати об'єкт мапи",
+	"vcmi.adventureMap.search.help"             : "Оберіть об'єкт для пошуку на мапі.",
 
 
 	"vcmi.adventureMap.confirmRestartGame"  : "Ви впевнені, що хочете перезапустити гру?",
 	"vcmi.adventureMap.confirmRestartGame"  : "Ви впевнені, що хочете перезапустити гру?",
 	"vcmi.adventureMap.noTownWithMarket"    : "Немає доступних ринків!",
 	"vcmi.adventureMap.noTownWithMarket"    : "Немає доступних ринків!",
@@ -20,8 +25,16 @@
 	"vcmi.adventureMap.playerAttacked"      : "Гравця атаковано: %s",
 	"vcmi.adventureMap.playerAttacked"      : "Гравця атаковано: %s",
 	"vcmi.adventureMap.moveCostDetails" : "Очки руху - Вартість: %TURNS ходів + %POINTS очок. Залишок очок: %REMAINING",
 	"vcmi.adventureMap.moveCostDetails" : "Очки руху - Вартість: %TURNS ходів + %POINTS очок. Залишок очок: %REMAINING",
 	"vcmi.adventureMap.moveCostDetailsNoTurns" : "Очки руху - Вартість: %POINTS очок, Залишок очок: %REMAINING",
 	"vcmi.adventureMap.moveCostDetailsNoTurns" : "Очки руху - Вартість: %POINTS очок, Залишок очок: %REMAINING",
+	"vcmi.adventureMap.movementPointsHeroInfo" : "(Очки руху: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Вибачте, функція повтору ходу суперника ще не реалізована!",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Вибачте, функція повтору ходу суперника ще не реалізована!",
 
 
+	"vcmi.bonusSource.artifact" : "Артифакт",
+	"vcmi.bonusSource.creature" : "Здібність",
+	"vcmi.bonusSource.spell" : "Закляття",
+	"vcmi.bonusSource.hero" : "Герой",
+	"vcmi.bonusSource.commander" : "Командир",
+	"vcmi.bonusSource.other" : "Інше",
+
 	"vcmi.capitalColors.0" : "Червоний",
 	"vcmi.capitalColors.0" : "Червоний",
 	"vcmi.capitalColors.1" : "Синій",
 	"vcmi.capitalColors.1" : "Синій",
 	"vcmi.capitalColors.2" : "Сірий",
 	"vcmi.capitalColors.2" : "Сірий",
@@ -36,6 +49,12 @@
 	"vcmi.heroOverview.secondarySkills" : "Навички",
 	"vcmi.heroOverview.secondarySkills" : "Навички",
 	"vcmi.heroOverview.spells" : "Закляття",
 	"vcmi.heroOverview.spells" : "Закляття",
 
 
+	"vcmi.quickExchange.moveUnit" : "Перемістити загін",
+	"vcmi.quickExchange.moveAllUnits" : "Перемістити усі загони",
+	"vcmi.quickExchange.swapAllUnits" : "Обміняти армії",
+	"vcmi.quickExchange.moveAllArtifacts" : "Перемістити усі артефакти",
+	"vcmi.quickExchange.swapAllArtifacts" : "Обміняти усі артефакти",
+
 	"vcmi.radialWheel.mergeSameUnit" : "Об'єднати однакових істот",
 	"vcmi.radialWheel.mergeSameUnit" : "Об'єднати однакових істот",
 	"vcmi.radialWheel.fillSingleUnit" : "Заповнити одиничними істотами",
 	"vcmi.radialWheel.fillSingleUnit" : "Заповнити одиничними істотами",
 	"vcmi.radialWheel.splitSingleUnit" : "Відділити одну істоту",
 	"vcmi.radialWheel.splitSingleUnit" : "Відділити одну істоту",
@@ -55,8 +74,26 @@
 	"vcmi.radialWheel.moveDown" : "Перемістити вниз",
 	"vcmi.radialWheel.moveDown" : "Перемістити вниз",
 	"vcmi.radialWheel.moveBottom" : "Перемістити у кінець",
 	"vcmi.radialWheel.moveBottom" : "Перемістити у кінець",
 
 
+	"vcmi.randomMap.description" : "Мапа створена генератором випадкових мап.\nШаблон був %s, розмір %dx%d, рівнів %d, гравців %d, гравців-ШІ %d, %s, %s монстри, мапа VCMI",
+	"vcmi.randomMap.description.isHuman" : ", %s людина",
+	"vcmi.randomMap.description.townChoice" : ", %s обрав замок %s",
+	"vcmi.randomMap.description.water.none" : "води немає",
+	"vcmi.randomMap.description.water.normal" : "вода присутня",
+	"vcmi.randomMap.description.water.islands" : "острівна",
+	"vcmi.randomMap.description.monster.weak" : "слабкі",
+	"vcmi.randomMap.description.monster.normal" : "звичайні",
+	"vcmi.randomMap.description.monster.strong" : "сильні",
+
 	"vcmi.spellBook.search" : "шукати...",
 	"vcmi.spellBook.search" : "шукати...",
 
 
+	"vcmi.spellResearch.canNotAfford" : "Ви не можете дозволити собі замінити {%SPELL1} на {%SPELL2}. Але ви все одно можете відкинути це закляття і продовжити дослідження заклять.",
+	"vcmi.spellResearch.comeAgain" : "Сьогодні дослідження вже зроблено. Приходьте завтра..",
+	"vcmi.spellResearch.pay" : "Ви хочете замінити {%SPELL1} на {%SPELL2}? Або відкинути це закляття і продовжити дослідження заклять??",
+	"vcmi.spellResearch.research" : "Дослідити це закляття",
+	"vcmi.spellResearch.skip" : "Пропустити це закляття",
+	"vcmi.spellResearch.abort" : "Припинити",
+	"vcmi.spellResearch.noMoreSpells" : "Більше немає жодного закляття, доступного для дослідження.",
+
 	"vcmi.mainMenu.serverConnecting" : "Підключення...",
 	"vcmi.mainMenu.serverConnecting" : "Підключення...",
 	"vcmi.mainMenu.serverAddressEnter" : "Вкажіть адресу:",
 	"vcmi.mainMenu.serverAddressEnter" : "Вкажіть адресу:",
 	"vcmi.mainMenu.serverConnectionFailed" : "Помилка з'єднання",
 	"vcmi.mainMenu.serverConnectionFailed" : "Помилка з'єднання",
@@ -73,6 +110,56 @@
 	"vcmi.lobby.sortDate" : "Сортувати мапи за датою зміни",
 	"vcmi.lobby.sortDate" : "Сортувати мапи за датою зміни",
 	"vcmi.lobby.backToLobby" : "Назад до лобі",
 	"vcmi.lobby.backToLobby" : "Назад до лобі",
 
 
+	"vcmi.lobby.author" : "Автор",
+	"vcmi.lobby.handicap" : "Гандикап",
+	"vcmi.lobby.handicap.resource" : "Дає гравцям відповідні ресурси для початку гри на додаток до звичних стартових ресурсів. Від'ємні значення дозволені, але обмежені загальним значенням 0 (гравець ніколи не починає з від'ємними ресурсами).",
+	"vcmi.lobby.handicap.income" : "Змінює різні доходи гравця на певний відсоток. Округлюється у більшу сторону.",
+	"vcmi.lobby.handicap.growth" : "Змінює рівень приросту істот у містах, якими володіє гравець. Округлюється в більшу сторону.",
+	"vcmi.lobby.deleteUnsupportedSave" : "{Знайдено непідтримувані збереження}\n\nVCMI знайдено %d збережених ігор, які більше не підтримуються, найімовірніше, через невідповідність версій VCMI.\n\nЧи бажаєте вилучити їх?",
+	"vcmi.lobby.deleteSaveGameTitle" : "Виберіть гру для видалення",
+	"vcmi.lobby.deleteMapTitle" : "Виберіть сценарій для видалення",
+	"vcmi.lobby.deleteFile" : "Чи бажаєте вилучити цей файл?",
+	"vcmi.lobby.deleteFolder" : "Чи бажаєте вилучити цю теку?",
+	"vcmi.lobby.deleteMode" : "Перехід у режим видалення та назад",
+
+	"vcmi.broadcast.failedLoadGame" : "Не вдалося завантажити гру",
+	"vcmi.broadcast.command" : "Введіть '!help' у чаті гри, щоб переглянути список доступних команд",
+	"vcmi.broadcast.simturn.end" : "Одночасні ходи закінчилися",
+	"vcmi.broadcast.simturn.endBetween" : "Одночасні ходи між гравцями %s та %s завершилися",
+	"vcmi.broadcast.serverProblem" : "Сервер зіткнувся з проблемою",
+	"vcmi.broadcast.gameTerminated" : "гру було завершено",
+	"vcmi.broadcast.gameSavedAs" : "гру збережено як",
+	"vcmi.broadcast.noCheater" : "Читерів не зареєстровано!",
+	"vcmi.broadcast.playerCheater" : "Гравець %s - шахрай!",
+	"vcmi.broadcast.statisticFile" : "Файли статистики можна знайти в каталозі %s",
+	"vcmi.broadcast.help.commands" : "Команди доступні для хоста:",
+	"vcmi.broadcast.help.exit" : "'!exit' - негайно завершує поточну гру",
+	"vcmi.broadcast.help.kick" : "'!kick <player>' - вигнати вказаного гравця з гри",
+	"vcmi.broadcast.help.save" : "'!save <filename>' - зберегти гру під вказаним ім'ям",
+	"vcmi.broadcast.help.statistic" : "'!statistic' - зберегти статистику гри у форматі csv",
+	"vcmi.broadcast.help.commandsAll" : "Команди доступні всім гравцям:",
+	"vcmi.broadcast.help.help" : "'!help' - відобразити цю довідку",
+	"vcmi.broadcast.help.cheaters" : "'!cheaters' - список гравців, які вводили чит-команду під час гри",
+	"vcmi.broadcast.help.vote" : "'!vote' - дозволяє змінити деякі налаштування гри, якщо всі гравці проголосують за це",
+	"vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - дозволяти одночасні ходи на визначену кількість днів або до контакту",
+	"vcmi.broadcast.vote.force" : "'!vote simturns force X' - увімкнути одночасні ходи на визначену кількість днів, блокуючи контакти гравців",
+	"vcmi.broadcast.vote.abort" : "'!vote simturns abort' - завершити одночасні ходи, як тільки цей хід закінчиться",
+	"vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - подовжити базовий таймер для всіх гравців на вказану кількість секунд",
+	"vcmi.broadcast.vote.noActive" : "Активне голосування відсутнє!",
+	"vcmi.broadcast.vote.yes" : "так",
+	"vcmi.broadcast.vote.no" : "ні",
+	"vcmi.broadcast.vote.notRecognized" : "Команда для голосування не розпізнана!",
+	"vcmi.broadcast.vote.success.untilContacts" : "Голосування пройшло успішно. Одночасні ходи триватимуть ще %s днів, або до контакту",
+	"vcmi.broadcast.vote.success.contactsBlocked" : "Голосування пройшло успішно. Одночасні ходи триватимуть ще %s днів. Контакти між гравцями заблоковані",
+	"vcmi.broadcast.vote.success.nextDay" : "Голосування пройшло успішно. Одночасні ходи закінчаться на наступний день",
+	"vcmi.broadcast.vote.success.timer" : "Голосування пройшло успішно. Таймер для всіх гравців було подовжено на %s секунд",
+	"vcmi.broadcast.vote.aborted" : "Гравець проголосував проти змін. Голосування перервано",
+	"vcmi.broadcast.vote.start.untilContacts" : "Розпочато голосування, за одночасні ходи на %s більше днів або до контакту гравців",
+	"vcmi.broadcast.vote.start.contactsBlocked" : "Розпочато голосування за безумовні одночасні ходи на %s більше днів",
+	"vcmi.broadcast.vote.start.nextDay" : "Розпочато голосування за припинення одночасних ходів з наступного дня",
+	"vcmi.broadcast.vote.start.timer" : "Розпочато голосування за продовження таймера для всіх гравців на %s секунд",
+	"vcmi.broadcast.vote.hint" : "Введіть \"!vote yes\", щоб погодитися з цією зміною, або \"!vote no\", щоб проголосувати проти неї",
+
 	"vcmi.lobby.login.title" : "Онлайн лобі VCMI",
 	"vcmi.lobby.login.title" : "Онлайн лобі VCMI",
 	"vcmi.lobby.login.username" : "Логін:",
 	"vcmi.lobby.login.username" : "Логін:",
 	"vcmi.lobby.login.connecting" : "Підключення...",
 	"vcmi.lobby.login.connecting" : "Підключення...",
@@ -80,6 +167,7 @@
 	"vcmi.lobby.login.create" : "Створити акаунт",
 	"vcmi.lobby.login.create" : "Створити акаунт",
 	"vcmi.lobby.login.login" : "Увійти",
 	"vcmi.lobby.login.login" : "Увійти",
 	"vcmi.lobby.login.as" : "Увійти як %s",
 	"vcmi.lobby.login.as" : "Увійти як %s",
+	"vcmi.lobby.login.spectator" : "Спостерігач",
 	"vcmi.lobby.header.rooms" : "Активні кімнати - %d",
 	"vcmi.lobby.header.rooms" : "Активні кімнати - %d",
 	"vcmi.lobby.header.channels" : "Канали чату",
 	"vcmi.lobby.header.channels" : "Канали чату",
 	"vcmi.lobby.header.chat.global" : "Глобальний ігровий чат - %s", // %s -> language name
 	"vcmi.lobby.header.chat.global" : "Глобальний ігровий чат - %s", // %s -> language name
@@ -135,11 +223,14 @@
 
 
 	"vcmi.client.errors.invalidMap" : "{Пошкоджена карта або кампанія}\n\nНе вдалося запустити гру! Вибрана карта або кампанія може бути невірною або пошкодженою. Причина:\n%s",
 	"vcmi.client.errors.invalidMap" : "{Пошкоджена карта або кампанія}\n\nНе вдалося запустити гру! Вибрана карта або кампанія може бути невірною або пошкодженою. Причина:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{Не вистачає файлів даних}\n\nФайли даних кампаній не знайдено! Можливо, ви використовуєте неповні або пошкоджені файли даних Heroes 3. Будь ласка, перевстановіть дані гри.",
 	"vcmi.client.errors.missingCampaigns" : "{Не вистачає файлів даних}\n\nФайли даних кампаній не знайдено! Можливо, ви використовуєте неповні або пошкоджені файли даних Heroes 3. Будь ласка, перевстановіть дані гри.",
+	"vcmi.server.errors.disconnected" : "{Помилка мережі}\n\nВтрачено зв'язок з сервером гри!",
+	"vcmi.server.errors.playerLeft" : "{Гравець покинув гру}\n\n%s гравець від'єднався від гри!", //%s -> player color
 	"vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його",
 	"vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його",
 	"vcmi.server.errors.modsToEnable"    : "{Потрібні модифікації для завантаження гри}",
 	"vcmi.server.errors.modsToEnable"    : "{Потрібні модифікації для завантаження гри}",
 	"vcmi.server.errors.modsToDisable"   : "{Модифікації що мають бути вимкнені}",
 	"vcmi.server.errors.modsToDisable"   : "{Модифікації що мають бути вимкнені}",
-	"vcmi.server.confirmReconnect"       : "Підключитися до минулої сесії?",
 	"vcmi.server.errors.unknownEntity" : "Не вдалося завантажити гру! У збереженій грі знайдено невідомий об'єкт '%s'! Це збереження може бути несумісним зі встановленою версією модифікацій!",
 	"vcmi.server.errors.unknownEntity" : "Не вдалося завантажити гру! У збереженій грі знайдено невідомий об'єкт '%s'! Це збереження може бути несумісним зі встановленою версією модифікацій!",
+	"vcmi.server.errors.wrongIdentified"   : "Ви були ідентифіковані як гравець %s, хоча очікували %s",
+	"vcmi.server.errors.notAllowed"   : "Ви не можете виконати цю дію!",
 	
 	
 	"vcmi.dimensionDoor.seaToLandError" : "Неможливо телепортуватися з моря на сушу або навпаки за допомогою просторової брами",
 	"vcmi.dimensionDoor.seaToLandError" : "Неможливо телепортуватися з моря на сушу або навпаки за допомогою просторової брами",
 
 
@@ -155,6 +246,38 @@
 	"vcmi.systemOptions.otherGroup" : "Інші налаштування",
 	"vcmi.systemOptions.otherGroup" : "Інші налаштування",
 	"vcmi.systemOptions.townsGroup" : "Екран міста",
 	"vcmi.systemOptions.townsGroup" : "Екран міста",
 
 
+	"vcmi.statisticWindow.statistics" : "Статистика",
+	"vcmi.statisticWindow.tsvCopy" : "Дані до буфера обміну",
+	"vcmi.statisticWindow.selectView" : "Оберіть представлення",
+	"vcmi.statisticWindow.value" : "Цінність",
+	"vcmi.statisticWindow.title.overview" : "Загальний огляд",
+	"vcmi.statisticWindow.title.resources" : "Ресурси",
+	"vcmi.statisticWindow.title.income" : "Прибуток",
+	"vcmi.statisticWindow.title.numberOfHeroes" : "К-сть героїв",
+	"vcmi.statisticWindow.title.numberOfTowns" : "К-сть міст",
+	"vcmi.statisticWindow.title.numberOfArtifacts" : "К-сть артефактів",
+	"vcmi.statisticWindow.title.numberOfDwellings" : "К-сть помешкань",
+	"vcmi.statisticWindow.title.numberOfMines" : "К-сть шахт",
+	"vcmi.statisticWindow.title.armyStrength" : "Сила армії",
+	"vcmi.statisticWindow.title.experience" : "Досвід",
+	"vcmi.statisticWindow.title.resourcesSpentArmy" : "Витрати на армію",
+	"vcmi.statisticWindow.title.resourcesSpentBuildings" : "Витрати на будівництво",
+	"vcmi.statisticWindow.title.mapExplored" : "Ступінь вивченості карти",
+	"vcmi.statisticWindow.param.playerName" : "Ім'я гравця",
+	"vcmi.statisticWindow.param.daysSurvived" : "Прожиті дні",
+	"vcmi.statisticWindow.param.maxHeroLevel" : "Макс. рівень героя",
+	"vcmi.statisticWindow.param.battleWinRatioHero" : "Частка перемог (проти героя)",
+	"vcmi.statisticWindow.param.battleWinRatioNeutral" : "Частка перемог (проти нейтральних)",
+	"vcmi.statisticWindow.param.battlesHero" : "Боїв (проти героя)",
+	"vcmi.statisticWindow.param.battlesNeutral" : "Боїв (проти нейтральних)",
+	"vcmi.statisticWindow.param.maxArmyStrength" : "Макс. сила армії",
+	"vcmi.statisticWindow.param.tradeVolume" : "Обсяг торгівлі",
+	"vcmi.statisticWindow.param.obeliskVisited" : "Відвідано обеліск",
+	"vcmi.statisticWindow.icon.townCaptured" : "Місто захоплено",
+	"vcmi.statisticWindow.icon.strongestHeroDefeated" : "Перемогли сильного героя суперника",
+	"vcmi.statisticWindow.icon.grailFound" : "Грааль знайдено",
+	"vcmi.statisticWindow.icon.defeated" : "Переможений",
+
 	"vcmi.systemOptions.fullscreenBorderless.hover" : "На весь екран (безрамкове вікно)",
 	"vcmi.systemOptions.fullscreenBorderless.hover" : "На весь екран (безрамкове вікно)",
 	"vcmi.systemOptions.fullscreenBorderless.help"  : "{На весь екран (безрамкове вікно)}\n\nЯкщо обрано, VCMI працюватиме у режимі безрамкового вікна на весь екран. У цьому режимі гра завжди використовує ту саму роздільну здатність, що й робочий стіл, ігноруючи вибрану роздільну здатність",
 	"vcmi.systemOptions.fullscreenBorderless.help"  : "{На весь екран (безрамкове вікно)}\n\nЯкщо обрано, VCMI працюватиме у режимі безрамкового вікна на весь екран. У цьому режимі гра завжди використовує ту саму роздільну здатність, що й робочий стіл, ігноруючи вибрану роздільну здатність",
 	"vcmi.systemOptions.fullscreenExclusive.hover"  : "На весь екран (ексклюзивний режим)",
 	"vcmi.systemOptions.fullscreenExclusive.hover"  : "На весь екран (ексклюзивний режим)",
@@ -195,8 +318,10 @@
 	"vcmi.adventureOptions.borderScroll.help" : "{{Прокрутка по краю}\n\nПрокручувати мапу пригод, коли курсор знаходиться біля краю вікна. Цю функцію можна вимкнути, утримуючи клавішу CTRL.",
 	"vcmi.adventureOptions.borderScroll.help" : "{{Прокрутка по краю}\n\nПрокручувати мапу пригод, коли курсор знаходиться біля краю вікна. Цю функцію можна вимкнути, утримуючи клавішу CTRL.",
 	"vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Керування істотами у вікні статусу",
 	"vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Керування істотами у вікні статусу",
 	"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Керування істотами у вікні статусу}\n\nДозволяє впорядковувати істот у вікні статусу замість циклічного перемикання між типовими компонентами",
 	"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Керування істотами у вікні статусу}\n\nДозволяє впорядковувати істот у вікні статусу замість циклічного перемикання між типовими компонентами",
-	"vcmi.adventureOptions.leftButtonDrag.hover" : "Переміщення мапи лівою кнопкою",
-	"vcmi.adventureOptions.leftButtonDrag.help" : "{Переміщення мапи лівою кнопкою}\n\nЯкщо увімкнено, переміщення миші з натиснутою лівою кнопкою буде перетягувати мапу пригод",
+	"vcmi.adventureOptions.leftButtonDrag.hover" : "Переміщення мапи ЛКМ",
+	"vcmi.adventureOptions.leftButtonDrag.help" : "{Переміщення мапи ЛКМ}\n\nЯкщо увімкнено, переміщення миші з натиснутою лівою кнопкою буде перетягувати мапу пригод",
+	"vcmi.adventureOptions.rightButtonDrag.hover" : "Переміщення мапи ПКМ",
+	"vcmi.adventureOptions.rightButtonDrag.help" : "{Переміщення мапи ПКМ}\n\nЯкщо увімкнено, переміщення миші з натиснутою правою кнопкою буде перетягувати мапу пригод",
 	"vcmi.adventureOptions.smoothDragging.hover" : "Плавне перетягування мапи",
 	"vcmi.adventureOptions.smoothDragging.hover" : "Плавне перетягування мапи",
 	"vcmi.adventureOptions.smoothDragging.help" : "{Плавне перетягування мапи}\n\nЯкщо увімкнено, перетягування мапи має сучасний ефект завершення.",
 	"vcmi.adventureOptions.smoothDragging.help" : "{Плавне перетягування мапи}\n\nЯкщо увімкнено, перетягування мапи має сучасний ефект завершення.",
 	"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Вимкнути ефекти зникнення",
 	"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Вимкнути ефекти зникнення",
@@ -235,6 +360,8 @@
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Пропускати вступну музику}\n\n Пропускати коротку музику, яка грає на початку кожної битви перед початком дії. Також можна пропустити, натиснувши клавішу ESC.",
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Пропускати вступну музику}\n\n Пропускати коротку музику, яка грає на початку кожної битви перед початком дії. Також можна пропустити, натиснувши клавішу ESC.",
 	"vcmi.battleOptions.endWithAutocombat.hover": "Завершує бій",
 	"vcmi.battleOptions.endWithAutocombat.hover": "Завершує бій",
 	"vcmi.battleOptions.endWithAutocombat.help": "{Завершує бій}\n\nАвто-бій миттєво завершує бій",
 	"vcmi.battleOptions.endWithAutocombat.help": "{Завершує бій}\n\nАвто-бій миттєво завершує бій",
+	"vcmi.battleOptions.showQuickSpell.hover": "Панель швидкого чарування",
+	"vcmi.battleOptions.showQuickSpell.help": "{Панель швидкого чарування}\n\nПоказати панель для швидкого вибору заклять.",
 
 
 	"vcmi.adventureMap.revisitObject.hover" : "Відвідати Об'єкт",
 	"vcmi.adventureMap.revisitObject.hover" : "Відвідати Об'єкт",
 	"vcmi.adventureMap.revisitObject.help" : "{Відвідати Об'єкт}\n\nЯкщо герой в даний момент стоїть на об'єкті мапи, він може знову відвідати цю локацію.",
 	"vcmi.adventureMap.revisitObject.help" : "{Відвідати Об'єкт}\n\nЯкщо герой в даний момент стоїть на об'єкті мапи, він може знову відвідати цю локацію.",
@@ -284,6 +411,9 @@
 	"vcmi.townHall.missingBase"             : "Спочатку необхідно звести початкову будівлю: %s",
 	"vcmi.townHall.missingBase"             : "Спочатку необхідно звести початкову будівлю: %s",
 	"vcmi.townHall.noCreaturesToRecruit"    : "Немає істот, яких можна завербувати!",
 	"vcmi.townHall.noCreaturesToRecruit"    : "Немає істот, яких можна завербувати!",
 
 
+	"vcmi.townStructure.bank.borrow" : "Ви заходите в банк. Вас бачить банкір і каже: 'Ми зробили для вас спеціальну пропозицію. Ви можете взяти у нас позику в розмірі 2500 золотих на 5 днів. Але щодня ви повинні будете повертати по 500 золотих'.",
+	"vcmi.townStructure.bank.payBack" : "Ви заходите в банк. Банкір бачить вас і каже: 'Ви вже отримали позику. Погасіть її, перш ніж брати нову позику'.",
+
 	"vcmi.logicalExpressions.anyOf"  : "Будь-що з перерахованого:",
 	"vcmi.logicalExpressions.anyOf"  : "Будь-що з перерахованого:",
 	"vcmi.logicalExpressions.allOf"  : "Все з перерахованого:",
 	"vcmi.logicalExpressions.allOf"  : "Все з перерахованого:",
 	"vcmi.logicalExpressions.noneOf" : "Нічого з перерахованого:",
 	"vcmi.logicalExpressions.noneOf" : "Нічого з перерахованого:",
@@ -292,6 +422,13 @@
 	"vcmi.heroWindow.openCommander.help"  : "Показує інформацію про командира героя",
 	"vcmi.heroWindow.openCommander.help"  : "Показує інформацію про командира героя",
 	"vcmi.heroWindow.openBackpack.hover" : "Відкрити вікно рюкзака з артефактами",
 	"vcmi.heroWindow.openBackpack.hover" : "Відкрити вікно рюкзака з артефактами",
 	"vcmi.heroWindow.openBackpack.help"  : "Відкриває вікно, що дозволяє легше керувати рюкзаком артефактів",
 	"vcmi.heroWindow.openBackpack.help"  : "Відкриває вікно, що дозволяє легше керувати рюкзаком артефактів",
+	"vcmi.heroWindow.sortBackpackByCost.hover"  : "Сортувати за вартістю",
+	"vcmi.heroWindow.sortBackpackByCost.help"  : "Сортувати артефакти в рюкзаку за вартістю.",
+	"vcmi.heroWindow.sortBackpackBySlot.hover"  : "Сортувати за типом",
+	"vcmi.heroWindow.sortBackpackBySlot.help"  : "Сортувати артефакти в рюкзаку за слотом, в який цей артефакт може бути екіпірований",
+	"vcmi.heroWindow.sortBackpackByClass.hover"  : "Сортування за рідкістю",
+	"vcmi.heroWindow.sortBackpackByClass.help"  : "Сортувати артефакти в рюкзаку за класом рідкісності артефакту. Скарб, Малий, Великий, Реліквія",
+	"vcmi.heroWindow.fusingArtifact.fusing" : "Ви володієте всіма компонентами, необхідними для злиття %s. Ви бажаєте виконати злиття? {Всі компоненти буде спожито під час злиття.}",
 
 
 	"vcmi.tavernWindow.inviteHero"  : "Запросити героя",
 	"vcmi.tavernWindow.inviteHero"  : "Запросити героя",
 
 
@@ -469,6 +606,8 @@
 	"core.seerhut.quest.reachDate.visit.4" : "Закрито до %s.",
 	"core.seerhut.quest.reachDate.visit.4" : "Закрито до %s.",
 	"core.seerhut.quest.reachDate.visit.5" : "Закрито до %s.",
 	"core.seerhut.quest.reachDate.visit.5" : "Закрито до %s.",
 
 
+	"mapObject.core.hillFort.object.description" : "Покращує істот. Рівні 1 - 4 коштують дешевше, ніж в асоційованому місті.",
+
 	"core.bonus.ADDITIONAL_ATTACK.name" : "Подвійний удар",
 	"core.bonus.ADDITIONAL_ATTACK.name" : "Подвійний удар",
 	"core.bonus.ADDITIONAL_ATTACK.description" : "Атакує двічі",
 	"core.bonus.ADDITIONAL_ATTACK.description" : "Атакує двічі",
 	"core.bonus.ADDITIONAL_RETALIATION.name" : "Додаткові відплати",
 	"core.bonus.ADDITIONAL_RETALIATION.name" : "Додаткові відплати",
@@ -610,5 +749,19 @@
 	"core.bonus.WIDE_BREATH.name" : "Широкий подих",
 	"core.bonus.WIDE_BREATH.name" : "Широкий подих",
 	"core.bonus.WIDE_BREATH.description" : "Атака широким подихом",
 	"core.bonus.WIDE_BREATH.description" : "Атака широким подихом",
 	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Обмежена дальність стрільби",
 	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Обмежена дальність стрільби",
-	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Не може стріляти по цілях на відстані більше ${val} гексів"
+	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Не може стріляти по цілях на відстані більше ${val} гексів",
+	"core.bonus.DISINTEGRATE.description" : "Після смерті не залишається трупа",
+	"core.bonus.DISINTEGRATE.name" : "Розпад",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.description" : "При атаці ігнорується ${val}% атаки нападника",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.name" : "Ігнорування атаки (${val}%)",
+	"core.bonus.FEROCITY.description" : "Атакує ${val} більше разів, якщо вбиває когось",
+	"core.bonus.FEROCITY.name" : "Лютість",
+	"core.bonus.INVINCIBLE.description" : "На нього ніщо не може вплинути",
+	"core.bonus.INVINCIBLE.name" : "Невразливий",
+	"core.bonus.MECHANICAL.description" : "Імунітет до багатьох ефектів, можна ремонтувати",
+	"core.bonus.MECHANICAL.name" : "Механічний",
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.description" : "Атака подихом у трьох напрямах",
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.name" : "Призматична атака",
+	"core.bonus.REVENGE.description" : "Завдає додаткової шкоди залежно від втраченого здоров'я в бою",
+	"core.bonus.REVENGE.name" : "Помста"
 }
 }

+ 2 - 2
android/vcmi-app/build.gradle

@@ -26,8 +26,8 @@ android {
 		minSdk = qtMinSdkVersion as Integer
 		minSdk = qtMinSdkVersion as Integer
 		targetSdk = qtTargetSdkVersion as Integer // ANDROID_TARGET_SDK_VERSION in the CMake project
 		targetSdk = qtTargetSdkVersion as Integer // ANDROID_TARGET_SDK_VERSION in the CMake project
 
 
-		versionCode 1600
-		versionName "1.6.0"
+		versionCode 1700
+		versionName "1.7.0"
 
 
 		setProperty("archivesBaseName", "vcmi")
 		setProperty("archivesBaseName", "vcmi")
 	}
 	}

+ 2 - 1
client/NetPacksClient.cpp

@@ -430,9 +430,10 @@ void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack)
 {
 {
 	callAllInterfaces(cl, &IGameEventsReceiver::gameOver, pack.player, pack.victoryLossCheckResult);
 	callAllInterfaces(cl, &IGameEventsReceiver::gameOver, pack.player, pack.victoryLossCheckResult);
 
 
+	bool localHumanWinsGame = vstd::contains(cl.playerint, pack.player) && cl.getPlayerState(pack.player)->human && pack.victoryLossCheckResult.victory();
 	bool lastHumanEndsGame = CSH->howManyPlayerInterfaces() == 1 && vstd::contains(cl.playerint, pack.player) && cl.getPlayerState(pack.player)->human && !settings["session"]["spectate"].Bool();
 	bool lastHumanEndsGame = CSH->howManyPlayerInterfaces() == 1 && vstd::contains(cl.playerint, pack.player) && cl.getPlayerState(pack.player)->human && !settings["session"]["spectate"].Bool();
 
 
-	if(lastHumanEndsGame)
+	if(lastHumanEndsGame || localHumanWinsGame)
 	{
 	{
 		assert(adventureInt);
 		assert(adventureInt);
 		if(adventureInt)
 		if(adventureInt)

+ 2 - 1
client/battle/BattleInterfaceClasses.cpp

@@ -1064,7 +1064,7 @@ StackQueue::StackBox::StackBox(StackQueue * owner):
 		icon = std::make_shared<CAnimImage>(AnimationPath::builtin("TWCRPORT"), 0, 0, 9, 1);
 		icon = std::make_shared<CAnimImage>(AnimationPath::builtin("TWCRPORT"), 0, 0, 9, 1);
 		amount = std::make_shared<CLabel>(pos.w/2, pos.h - 8, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE);
 		amount = std::make_shared<CLabel>(pos.w/2, pos.h - 8, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE);
 		roundRect = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, 15, 18), ColorRGBA(0, 0, 0, 255), ColorRGBA(241, 216, 120, 255));
 		roundRect = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, 15, 18), ColorRGBA(0, 0, 0, 255), ColorRGBA(241, 216, 120, 255));
-		round = std::make_shared<CLabel>(4, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
+		round = std::make_shared<CLabel>(6, 9, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
 
 
 		Point iconPos(pos.w - 16, pos.h - 16);
 		Point iconPos(pos.w - 16, pos.h - 16);
 
 
@@ -1105,6 +1105,7 @@ void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn, std::
 			const auto & font = GH.renderHandler().loadFont(FONT_SMALL);
 			const auto & font = GH.renderHandler().loadFont(FONT_SMALL);
 			int len = font->getStringWidth(tmp);
 			int len = font->getStringWidth(tmp);
 			roundRect->pos.w = len + 6;
 			roundRect->pos.w = len + 6;
+			round->pos = Rect(roundRect->pos.center().x, roundRect->pos.center().y, 0, 0);
 			round->setText(tmp);
 			round->setText(tmp);
 		}
 		}
 
 

+ 13 - 6
client/eventsSDL/InputSourceKeyboard.cpp

@@ -32,15 +32,22 @@ InputSourceKeyboard::InputSourceKeyboard()
 #endif
 #endif
 }
 }
 
 
-std::string InputSourceKeyboard::getKeyNameWithModifiers(const std::string & keyName) const
+std::string InputSourceKeyboard::getKeyNameWithModifiers(const std::string & keyName, bool keyUp)
 {
 {
 	std::string result;
 	std::string result;
 
 
-	if (isKeyboardCtrlDown())
+	if(!keyUp)
+	{
+		wasKeyboardCtrlDown = isKeyboardCtrlDown();
+		wasKeyboardAltDown = isKeyboardAltDown();
+		wasKeyboardShiftDown = isKeyboardShiftDown();
+	}
+
+	if (wasKeyboardCtrlDown)
 		result += "Ctrl+";
 		result += "Ctrl+";
-	if (isKeyboardAltDown())
+	if (wasKeyboardAltDown)
 		result += "Alt+";
 		result += "Alt+";
-	if (isKeyboardShiftDown())
+	if (wasKeyboardShiftDown)
 		result += "Shift+";
 		result += "Shift+";
 	result += keyName;
 	result += keyName;
 
 
@@ -49,7 +56,7 @@ std::string InputSourceKeyboard::getKeyNameWithModifiers(const std::string & key
 
 
 void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key)
 void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key)
 {
 {
-	std::string keyName = getKeyNameWithModifiers(SDL_GetKeyName(key.keysym.sym));
+	std::string keyName = getKeyNameWithModifiers(SDL_GetKeyName(key.keysym.sym), false);
 	logGlobal->trace("keyboard: key '%s' pressed", keyName);
 	logGlobal->trace("keyboard: key '%s' pressed", keyName);
 	assert(key.state == SDL_PRESSED);
 	assert(key.state == SDL_PRESSED);
 
 
@@ -111,7 +118,7 @@ void InputSourceKeyboard::handleEventKeyUp(const SDL_KeyboardEvent & key)
 	if(key.repeat != 0)
 	if(key.repeat != 0)
 		return; // ignore periodic event resends
 		return; // ignore periodic event resends
 
 
-	std::string keyName = getKeyNameWithModifiers(SDL_GetKeyName(key.keysym.sym));
+	std::string keyName = getKeyNameWithModifiers(SDL_GetKeyName(key.keysym.sym), true);
 	logGlobal->trace("keyboard: key '%s' released", keyName);
 	logGlobal->trace("keyboard: key '%s' released", keyName);
 
 
 	if (SDL_IsTextInputActive() == SDL_TRUE)
 	if (SDL_IsTextInputActive() == SDL_TRUE)

+ 5 - 1
client/eventsSDL/InputSourceKeyboard.h

@@ -15,7 +15,11 @@ struct SDL_KeyboardEvent;
 /// Class that handles keyboard input from SDL events
 /// Class that handles keyboard input from SDL events
 class InputSourceKeyboard
 class InputSourceKeyboard
 {
 {
-	std::string getKeyNameWithModifiers(const std::string & keyName) const;
+	bool wasKeyboardCtrlDown;
+	bool wasKeyboardAltDown;
+	bool wasKeyboardShiftDown;
+
+	std::string getKeyNameWithModifiers(const std::string & keyName, bool keyUp);
 public:
 public:
 	InputSourceKeyboard();
 	InputSourceKeyboard();
 
 

+ 12 - 3
client/media/CVideoHandler.cpp

@@ -327,6 +327,11 @@ bool CVideoInstance::videoEnded()
 	return getCurrentFrame() == nullptr;
 	return getCurrentFrame() == nullptr;
 }
 }
 
 
+CVideoInstance::CVideoInstance()
+	: startTimeInitialized(false), deactivationStartTimeHandling(false)
+{
+}
+
 CVideoInstance::~CVideoInstance()
 CVideoInstance::~CVideoInstance()
 {
 {
 	sws_freeContext(sws);
 	sws_freeContext(sws);
@@ -391,8 +396,11 @@ void CVideoInstance::tick(uint32_t msPassed)
 	if(videoEnded())
 	if(videoEnded())
 		throw std::runtime_error("Video already ended!");
 		throw std::runtime_error("Video already ended!");
 
 
-	if(startTime == std::chrono::steady_clock::time_point())
+	if(!startTimeInitialized)
+	{
 		startTime = std::chrono::steady_clock::now();
 		startTime = std::chrono::steady_clock::now();
+		startTimeInitialized = true;
+	}
 
 
 	auto nowTime = std::chrono::steady_clock::now();
 	auto nowTime = std::chrono::steady_clock::now();
 	double difference = std::chrono::duration_cast<std::chrono::milliseconds>(nowTime - startTime).count() / 1000.0;
 	double difference = std::chrono::duration_cast<std::chrono::milliseconds>(nowTime - startTime).count() / 1000.0;
@@ -410,17 +418,18 @@ void CVideoInstance::tick(uint32_t msPassed)
 
 
 void CVideoInstance::activate()
 void CVideoInstance::activate()
 {
 {
-	if(deactivationStartTime != std::chrono::steady_clock::time_point())
+	if(deactivationStartTimeHandling)
 	{
 	{
 		auto pauseDuration = std::chrono::steady_clock::now() - deactivationStartTime;
 		auto pauseDuration = std::chrono::steady_clock::now() - deactivationStartTime;
 		startTime += pauseDuration;
 		startTime += pauseDuration;
-		deactivationStartTime = std::chrono::steady_clock::time_point();
+		deactivationStartTimeHandling = false;
 	}
 	}
 }
 }
 
 
 void CVideoInstance::deactivate()
 void CVideoInstance::deactivate()
 {
 {
 	deactivationStartTime = std::chrono::steady_clock::now();
 	deactivationStartTime = std::chrono::steady_clock::now();
+	deactivationStartTimeHandling = true;
 }
 }
 
 
 struct FFMpegFormatDescription
 struct FFMpegFormatDescription

+ 3 - 0
client/media/CVideoHandler.h

@@ -78,6 +78,8 @@ class CVideoInstance final : public IVideoInstance, public FFMpegStream
 	Point dimensions;
 	Point dimensions;
 
 
 	/// video playback start time point
 	/// video playback start time point
+	bool startTimeInitialized;
+	bool deactivationStartTimeHandling;
 	std::chrono::steady_clock::time_point startTime;
 	std::chrono::steady_clock::time_point startTime;
 	std::chrono::steady_clock::time_point deactivationStartTime;
 	std::chrono::steady_clock::time_point deactivationStartTime;
 
 
@@ -86,6 +88,7 @@ class CVideoInstance final : public IVideoInstance, public FFMpegStream
 	const int MAX_FRAMESKIP = 5;
 	const int MAX_FRAMESKIP = 5;
 
 
 public:
 public:
+	CVideoInstance();
 	~CVideoInstance();
 	~CVideoInstance();
 
 
 	void openVideo();
 	void openVideo();

+ 35 - 27
client/render/CBitmapHandler.cpp

@@ -12,6 +12,7 @@
 
 
 #include "../renderSDL/SDL_Extensions.h"
 #include "../renderSDL/SDL_Extensions.h"
 
 
+#include "../lib/ExceptionsCommon.h"
 #include "../lib/filesystem/Filesystem.h"
 #include "../lib/filesystem/Filesystem.h"
 #include "../lib/vcmi_endian.h"
 #include "../lib/vcmi_endian.h"
 
 
@@ -112,40 +113,47 @@ SDL_Surface * BitmapHandler::loadBitmapFromDir(const ImagePath & path)
 
 
 	SDL_Surface * ret=nullptr;
 	SDL_Surface * ret=nullptr;
 
 
-	auto readFile = CResourceHandler::get()->load(path)->readAll();
+	try {
+		auto readFile = CResourceHandler::get()->load(path)->readAll();
 
 
-	if (isPCX(readFile.first.get()))
-	{//H3-style PCX
-		ret = loadH3PCX(readFile.first.get(), readFile.second);
-		if (!ret)
-		{
-			logGlobal->error("Failed to open %s as H3 PCX!", path.getOriginalName());
-			return nullptr;
-		}
-	}
-	else
-	{ //loading via SDL_Image
-		ret = IMG_Load_RW(
-				  //create SDL_RW with our data (will be deleted by SDL)
-				  SDL_RWFromConstMem((void*)readFile.first.get(), (int)readFile.second),
-				  1); // mark it for auto-deleting
-		if (ret)
-		{
-			if (ret->format->palette)
+		if (isPCX(readFile.first.get()))
+		{//H3-style PCX
+			ret = loadH3PCX(readFile.first.get(), readFile.second);
+			if (!ret)
 			{
 			{
-				// set correct value for alpha\unused channel
-				// NOTE: might be unnecessary with SDL2
-				for (int i=0; i < ret->format->palette->ncolors; i++)
-					ret->format->palette->colors[i].a = SDL_ALPHA_OPAQUE;
+				logGlobal->error("Failed to open %s as H3 PCX!", path.getOriginalName());
+				return nullptr;
 			}
 			}
 		}
 		}
 		else
 		else
-		{
-			logGlobal->error("Failed to open %s via SDL_Image", path.getOriginalName());
-			logGlobal->error("Reason: %s", IMG_GetError());
-			return nullptr;
+		{ //loading via SDL_Image
+			ret = IMG_Load_RW(
+					  //create SDL_RW with our data (will be deleted by SDL)
+					  SDL_RWFromConstMem((void*)readFile.first.get(), (int)readFile.second),
+					  1); // mark it for auto-deleting
+			if (ret)
+			{
+				if (ret->format->palette)
+				{
+					// set correct value for alpha\unused channel
+					// NOTE: might be unnecessary with SDL2
+					for (int i=0; i < ret->format->palette->ncolors; i++)
+						ret->format->palette->colors[i].a = SDL_ALPHA_OPAQUE;
+				}
+			}
+			else
+			{
+				logGlobal->error("Failed to open %s via SDL_Image", path.getOriginalName());
+				logGlobal->error("Reason: %s", IMG_GetError());
+				return nullptr;
+			}
 		}
 		}
 	}
 	}
+	catch (const DataLoadingException & e)
+	{
+		logGlobal->error("%s", e.what());
+		return nullptr;
+	}
 
 
 	// When modifying anything here please check two use cases:
 	// When modifying anything here please check two use cases:
 	// 1) Vampire mansion in Necropolis (not 1st color is transparent)
 	// 1) Vampire mansion in Necropolis (not 1st color is transparent)

+ 5 - 5
client/render/IImage.h

@@ -111,12 +111,12 @@ public:
 	virtual bool isTransparent(const Point & coords) const = 0;
 	virtual bool isTransparent(const Point & coords) const = 0;
 	virtual void draw(SDL_Surface * where, SDL_Palette * palette, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const = 0;
 	virtual void draw(SDL_Surface * where, SDL_Palette * palette, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const = 0;
 
 
-	virtual std::shared_ptr<IImage> createImageReference(EImageBlitMode mode) const = 0;
+	[[nodiscard]] virtual std::shared_ptr<IImage> createImageReference(EImageBlitMode mode) const = 0;
 
 
-	virtual std::shared_ptr<const ISharedImage> horizontalFlip() const = 0;
-	virtual std::shared_ptr<const ISharedImage> verticalFlip() const = 0;
-	virtual std::shared_ptr<const ISharedImage> scaleInteger(int factor, SDL_Palette * palette) const = 0;
-	virtual std::shared_ptr<const ISharedImage> scaleTo(const Point & size, SDL_Palette * palette) const = 0;
+	[[nodiscard]] virtual std::shared_ptr<const ISharedImage> horizontalFlip() const = 0;
+	[[nodiscard]] virtual std::shared_ptr<const ISharedImage> verticalFlip() const = 0;
+	[[nodiscard]] virtual std::shared_ptr<const ISharedImage> scaleInteger(int factor, SDL_Palette * palette, EImageBlitMode blitMode) const = 0;
+	[[nodiscard]] virtual std::shared_ptr<const ISharedImage> scaleTo(const Point & size, SDL_Palette * palette) const = 0;
 
 
 
 
 	virtual ~ISharedImage() = default;
 	virtual ~ISharedImage() = default;

+ 1 - 1
client/renderSDL/CBitmapFont.cpp

@@ -201,7 +201,7 @@ CBitmapFont::CBitmapFont(const std::string & filename):
 		static const std::map<std::string, EScalingAlgorithm> filterNameToEnum = {
 		static const std::map<std::string, EScalingAlgorithm> filterNameToEnum = {
 			{ "nearest", EScalingAlgorithm::NEAREST},
 			{ "nearest", EScalingAlgorithm::NEAREST},
 			{ "bilinear", EScalingAlgorithm::BILINEAR},
 			{ "bilinear", EScalingAlgorithm::BILINEAR},
-			{ "xbrz", EScalingAlgorithm::XBRZ}
+			{ "xbrz", EScalingAlgorithm::XBRZ_ALPHA}
 		};
 		};
 
 
 		auto filterName = settings["video"]["fontUpscalingFilter"].String();
 		auto filterName = settings["video"]["fontUpscalingFilter"].String();

+ 3 - 1
client/renderSDL/ImageScaled.cpp

@@ -43,6 +43,9 @@ void ImageScaled::scaleInteger(int factor)
 
 
 void ImageScaled::scaleTo(const Point & size)
 void ImageScaled::scaleTo(const Point & size)
 {
 {
+	if (source)
+		source = source->scaleTo(size, nullptr);
+
 	if (body)
 	if (body)
 		body = body->scaleTo(size * GH.screenHandler().getScalingFactor(), nullptr);
 		body = body->scaleTo(size * GH.screenHandler().getScalingFactor(), nullptr);
 }
 }
@@ -137,7 +140,6 @@ void ImageScaled::prepareImages()
 
 
 	switch(blitMode)
 	switch(blitMode)
 	{
 	{
-		case EImageBlitMode::SIMPLE:
 		case EImageBlitMode::WITH_SHADOW:
 		case EImageBlitMode::WITH_SHADOW:
 		case EImageBlitMode::ONLY_SHADOW:
 		case EImageBlitMode::ONLY_SHADOW:
 		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
 		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:

+ 18 - 4
client/renderSDL/SDLImage.cpp

@@ -19,6 +19,8 @@
 #include "../render/CDefFile.h"
 #include "../render/CDefFile.h"
 #include "../render/Graphics.h"
 #include "../render/Graphics.h"
 #include "../xBRZ/xbrz.h"
 #include "../xBRZ/xbrz.h"
+#include "../gui/CGuiHandler.h"
+#include "../render/IScreenHandler.h"
 
 
 #include <tbb/parallel_for.h>
 #include <tbb/parallel_for.h>
 #include <SDL_surface.h>
 #include <SDL_surface.h>
@@ -276,9 +278,15 @@ void SDLImageShared::optimizeSurface()
 		margins.x += left;
 		margins.x += left;
 		margins.y += top;
 		margins.y += top;
 	}
 	}
+
+	if(preScaleFactor > 1 && preScaleFactor != GH.screenHandler().getScalingFactor())
+	{
+		margins.x = margins.x * GH.screenHandler().getScalingFactor() / preScaleFactor;
+		margins.y = margins.y * GH.screenHandler().getScalingFactor() / preScaleFactor;
+	}
 }
 }
 
 
-std::shared_ptr<const ISharedImage> SDLImageShared::scaleInteger(int factor, SDL_Palette * palette) const
+std::shared_ptr<const ISharedImage> SDLImageShared::scaleInteger(int factor, SDL_Palette * palette, EImageBlitMode mode) const
 {
 {
 	if (factor <= 0)
 	if (factor <= 0)
 		throw std::runtime_error("Unable to scale by integer value of " + std::to_string(factor));
 		throw std::runtime_error("Unable to scale by integer value of " + std::to_string(factor));
@@ -293,7 +301,13 @@ std::shared_ptr<const ISharedImage> SDLImageShared::scaleInteger(int factor, SDL
 	if(preScaleFactor == factor)
 	if(preScaleFactor == factor)
 		return shared_from_this();
 		return shared_from_this();
 	else if(preScaleFactor == 1)
 	else if(preScaleFactor == 1)
-		scaled = CSDL_Ext::scaleSurfaceIntegerFactor(surf, factor, EScalingAlgorithm::XBRZ);
+	{
+		// dump heuristics to differentiate tileable UI elements from map object / combat assets
+		if (mode == EImageBlitMode::OPAQUE || mode == EImageBlitMode::COLORKEY || mode == EImageBlitMode::SIMPLE)
+			scaled = CSDL_Ext::scaleSurfaceIntegerFactor(surf, factor, EScalingAlgorithm::XBRZ_OPAQUE);
+		else
+			scaled = CSDL_Ext::scaleSurfaceIntegerFactor(surf, factor, EScalingAlgorithm::XBRZ_ALPHA);
+	}
 	else
 	else
 		scaled = CSDL_Ext::scaleSurface(surf, (surf->w / preScaleFactor) * factor, (surf->h / preScaleFactor) * factor);
 		scaled = CSDL_Ext::scaleSurface(surf, (surf->w / preScaleFactor) * factor, (surf->h / preScaleFactor) * factor);
 
 
@@ -589,12 +603,12 @@ void SDLImageRGB::scaleTo(const Point & size)
 
 
 void SDLImageIndexed::scaleInteger(int factor)
 void SDLImageIndexed::scaleInteger(int factor)
 {
 {
-	image = image->scaleInteger(factor, currentPalette);
+	image = image->scaleInteger(factor, currentPalette, blitMode);
 }
 }
 
 
 void SDLImageRGB::scaleInteger(int factor)
 void SDLImageRGB::scaleInteger(int factor)
 {
 {
-	image = image->scaleInteger(factor, nullptr);
+	image = image->scaleInteger(factor, nullptr, blitMode);
 }
 }
 
 
 void SDLImageRGB::exportBitmap(const boost::filesystem::path & path) const
 void SDLImageRGB::exportBitmap(const boost::filesystem::path & path) const

+ 5 - 5
client/renderSDL/SDLImage.h

@@ -57,11 +57,11 @@ public:
 	void exportBitmap(const boost::filesystem::path & path, SDL_Palette * palette) const override;
 	void exportBitmap(const boost::filesystem::path & path, SDL_Palette * palette) const override;
 	Point dimensions() const override;
 	Point dimensions() const override;
 	bool isTransparent(const Point & coords) const override;
 	bool isTransparent(const Point & coords) const override;
-	std::shared_ptr<IImage> createImageReference(EImageBlitMode mode) const override;
-	std::shared_ptr<const ISharedImage> horizontalFlip() const override;
-	std::shared_ptr<const ISharedImage> verticalFlip() const override;
-	std::shared_ptr<const ISharedImage> scaleInteger(int factor, SDL_Palette * palette) const override;
-	std::shared_ptr<const ISharedImage> scaleTo(const Point & size, SDL_Palette * palette) const override;
+	[[nodiscard]] std::shared_ptr<IImage> createImageReference(EImageBlitMode mode) const override;
+	[[nodiscard]] std::shared_ptr<const ISharedImage> horizontalFlip() const override;
+	[[nodiscard]] std::shared_ptr<const ISharedImage> verticalFlip() const override;
+	[[nodiscard]] std::shared_ptr<const ISharedImage> scaleInteger(int factor, SDL_Palette * palette, EImageBlitMode blitMode) const override;
+	[[nodiscard]] std::shared_ptr<const ISharedImage> scaleTo(const Point & size, SDL_Palette * palette) const override;
 
 
 	friend class SDLImageLoader;
 	friend class SDLImageLoader;
 };
 };

+ 8 - 3
client/renderSDL/SDL_Extensions.cpp

@@ -683,12 +683,17 @@ SDL_Surface * CSDL_Ext::scaleSurfaceIntegerFactor(SDL_Surface * surf, int factor
 		case EScalingAlgorithm::BILINEAR:
 		case EScalingAlgorithm::BILINEAR:
 			xbrz::bilinearScale(srcPixels, intermediate->w, intermediate->h, dstPixels, ret->w, ret->h);
 			xbrz::bilinearScale(srcPixels, intermediate->w, intermediate->h, dstPixels, ret->w, ret->h);
 			break;
 			break;
-		case EScalingAlgorithm::XBRZ:
-			tbb::parallel_for(tbb::blocked_range<size_t>(0, intermediate->h, granulation), [factor, srcPixels, dstPixels, intermediate](const tbb::blocked_range<size_t> & r)
+		case EScalingAlgorithm::XBRZ_ALPHA:
+		case EScalingAlgorithm::XBRZ_OPAQUE:
+		{
+			auto format = algorithm == EScalingAlgorithm::XBRZ_OPAQUE ? xbrz::ColorFormat::ARGB_CLAMPED : xbrz::ColorFormat::ARGB;
+			tbb::parallel_for(tbb::blocked_range<size_t>(0, intermediate->h, granulation), [factor, srcPixels, dstPixels, intermediate, format](const tbb::blocked_range<size_t> & r)
 			{
 			{
-				xbrz::scale(factor, srcPixels, dstPixels, intermediate->w, intermediate->h, xbrz::ColorFormat::ARGB, {}, r.begin(), r.end());
+
+				xbrz::scale(factor, srcPixels, dstPixels, intermediate->w, intermediate->h, format, {}, r.begin(), r.end());
 			});
 			});
 			break;
 			break;
+		}
 		default:
 		default:
 			throw std::runtime_error("invalid scaling algorithm!");
 			throw std::runtime_error("invalid scaling algorithm!");
 	}
 	}

+ 2 - 1
client/renderSDL/SDL_Extensions.h

@@ -31,7 +31,8 @@ enum class EScalingAlgorithm : int8_t
 {
 {
 	NEAREST,
 	NEAREST,
 	BILINEAR,
 	BILINEAR,
-	XBRZ
+	XBRZ_OPAQUE, // xbrz, image edges are considered to have same color as pixel inside image
+	XBRZ_ALPHA // xbrz, image edges are considered to be transparent
 };
 };
 
 
 namespace CSDL_Ext
 namespace CSDL_Ext

+ 1 - 1
client/widgets/VideoWidget.cpp

@@ -173,7 +173,7 @@ void VideoWidgetBase::tick(uint32_t msPassed)
 	{
 	{
 		videoInstance->tick(msPassed);
 		videoInstance->tick(msPassed);
 
 
-		if(subTitle)
+		if(!videoInstance->videoEnded() && subTitle)
 			subTitle->setText(getSubTitleLine(videoInstance->timeStamp()));
 			subTitle->setText(getSubTitleLine(videoInstance->timeStamp()));
 
 
 		if(videoInstance->videoEnded())
 		if(videoInstance->videoEnded())

+ 7 - 4
client/windows/CCreatureWindow.cpp

@@ -498,15 +498,18 @@ CStackWindow::CommanderMainSection::CommanderMainSection(CStackWindow * owner, i
 			{
 			{
 				if(index == 0 && skillID >= 100)
 				if(index == 0 && skillID >= 100)
 				{
 				{
-					const auto bonus = CGI->creh->skillRequirements[skillID-100].first;
+					const auto bonuses = CGI->creh->skillRequirements[skillID-100].first;
 					const CStackInstance * stack = parent->info->commander;
 					const CStackInstance * stack = parent->info->commander;
-					auto icon = std::make_shared<CCommanderSkillIcon>(std::make_shared<CPicture>(stack->bonusToGraphics(bonus)), true,  [](){});
+					auto icon = std::make_shared<CCommanderSkillIcon>(std::make_shared<CPicture>(stack->bonusToGraphics(bonuses[0])), true, [](){});
 					icon->callback = [=]()
 					icon->callback = [=]()
 					{
 					{
 						parent->setSelection(skillID, icon);
 						parent->setSelection(skillID, icon);
 					};
 					};
-					icon->text = stack->bonusToString(bonus, true);
-					icon->hoverText = stack->bonusToString(bonus, false);
+					for(int i = 0; i < bonuses.size(); i++) 
+					{
+						icon->text += stack->bonusToString(bonuses[i], true) + "\n";
+						icon->hoverText += stack->bonusToString(bonuses[i], false) + "\n";
+					}
 
 
 					return icon;
 					return icon;
 				}
 				}

+ 0 - 5
client/windows/settings/SettingsMainWindow.cpp

@@ -198,8 +198,3 @@ void SettingsMainWindow::onScreenResize()
 	if (tab)
 	if (tab)
 		tab->updateResolutionSelector();
 		tab->updateResolutionSelector();
 }
 }
-
-void SettingsMainWindow::inputModeChanged(InputMode mode)
-{
-	tabContentArea->reset();
-}

+ 0 - 1
client/windows/settings/SettingsMainWindow.h

@@ -42,6 +42,5 @@ public:
 
 
 	void showAll(Canvas & to) override;
 	void showAll(Canvas & to) override;
 	void onScreenResize() override;
 	void onScreenResize() override;
-	void inputModeChanged(InputMode mode) override;
 };
 };
 
 

+ 17 - 0
client/xBRZ/xbrz.cpp

@@ -1195,6 +1195,22 @@ void xbrz::scale(size_t factor, const uint32_t* src, uint32_t* trg, int srcWidth
             }
             }
             break;
             break;
 
 
+        case ColorFormat::ARGB_CLAMPED:
+            switch (factor)
+            {
+                case 2:
+                    return scaleImage<Scaler2x<ColorGradientARGB>, ColorDistanceARGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast);
+                case 3:
+                    return scaleImage<Scaler3x<ColorGradientARGB>, ColorDistanceARGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast);
+                case 4:
+                    return scaleImage<Scaler4x<ColorGradientARGB>, ColorDistanceARGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast);
+                case 5:
+                    return scaleImage<Scaler5x<ColorGradientARGB>, ColorDistanceARGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast);
+                case 6:
+                    return scaleImage<Scaler6x<ColorGradientARGB>, ColorDistanceARGB, OobReaderDuplicate>(src, trg, srcWidth, srcHeight, cfg, yFirst, yLast);
+            }
+            break;
+
         case ColorFormat::ARGB:
         case ColorFormat::ARGB:
             switch (factor)
             switch (factor)
             {
             {
@@ -1238,6 +1254,7 @@ bool xbrz::equalColorTest(uint32_t col1, uint32_t col2, ColorFormat colFmt, doub
         case ColorFormat::RGB:
         case ColorFormat::RGB:
             return ColorDistanceRGB::dist(col1, col2, luminanceWeight) < equalColorTolerance;
             return ColorDistanceRGB::dist(col1, col2, luminanceWeight) < equalColorTolerance;
         case ColorFormat::ARGB:
         case ColorFormat::ARGB:
+        case ColorFormat::ARGB_CLAMPED:
             return ColorDistanceARGB::dist(col1, col2, luminanceWeight) < equalColorTolerance;
             return ColorDistanceARGB::dist(col1, col2, luminanceWeight) < equalColorTolerance;
         case ColorFormat::ARGB_UNBUFFERED:
         case ColorFormat::ARGB_UNBUFFERED:
             return ColorDistanceUnbufferedARGB::dist(col1, col2, luminanceWeight) < equalColorTolerance;
             return ColorDistanceUnbufferedARGB::dist(col1, col2, luminanceWeight) < equalColorTolerance;

+ 1 - 0
client/xBRZ/xbrz.h

@@ -44,6 +44,7 @@ enum class ColorFormat //from high bits -> low bits, 8 bit per channel
 {
 {
     RGB,  //8 bit for each red, green, blue, upper 8 bits unused
     RGB,  //8 bit for each red, green, blue, upper 8 bits unused
     ARGB, //including alpha channel, BGRA byte order on little-endian machines
     ARGB, //including alpha channel, BGRA byte order on little-endian machines
+    ARGB_CLAMPED, // like ARGB, but edges are treated as opaque, with same color as edge
     ARGB_UNBUFFERED, //like ARGB, but without the one-time buffer creation overhead (ca. 100 - 300 ms) at the expense of a slightly slower scaling time
     ARGB_UNBUFFERED, //like ARGB, but without the one-time buffer creation overhead (ca. 100 - 300 ms) at the expense of a slightly slower scaling time
 };
 };
 
 

+ 1 - 1
cmake_modules/VersionDefinition.cmake

@@ -1,5 +1,5 @@
 set(VCMI_VERSION_MAJOR 1)
 set(VCMI_VERSION_MAJOR 1)
-set(VCMI_VERSION_MINOR 6)
+set(VCMI_VERSION_MINOR 7)
 set(VCMI_VERSION_PATCH 0)
 set(VCMI_VERSION_PATCH 0)
 add_definitions(
 add_definitions(
 	-DVCMI_VERSION_MAJOR=${VCMI_VERSION_MAJOR}
 	-DVCMI_VERSION_MAJOR=${VCMI_VERSION_MAJOR}

+ 1 - 1
config/commanders.json

@@ -25,7 +25,7 @@
 		{"ability": ["ENEMY_DEFENCE_REDUCTION", 50, null, 0 ], "skills": [0, 1]},
 		{"ability": ["ENEMY_DEFENCE_REDUCTION", 50, null, 0 ], "skills": [0, 1]},
 		{"ability": ["FEAR", 0, null, 0 ], "skills": [0, 2]},
 		{"ability": ["FEAR", 0, null, 0 ], "skills": [0, 2]},
 		{"ability": ["ALWAYS_MAXIMUM_DAMAGE", 0, null, 0 ], "skills": [0, 3]},
 		{"ability": ["ALWAYS_MAXIMUM_DAMAGE", 0, null, 0 ], "skills": [0, 3]},
-		{"ability": ["SHOOTER", 0, null, 0 ], "skills": [0, 4]},
+		{"ability": [["SHOOTER", 0, null, 0 ], ["NO_MELEE_PENALTY", 0, null, 0 ]], "skills": [0, 4]},
 		{"ability": ["BLOCKS_RETALIATION", 0, null, 0 ], "skills": [0,5]},
 		{"ability": ["BLOCKS_RETALIATION", 0, null, 0 ], "skills": [0,5]},
 		{"ability": ["UNLIMITED_RETALIATIONS", 0, null, 0 ], "skills": [1, 2]},
 		{"ability": ["UNLIMITED_RETALIATIONS", 0, null, 0 ], "skills": [1, 2]},
 		{"ability": ["ATTACKS_ALL_ADJACENT", 0, null, 0 ], "skills": [1, 3]},
 		{"ability": ["ATTACKS_ALL_ADJACENT", 0, null, 0 ], "skills": [1, 3]},

+ 7 - 1
debian/changelog

@@ -1,8 +1,14 @@
+vcmi (1.7.0) jammy; urgency=medium
+
+  * New upstream release
+
+ -- Ivan Savenko <[email protected]>  Fri, 30 May 2025 12:00:00 +0200
+
 vcmi (1.6.0) jammy; urgency=medium
 vcmi (1.6.0) jammy; urgency=medium
 
 
   * New upstream release
   * New upstream release
 
 
- -- Ivan Savenko <[email protected]>  Fri, 30 Aug 2024 12:00:00 +0200
+ -- Ivan Savenko <[email protected]>  Fri, 20 Dec 2024 12:00:00 +0200
 
 
 vcmi (1.5.7) jammy; urgency=medium
 vcmi (1.5.7) jammy; urgency=medium
 
 

+ 3 - 5
docs/Readme.md

@@ -1,9 +1,7 @@
 # VCMI Project
 # VCMI Project
 
 
 [![VCMI](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg?branch=develop&event=push)](https://github.com/vcmi/vcmi/actions/workflows/github.yml?query=branch%3Adevelop+event%3Apush)
 [![VCMI](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg?branch=develop&event=push)](https://github.com/vcmi/vcmi/actions/workflows/github.yml?query=branch%3Adevelop+event%3Apush)
-[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.0)
-[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.6/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.6)
-[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.7/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.7)
+[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.6.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.6.0)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases)
 
 
 VCMI is an open-source recreation of Heroes of Might & Magic III engine, giving it new and extended possibilities.
 VCMI is an open-source recreation of Heroes of Might & Magic III engine, giving it new and extended possibilities.
@@ -41,8 +39,8 @@ See also installation guide for [Heroes Chronicles](players/Heroes_Chronicles.md
 
 
 <p>
 <p>
   <img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.4.0/Antagarich%20Burning%20Battle.jpg?raw=true" alt="Forge Town in battle" style="height:120px;"/>
   <img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.4.0/Antagarich%20Burning%20Battle.jpg?raw=true" alt="Forge Town in battle" style="height:120px;"/>
-  <img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.4.0/Town%20and%20Unit.jpg?raw=true" alt="Asylum town with new creature dialog" style="height:120px;"/>
-  <img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.4.0/Siege.jpg?raw=true" alt="Ruins town siege" style="height:120px;"/>
+  <img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.6.0/New%20bonus%20descriptions.png?raw=true" alt="New creature dialog with ability descriptions" style="height:120px;"/>
+  <img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.6.0/Preserve%20siege.jpg?raw=true" alt="Preserve town siege" style="height:120px;"/>
   <img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.4.0/Editor.jpg?raw=true" alt="Map editor" style="height:120px;"/>
   <img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.4.0/Editor.jpg?raw=true" alt="Map editor" style="height:120px;"/>
 </p>
 </p>
 
 

+ 3 - 0
launcher/CMakeLists.txt

@@ -15,6 +15,7 @@ set(launcher_SRCS
 		modManager/imageviewer_moc.cpp
 		modManager/imageviewer_moc.cpp
 		modManager/chroniclesextractor.cpp
 		modManager/chroniclesextractor.cpp
 		settingsView/csettingsview_moc.cpp
 		settingsView/csettingsview_moc.cpp
+		startGame/StartGameTab.cpp
 		firstLaunch/firstlaunch_moc.cpp
 		firstLaunch/firstlaunch_moc.cpp
 		main.cpp
 		main.cpp
 		helper.cpp
 		helper.cpp
@@ -46,6 +47,7 @@ set(launcher_HEADERS
 		modManager/imageviewer_moc.h
 		modManager/imageviewer_moc.h
 		modManager/chroniclesextractor.h
 		modManager/chroniclesextractor.h
 		settingsView/csettingsview_moc.h
 		settingsView/csettingsview_moc.h
+		startGame/StartGameTab.h
 		firstLaunch/firstlaunch_moc.h
 		firstLaunch/firstlaunch_moc.h
 		mainwindow_moc.h
 		mainwindow_moc.h
 		languages.h
 		languages.h
@@ -63,6 +65,7 @@ set(launcher_FORMS
 		settingsView/csettingsview_moc.ui
 		settingsView/csettingsview_moc.ui
 		firstLaunch/firstlaunch_moc.ui
 		firstLaunch/firstlaunch_moc.ui
 		mainwindow_moc.ui
 		mainwindow_moc.ui
+		startGame/StartGameTab.ui
 		updatedialog_moc.ui
 		updatedialog_moc.ui
 )
 )
 
 

+ 2 - 1
launcher/eu.vcmi.VCMI.metainfo.xml

@@ -90,7 +90,8 @@
 	</screenshots>
 	</screenshots>
 	<launchable type="desktop-id">vcmilauncher.desktop</launchable>
 	<launchable type="desktop-id">vcmilauncher.desktop</launchable>
 	<releases>
 	<releases>
-		<release version="1.6.0" date="2024-08-30" type="development"/>
+		<release version="1.7.0" date="2025-05-30" type="development"/>
+		<release version="1.6.0" date="2024-12-20" type="stable"/>
 		<release version="1.5.7" date="2024-08-26" type="stable"/>
 		<release version="1.5.7" date="2024-08-26" type="stable"/>
 		<release version="1.5.6" date="2024-08-04" type="stable"/>
 		<release version="1.5.6" date="2024-08-04" type="stable"/>
 		<release version="1.5.5" date="2024-07-17" type="stable"/>
 		<release version="1.5.5" date="2024-07-17" type="stable"/>

+ 25 - 16
launcher/firstLaunch/firstlaunch_moc.cpp

@@ -295,13 +295,21 @@ bool FirstLaunchView::heroesDataDetect()
 QString FirstLaunchView::getHeroesInstallDir()
 QString FirstLaunchView::getHeroesInstallDir()
 {
 {
 #ifdef VCMI_WINDOWS
 #ifdef VCMI_WINDOWS
-	QString gogPath = QSettings("HKEY_LOCAL_MACHINE\\SOFTWARE\\GOG.com\\Games\\1207658787", QSettings::NativeFormat).value("path").toString();
-	if(!gogPath.isEmpty())
-		return gogPath;
+	QVector<QPair<QString, QString>> regKeys = {
+		{ "HKEY_LOCAL_MACHINE\\SOFTWARE\\GOG.com\\Games\\1207658787",                                            "path"    }, // Gog on x86 system
+		{ "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\GOG.com\\Games\\1207658787",                               "path"    }, // Gog on x64 system
+		{ "HKEY_LOCAL_MACHINE\\SOFTWARE\\New World Computing\\Heroes of Might and Magic® III\\1.0",              "AppPath" }, // H3 Complete on x86 system
+		{ "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\New World Computing\\Heroes of Might and Magic® III\\1.0", "AppPath" }, // H3 Complete on x64 system
+		{ "HKEY_LOCAL_MACHINE\\SOFTWARE\\New World Computing\\Heroes of Might and Magic III\\1.0",               "AppPath" }, // some localized H3 on x86 system
+		{ "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\New World Computing\\Heroes of Might and Magic III\\1.0",  "AppPath" }, // some localized H3 on x64 system
+	};
 
 
-	QString cdPath = QSettings("HKEY_LOCAL_MACHINE\\SOFTWARE\\New World Computing\\Heroes of Might and Magic® III\\1.0", QSettings::NativeFormat).value("AppPath").toString();
-	if(!cdPath.isEmpty())
-		return cdPath;
+	for(auto & regKey : regKeys)
+	{
+		QString path = QSettings(regKey.first, QSettings::NativeFormat).value(regKey.second).toString();
+		if(!path.isEmpty())
+			return path;
+	}
 #endif
 #endif
 	return QString{};
 	return QString{};
 }
 }
@@ -363,6 +371,9 @@ void FirstLaunchView::extractGogData()
 		QFile(fileExe).copy(tmpFileExe);
 		QFile(fileExe).copy(tmpFileExe);
 		QFile(fileBin).copy(tmpFileBin);
 		QFile(fileBin).copy(tmpFileBin);
 
 
+		logGlobal->info("Installing exe '%s' ('%s')", tmpFileExe.toStdString(), fileExe.toStdString());
+		logGlobal->info("Installing bin '%s' ('%s')", tmpFileBin.toStdString(), fileBin.toStdString());
+
 		QString errorText{};
 		QString errorText{};
 
 
 		auto isGogGalaxyExe = [](QString fileToTest) {
 		auto isGogGalaxyExe = [](QString fileToTest) {
@@ -381,7 +392,7 @@ void FirstLaunchView::extractGogData()
 		};
 		};
 
 
 		if(isGogGalaxyExe(tmpFileExe))
 		if(isGogGalaxyExe(tmpFileExe))
-			errorText = tr("You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer!");
+			errorText = tr("You've provided a GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer!");
 
 
 		if(errorText.isEmpty())
 		if(errorText.isEmpty())
 			errorText = Innoextract::extract(tmpFileExe, tempDir.path(), [this](float progress) {
 			errorText = Innoextract::extract(tmpFileExe, tempDir.path(), [this](float progress) {
@@ -451,7 +462,7 @@ void FirstLaunchView::copyHeroesData(const QString & path, bool move)
 	QStringList dirMaps = sourceRoot.entryList({"maps"}, QDir::Filter::Dirs);
 	QStringList dirMaps = sourceRoot.entryList({"maps"}, QDir::Filter::Dirs);
 	QStringList dirMp3 = sourceRoot.entryList({"mp3"}, QDir::Filter::Dirs);
 	QStringList dirMp3 = sourceRoot.entryList({"mp3"}, QDir::Filter::Dirs);
 
 
-	const auto noDataMessage = tr("Failed to detect valid Heroes III data in chosen directory.\nPlease select directory with installed Heroes III data.");
+	const auto noDataMessage = tr("Failed to detect valid Heroes III data in chosen directory.\nPlease select the directory with installed Heroes III data.");
 	if(dirData.empty())
 	if(dirData.empty())
 	{
 	{
 		QMessageBox::critical(this, tr("Heroes III data not found!"), noDataMessage);
 		QMessageBox::critical(this, tr("Heroes III data not found!"), noDataMessage);
@@ -475,12 +486,12 @@ void FirstLaunchView::copyHeroesData(const QString & path, bool move)
 		if (!hdFiles.empty())
 		if (!hdFiles.empty())
 		{
 		{
 			// HD Edition contains only RoE data so we can't use even unmodified files from it
 			// HD Edition contains only RoE data so we can't use even unmodified files from it
-			QMessageBox::critical(this, tr("Heroes III data not found!"), tr("Heroes III: HD Edition files are not supported by VCMI.\nPlease select directory with Heroes III: Complete Edition or Heroes III: Shadow of Death."));
+			QMessageBox::critical(this, tr("Heroes III data not found!"), tr("Heroes III: HD Edition files are not supported by VCMI.\nPlease select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death."));
 			return;
 			return;
 		}
 		}
 
 
 		// RoE or some other unsupported edition. Demo version?
 		// RoE or some other unsupported edition. Demo version?
-		QMessageBox::critical(this, tr("Heroes III data not found!"), tr("Unknown or unsupported Heroes III version found.\nPlease select directory with Heroes III: Complete Edition or Heroes III: Shadow of Death."));
+		QMessageBox::critical(this, tr("Heroes III data not found!"), tr("Unknown or unsupported Heroes III version found.\nPlease select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death."));
 		return;
 		return;
 	}
 	}
 
 
@@ -541,15 +552,13 @@ void FirstLaunchView::modPresetUpdate()
 
 
 QString FirstLaunchView::findTranslationModName()
 QString FirstLaunchView::findTranslationModName()
 {
 {
-	if (!getModView())
-		return QString();
-
-	QString preferredlanguage = QString::fromStdString(settings["general"]["language"].String());
-	QString installedlanguage = QString::fromStdString(settings["session"]["language"].String());
+	auto * mainWindow = dynamic_cast<MainWindow *>(QApplication::activeWindow());
+	auto status = mainWindow->getTranslationStatus();
 
 
-	if (preferredlanguage == installedlanguage)
+	if (status == ETranslationStatus::ACTIVE || status == ETranslationStatus::NOT_AVAILABLE)
 		return QString();
 		return QString();
 
 
+	QString preferredlanguage = QString::fromStdString(settings["general"]["language"].String());
 	return getModView()->getTranslationModName(preferredlanguage);
 	return getModView()->getTranslationModName(preferredlanguage);
 }
 }
 
 

+ 0 - 2
launcher/firstLaunch/firstlaunch_moc.h

@@ -55,8 +55,6 @@ class FirstLaunchView : public QWidget
 	bool checkCanInstallExtras();
 	bool checkCanInstallExtras();
 	bool checkCanInstallMod(const QString & modID);
 	bool checkCanInstallMod(const QString & modID);
 
 
-	void installMod(const QString & modID);
-
 public:
 public:
 	explicit FirstLaunchView(QWidget * parent = nullptr);
 	explicit FirstLaunchView(QWidget * parent = nullptr);
 
 

+ 12 - 10
launcher/firstLaunch/firstlaunch_moc.ui

@@ -96,7 +96,7 @@
    <item>
    <item>
     <widget class="QStackedWidget" name="installerTabs">
     <widget class="QStackedWidget" name="installerTabs">
      <property name="currentIndex">
      <property name="currentIndex">
-      <number>1</number>
+      <number>2</number>
      </property>
      </property>
      <widget class="QWidget" name="pageLanguageSelect">
      <widget class="QWidget" name="pageLanguageSelect">
       <layout class="QGridLayout" name="gridLayout_3">
       <layout class="QGridLayout" name="gridLayout_3">
@@ -177,9 +177,9 @@
          <property name="text">
          <property name="text">
           <string>Thank you for installing VCMI!
           <string>Thank you for installing VCMI!
 
 
-Before you can start playing, there are a few more steps that need to be completed.
+Before you can start playing, there are a few more steps to complete.
 
 
-Please keep in mind that in order to use VCMI you must own the original data files for Heroes® of Might and Magic® III: Complete or The Shadow of Death.
+Please remember that to use VCMI, you must own the original data files for Heroes® of Might and Magic® III: Complete or The Shadow of Death.
 
 
 Heroes® of Might and Magic® III HD is currently not supported!</string>
 Heroes® of Might and Magic® III HD is currently not supported!</string>
          </property>
          </property>
@@ -307,7 +307,7 @@ Heroes® of Might and Magic® III HD is currently not supported!</string>
             </sizepolicy>
             </sizepolicy>
            </property>
            </property>
            <property name="text">
            <property name="text">
-            <string>You can manually copy directories Maps, Data and Mp3 from the original game directory to VCMI data directory that you can see on top of this page</string>
+            <string>You can manually copy directories Maps, Data, and Mp3 from the original game directory to the VCMI data directory that you can see on top of this page</string>
            </property>
            </property>
            <property name="alignment">
            <property name="alignment">
             <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
             <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
@@ -501,8 +501,8 @@ Heroes® of Might and Magic® III HD is currently not supported!</string>
             </sizepolicy>
             </sizepolicy>
            </property>
            </property>
            <property name="text">
            <property name="text">
-            <string>If you own Heroes III on gog.com you can download backup offline installer from gog.com, and VCMI will import Heroes III data using offline installer. 
-Offline installer consists of two parts, .exe and .bin. Make sure you download both of them.</string>
+            <string>If you own Heroes III on gog.com, you can download a backup offline installer from gog.com. VCMI will then import Heroes III data using the offline installer. 
+Offline installer consists of two files: &quot;.exe&quot; and &quot;.bin&quot; - you must download both.</string>
            </property>
            </property>
            <property name="textFormat">
            <property name="textFormat">
             <enum>Qt::PlainText</enum>
             <enum>Qt::PlainText</enum>
@@ -785,7 +785,7 @@ Offline installer consists of two parts, .exe and .bin. Make sure you download b
             </sizepolicy>
             </sizepolicy>
            </property>
            </property>
            <property name="text">
            <property name="text">
-            <string>Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles</string>
+            <string>Install mod that provides various interface improvements, such as a better interface for random maps and selectable actions in battles</string>
            </property>
            </property>
            <property name="wordWrap">
            <property name="wordWrap">
             <bool>true</bool>
             <bool>true</bool>
@@ -809,9 +809,9 @@ Offline installer consists of two parts, .exe and .bin. Make sure you download b
             <string>In The Wake of Gods</string>
             <string>In The Wake of Gods</string>
            </property>
            </property>
            <property name="icon">
            <property name="icon">
-            <iconset>
+            <iconset resource="../resources.qrc">
              <normaloff>:/icons/mod-disabled.png</normaloff>
              <normaloff>:/icons/mod-disabled.png</normaloff>
-             <normalon>:icons/mod-enabled.png</normalon>:icons/mod-disabled.png</iconset>
+             <normalon>:icons/mod-enabled.png</normalon>:/icons/mod-disabled.png</iconset>
            </property>
            </property>
            <property name="checkable">
            <property name="checkable">
             <bool>true</bool>
             <bool>true</bool>
@@ -876,6 +876,8 @@ Offline installer consists of two parts, .exe and .bin. Make sure you download b
    </item>
    </item>
   </layout>
   </layout>
  </widget>
  </widget>
- <resources/>
+ <resources>
+  <include location="../resources.qrc"/>
+ </resources>
  <connections/>
  <connections/>
 </ui>
 </ui>

+ 2 - 0
launcher/innoextract.cpp

@@ -83,6 +83,8 @@ QString Innoextract::getHashError(QString exeFile, QString binFile, QString exeF
 	};
 	};
 	
 	
 	std::vector<data> knownHashes = {
 	std::vector<data> knownHashes = {
+	//	{ H3_COMPLETE, "english", 973162040,          0, "7cf1ecec73e8c2f2c2619415cd16749be5641942", ""                                         }, // setup_homm_3_complete_4.0_(10665).exe
+	//	{ H3_COMPLETE, "french",        ???,          0, "7e5a737c51530a1888033d188ab0635825ee622f", ""                                         }, // setup_homm_3_complete_french_4.0_(10665).exe
 		{ H3_COMPLETE, "english",    822520, 1005040617, "66646a353b06417fa12c6384405688c84a315cc1", "c624e2071f4e35386765ab044ad5860ac245b7f4" }, // setup_heroes_of_might_and_magic_3_complete_4.0_(28740).exe
 		{ H3_COMPLETE, "english",    822520, 1005040617, "66646a353b06417fa12c6384405688c84a315cc1", "c624e2071f4e35386765ab044ad5860ac245b7f4" }, // setup_heroes_of_might_and_magic_3_complete_4.0_(28740).exe
 		{ H3_COMPLETE, "french",     824960,  997305870, "072f1d4466ff16444d8c7949c6530448a9c53cfa", "9b6b451d2bd2f8b4be159e62fa6d32e87ee10455" }, // setup_heroes_of_might_and_magic_3_complete_4.0_(french)_(28740).exe
 		{ H3_COMPLETE, "french",     824960,  997305870, "072f1d4466ff16444d8c7949c6530448a9c53cfa", "9b6b451d2bd2f8b4be159e62fa6d32e87ee10455" }, // setup_heroes_of_might_and_magic_3_complete_4.0_(french)_(28740).exe
 		{ H3_COMPLETE, "polish",     822288,  849286313, "74ffde00156dd5a8e237668f87213387f0dd9c7c", "2523cf9943043ae100186f89e4ebf7c28be09804" }, // setup_heroes_of_might_and_magic_3_complete_4.0_(polish)_(28740).exe
 		{ H3_COMPLETE, "polish",     822288,  849286313, "74ffde00156dd5a8e237668f87213387f0dd9c7c", "2523cf9943043ae100186f89e4ebf7c28be09804" }, // setup_heroes_of_might_and_magic_3_complete_4.0_(polish)_(28740).exe

+ 105 - 17
launcher/mainwindow_moc.cpp

@@ -47,7 +47,6 @@ void MainWindow::computeSidePanelSizes()
 		ui->modslistButton,
 		ui->modslistButton,
 		ui->settingsButton,
 		ui->settingsButton,
 		ui->aboutButton,
 		ui->aboutButton,
-		ui->startEditorButton,
 		ui->startGameButton
 		ui->startGameButton
 	};
 	};
 
 
@@ -78,11 +77,12 @@ MainWindow::MainWindow(QWidget * parent)
 
 
 	ui->setupUi(this);
 	ui->setupUi(this);
 
 
+	setAcceptDrops(true);
+
 	setWindowIcon(QIcon{":/icons/menu-game.png"});
 	setWindowIcon(QIcon{":/icons/menu-game.png"});
 	ui->modslistButton->setIcon(QIcon{":/icons/menu-mods.png"});
 	ui->modslistButton->setIcon(QIcon{":/icons/menu-mods.png"});
 	ui->settingsButton->setIcon(QIcon{":/icons/menu-settings.png"});
 	ui->settingsButton->setIcon(QIcon{":/icons/menu-settings.png"});
 	ui->aboutButton->setIcon(QIcon{":/icons/about-project.png"});
 	ui->aboutButton->setIcon(QIcon{":/icons/about-project.png"});
-	ui->startEditorButton->setIcon(QIcon{":/icons/menu-editor.png"});
 	ui->startGameButton->setIcon(QIcon{":/icons/menu-game.png"});
 	ui->startGameButton->setIcon(QIcon{":/icons/menu-game.png"});
 
 
 #ifndef VCMI_MOBILE
 #ifndef VCMI_MOBILE
@@ -101,16 +101,12 @@ MainWindow::MainWindow(QWidget * parent)
 	}
 	}
 #endif
 #endif
 
 
-#ifndef ENABLE_EDITOR
-	ui->startEditorButton->hide();
-#endif
-
 	computeSidePanelSizes();
 	computeSidePanelSizes();
 
 
 	bool h3DataFound = CResourceHandler::get()->existsResource(ResourcePath("DATA/GENRLTXT.TXT"));
 	bool h3DataFound = CResourceHandler::get()->existsResource(ResourcePath("DATA/GENRLTXT.TXT"));
 
 
 	if (h3DataFound && setupCompleted)
 	if (h3DataFound && setupCompleted)
-		ui->tabListWidget->setCurrentIndex(TabRows::MODS);
+		ui->tabListWidget->setCurrentIndex(TabRows::START);
 	else
 	else
 		enterSetup();
 		enterSetup();
 
 
@@ -147,7 +143,6 @@ void MainWindow::detectPreferredLanguage()
 void MainWindow::enterSetup()
 void MainWindow::enterSetup()
 {
 {
 	ui->startGameButton->setEnabled(false);
 	ui->startGameButton->setEnabled(false);
-	ui->startEditorButton->setEnabled(false);
 	ui->settingsButton->setEnabled(false);
 	ui->settingsButton->setEnabled(false);
 	ui->aboutButton->setEnabled(false);
 	ui->aboutButton->setEnabled(false);
 	ui->modslistButton->setEnabled(false);
 	ui->modslistButton->setEnabled(false);
@@ -160,16 +155,27 @@ void MainWindow::exitSetup()
 	writer->Bool() = true;
 	writer->Bool() = true;
 
 
 	ui->startGameButton->setEnabled(true);
 	ui->startGameButton->setEnabled(true);
-	ui->startEditorButton->setEnabled(true);
 	ui->settingsButton->setEnabled(true);
 	ui->settingsButton->setEnabled(true);
 	ui->aboutButton->setEnabled(true);
 	ui->aboutButton->setEnabled(true);
 	ui->modslistButton->setEnabled(true);
 	ui->modslistButton->setEnabled(true);
 	ui->tabListWidget->setCurrentIndex(TabRows::MODS);
 	ui->tabListWidget->setCurrentIndex(TabRows::MODS);
 }
 }
 
 
+void MainWindow::switchToStartTab()
+{
+	ui->startGameButton->setEnabled(true);
+	ui->startGameButton->setChecked(true);
+	ui->tabListWidget->setCurrentIndex(TabRows::START);
+
+	auto* startGameTabWidget = qobject_cast<StartGameTab*>(ui->tabListWidget->widget(TabRows::START));
+	if(startGameTabWidget)
+		startGameTabWidget->refreshState();
+}
+
 void MainWindow::switchToModsTab()
 void MainWindow::switchToModsTab()
 {
 {
 	ui->startGameButton->setEnabled(true);
 	ui->startGameButton->setEnabled(true);
+	ui->modslistButton->setChecked(true);
 	ui->tabListWidget->setCurrentIndex(TabRows::MODS);
 	ui->tabListWidget->setCurrentIndex(TabRows::MODS);
 }
 }
 
 
@@ -196,14 +202,7 @@ MainWindow::~MainWindow()
 
 
 void MainWindow::on_startGameButton_clicked()
 void MainWindow::on_startGameButton_clicked()
 {
 {
-	hide();
-	startGame({});
-}
-
-void MainWindow::on_startEditorButton_clicked()
-{
-	hide();
-	startEditor({});
+	switchToStartTab();
 }
 }
 
 
 CModListView * MainWindow::getModView()
 CModListView * MainWindow::getModView()
@@ -228,6 +227,95 @@ void MainWindow::on_aboutButton_clicked()
 	ui->tabListWidget->setCurrentIndex(TabRows::ABOUT);
 	ui->tabListWidget->setCurrentIndex(TabRows::ABOUT);
 }
 }
 
 
+void MainWindow::dragEnterEvent(QDragEnterEvent* event)
+{
+	if(event->mimeData()->hasUrls())
+		for(const auto & url : event->mimeData()->urls())
+			for(const auto & ending : QStringList({".zip", ".h3m", ".h3c", ".vmap", ".vcmp", ".json", ".exe"}))
+				if(url.fileName().endsWith(ending, Qt::CaseInsensitive))
+				{
+					event->acceptProposedAction();
+					return;
+				}
+}
+
+void MainWindow::dropEvent(QDropEvent* event)
+{
+	const QMimeData* mimeData = event->mimeData();
+
+	if(mimeData->hasUrls())
+	{
+		const QList<QUrl> urlList = mimeData->urls();
+		for (const auto & url : urlList)
+			manualInstallFile(url.toLocalFile());
+	}
+}
+
+void MainWindow::manualInstallFile(QString filePath)
+{
+	if(filePath.endsWith(".zip", Qt::CaseInsensitive) || filePath.endsWith(".exe", Qt::CaseInsensitive))
+		switchToModsTab();
+
+	QString fileName = QFileInfo{filePath}.fileName();
+	if(filePath.endsWith(".zip", Qt::CaseInsensitive))
+	{
+		QString filenameClean = fileName.toLower()
+			// mod name currently comes from zip file -> remove suffixes from github zip download
+			.replace(QRegularExpression("-[0-9a-f]{40}"), "")
+			.replace(QRegularExpression("-vcmi-.+\\.zip"), ".zip")
+			.replace("-main.zip", ".zip");
+
+		getModView()->downloadFile(filenameClean, QUrl::fromLocalFile(filePath), "mods");
+	}
+	else if(filePath.endsWith(".json", Qt::CaseInsensitive))
+	{
+		QDir configDir(QString::fromStdString(VCMIDirs::get().userConfigPath().string()));
+		QStringList configFile = configDir.entryList({fileName}, QDir::Filter::Files); // case insensitive check
+		if(!configFile.empty())
+		{
+			auto dialogResult = QMessageBox::warning(this, tr("Replace config file?"), tr("Do you want to replace %1?").arg(configFile[0]), QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
+			if(dialogResult == QMessageBox::Yes)
+			{
+				const auto configFilePath = configDir.filePath(configFile[0]);
+				QFile::remove(configFilePath);
+				QFile::copy(filePath, configFilePath);
+
+				// reload settings
+				Helper::loadSettings();
+				for(const auto widget : qApp->allWidgets())
+					if(auto settingsView = qobject_cast<CSettingsView *>(widget))
+						settingsView->loadSettings();
+
+				getModView()->reload();
+			}
+		}
+	}
+	else
+		getModView()->installFiles(QStringList{filePath});
+}
+
+ETranslationStatus MainWindow::getTranslationStatus()
+{
+	QString preferredlanguage = QString::fromStdString(settings["general"]["language"].String());
+	QString installedlanguage = QString::fromStdString(settings["session"]["language"].String());
+
+	if (preferredlanguage == installedlanguage)
+		return ETranslationStatus::ACTIVE;
+
+	QString modName = getModView()->getTranslationModName(preferredlanguage);
+
+	if (modName.isEmpty())
+		return ETranslationStatus::NOT_AVAILABLE;
+
+	if (!getModView()->isModInstalled(modName))
+		return ETranslationStatus::NOT_INSTALLLED;
+
+	if (!getModView()->isModEnabled(modName))
+		return ETranslationStatus::DISABLED;
+
+	return ETranslationStatus::ACTIVE;
+}
+
 void MainWindow::updateTranslation()
 void MainWindow::updateTranslation()
 {
 {
 #ifdef ENABLE_QT_TRANSLATIONS
 #ifdef ENABLE_QT_TRANSLATIONS

+ 16 - 1
launcher/mainwindow_moc.h

@@ -23,6 +23,14 @@ class QTableWidgetItem;
 class CModList;
 class CModList;
 class CModListView;
 class CModListView;
 
 
+enum class ETranslationStatus : int8_t
+{
+	NOT_AVAILABLE, // translation for this language was not found in mod list. Could also happen if player is offline or disabled repository checkout
+	NOT_INSTALLLED, // translation mod found, but it is not installed
+	DISABLED, // translation mod found, and installed, but toggled off
+	ACTIVE // translation mod active OR game is already in specified language (e.g. English H3 for players with English language)
+};
+
 class MainWindow : public QMainWindow
 class MainWindow : public QMainWindow
 {
 {
 	Q_OBJECT
 	Q_OBJECT
@@ -40,6 +48,7 @@ class MainWindow : public QMainWindow
 		SETTINGS = 1,
 		SETTINGS = 1,
 		SETUP = 2,
 		SETUP = 2,
 		ABOUT = 3,
 		ABOUT = 3,
+		START = 4,
 	};
 	};
 
 
 public:
 public:
@@ -55,6 +64,13 @@ public:
 	void enterSetup();
 	void enterSetup();
 	void exitSetup();
 	void exitSetup();
 	void switchToModsTab();
 	void switchToModsTab();
+	void switchToStartTab();
+
+	void dragEnterEvent(QDragEnterEvent* event) override;
+	void dropEvent(QDropEvent *event) override;
+
+	void manualInstallFile(QString filePath);
+	ETranslationStatus getTranslationStatus();
 
 
 protected:
 protected:
 	void changeEvent(QEvent * event) override;
 	void changeEvent(QEvent * event) override;
@@ -65,6 +81,5 @@ public slots:
 private slots:
 private slots:
 	void on_modslistButton_clicked();
 	void on_modslistButton_clicked();
 	void on_settingsButton_clicked();
 	void on_settingsButton_clicked();
-	void on_startEditorButton_clicked();
 	void on_aboutButton_clicked();
 	void on_aboutButton_clicked();
 };
 };

+ 37 - 77
launcher/mainwindow_moc.ui

@@ -30,7 +30,7 @@
     <item>
     <item>
      <layout class="QVBoxLayout" name="verticalLayout">
      <layout class="QVBoxLayout" name="verticalLayout">
       <item>
       <item>
-       <widget class="QToolButton" name="modslistButton">
+       <widget class="QToolButton" name="startGameButton">
         <property name="sizePolicy">
         <property name="sizePolicy">
          <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
          <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
           <horstretch>1</horstretch>
           <horstretch>1</horstretch>
@@ -49,8 +49,13 @@
           <height>16777215</height>
           <height>16777215</height>
          </size>
          </size>
         </property>
         </property>
+        <property name="font">
+         <font>
+          <bold>true</bold>
+         </font>
+        </property>
         <property name="text">
         <property name="text">
-         <string>Mods</string>
+         <string>Game</string>
         </property>
         </property>
         <property name="iconSize">
         <property name="iconSize">
          <size>
          <size>
@@ -76,7 +81,7 @@
        </widget>
        </widget>
       </item>
       </item>
       <item>
       <item>
-       <widget class="QToolButton" name="settingsButton">
+       <widget class="QToolButton" name="modslistButton">
         <property name="sizePolicy">
         <property name="sizePolicy">
          <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
          <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
           <horstretch>1</horstretch>
           <horstretch>1</horstretch>
@@ -96,7 +101,7 @@
          </size>
          </size>
         </property>
         </property>
         <property name="text">
         <property name="text">
-         <string>Settings</string>
+         <string>Mods</string>
         </property>
         </property>
         <property name="iconSize">
         <property name="iconSize">
          <size>
          <size>
@@ -122,7 +127,7 @@
        </widget>
        </widget>
       </item>
       </item>
       <item>
       <item>
-       <widget class="QToolButton" name="aboutButton">
+       <widget class="QToolButton" name="settingsButton">
         <property name="sizePolicy">
         <property name="sizePolicy">
          <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
          <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
           <horstretch>1</horstretch>
           <horstretch>1</horstretch>
@@ -142,12 +147,12 @@
          </size>
          </size>
         </property>
         </property>
         <property name="text">
         <property name="text">
-         <string>Help</string>
+         <string>Settings</string>
         </property>
         </property>
         <property name="iconSize">
         <property name="iconSize">
          <size>
          <size>
-          <width>32</width>
-          <height>32</height>
+          <width>64</width>
+          <height>64</height>
          </size>
          </size>
         </property>
         </property>
         <property name="checkable">
         <property name="checkable">
@@ -168,24 +173,11 @@
        </widget>
        </widget>
       </item>
       </item>
       <item>
       <item>
-       <spacer name="verticalSpacer">
-        <property name="orientation">
-         <enum>Qt::Vertical</enum>
-        </property>
-        <property name="sizeHint" stdset="0">
-         <size>
-          <width>100</width>
-          <height>0</height>
-         </size>
-        </property>
-       </spacer>
-      </item>
-      <item>
-       <widget class="QToolButton" name="startEditorButton">
+       <widget class="QToolButton" name="aboutButton">
         <property name="sizePolicy">
         <property name="sizePolicy">
          <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
          <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
           <horstretch>1</horstretch>
           <horstretch>1</horstretch>
-          <verstretch>5</verstretch>
+          <verstretch>10</verstretch>
          </sizepolicy>
          </sizepolicy>
         </property>
         </property>
         <property name="minimumSize">
         <property name="minimumSize">
@@ -200,83 +192,44 @@
           <height>16777215</height>
           <height>16777215</height>
          </size>
          </size>
         </property>
         </property>
-        <property name="font">
-         <font>
-          <weight>75</weight>
-          <bold>true</bold>
-         </font>
-        </property>
         <property name="text">
         <property name="text">
-         <string>Map Editor</string>
+         <string>Help</string>
         </property>
         </property>
         <property name="iconSize">
         <property name="iconSize">
          <size>
          <size>
-          <width>32</width>
-          <height>32</height>
+          <width>48</width>
+          <height>48</height>
          </size>
          </size>
         </property>
         </property>
         <property name="checkable">
         <property name="checkable">
-         <bool>false</bool>
+         <bool>true</bool>
         </property>
         </property>
         <property name="checked">
         <property name="checked">
          <bool>false</bool>
          <bool>false</bool>
         </property>
         </property>
+        <property name="autoExclusive">
+         <bool>true</bool>
+        </property>
         <property name="toolButtonStyle">
         <property name="toolButtonStyle">
          <enum>Qt::ToolButtonTextUnderIcon</enum>
          <enum>Qt::ToolButtonTextUnderIcon</enum>
         </property>
         </property>
         <property name="autoRaise">
         <property name="autoRaise">
-         <bool>false</bool>
+         <bool>true</bool>
         </property>
         </property>
        </widget>
        </widget>
       </item>
       </item>
       <item>
       <item>
-       <widget class="QToolButton" name="startGameButton">
-        <property name="sizePolicy">
-         <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
-          <horstretch>1</horstretch>
-          <verstretch>10</verstretch>
-         </sizepolicy>
+       <spacer name="verticalSpacer">
+        <property name="orientation">
+         <enum>Qt::Vertical</enum>
         </property>
         </property>
-        <property name="minimumSize">
+        <property name="sizeHint" stdset="0">
          <size>
          <size>
-          <width>0</width>
+          <width>100</width>
           <height>0</height>
           <height>0</height>
          </size>
          </size>
         </property>
         </property>
-        <property name="maximumSize">
-         <size>
-          <width>16777215</width>
-          <height>16777215</height>
-         </size>
-        </property>
-        <property name="font">
-         <font>
-          <weight>75</weight>
-          <bold>true</bold>
-         </font>
-        </property>
-        <property name="text">
-         <string>Start game</string>
-        </property>
-        <property name="iconSize">
-         <size>
-          <width>64</width>
-          <height>64</height>
-         </size>
-        </property>
-        <property name="checkable">
-         <bool>false</bool>
-        </property>
-        <property name="checked">
-         <bool>false</bool>
-        </property>
-        <property name="toolButtonStyle">
-         <enum>Qt::ToolButtonTextUnderIcon</enum>
-        </property>
-        <property name="autoRaise">
-         <bool>false</bool>
-        </property>
-       </widget>
+       </spacer>
       </item>
       </item>
      </layout>
      </layout>
     </item>
     </item>
@@ -292,12 +245,13 @@
        </sizepolicy>
        </sizepolicy>
       </property>
       </property>
       <property name="currentIndex">
       <property name="currentIndex">
-       <number>3</number>
+       <number>4</number>
       </property>
       </property>
       <widget class="CModListView" name="modlistView"/>
       <widget class="CModListView" name="modlistView"/>
       <widget class="CSettingsView" name="settingsView"/>
       <widget class="CSettingsView" name="settingsView"/>
       <widget class="FirstLaunchView" name="setupView"/>
       <widget class="FirstLaunchView" name="setupView"/>
       <widget class="AboutProjectView" name="aboutView"/>
       <widget class="AboutProjectView" name="aboutView"/>
+      <widget class="StartGameTab" name="startGameView"/>
      </widget>
      </widget>
     </item>
     </item>
    </layout>
    </layout>
@@ -329,6 +283,12 @@
    <header>aboutProject/aboutproject_moc.h</header>
    <header>aboutProject/aboutproject_moc.h</header>
    <container>1</container>
    <container>1</container>
   </customwidget>
   </customwidget>
+  <customwidget>
+   <class>StartGameTab</class>
+   <extends>QWidget</extends>
+   <header>startGame/StartGameTab.h</header>
+   <container>1</container>
+  </customwidget>
  </customwidgets>
  </customwidgets>
  <resources/>
  <resources/>
  <connections/>
  <connections/>

+ 49 - 45
launcher/modManager/chroniclesextractor.cpp

@@ -41,37 +41,25 @@ void ChroniclesExtractor::removeTempDir()
 	tempDir.removeRecursively();
 	tempDir.removeRecursively();
 }
 }
 
 
-int ChroniclesExtractor::getChronicleNo(QFile & file)
+int ChroniclesExtractor::getChronicleNo()
 {
 {
-	if(!file.open(QIODevice::ReadOnly))
-	{
-		QMessageBox::critical(parent, tr("File cannot opened"), file.errorString());
-		return 0;
-	}
+	QStringList appDirCandidates = tempDir.entryList({"app"}, QDir::Filter::Dirs);
 
 
-	QByteArray magic{"MZ"};
-	QByteArray magicFile = file.read(magic.length());
-	if(!magicFile.startsWith(magic))
+	if (!appDirCandidates.empty())
 	{
 	{
-		QMessageBox::critical(parent, tr("Invalid file selected"), tr("You have to select an gog installer file!"));
-		return 0;
-	}
+		QDir appDir = tempDir.filePath(appDirCandidates.front());
 
 
-	QByteArray dataBegin = file.read(1'000'000);
-	int chronicle = 0;
-	for (const auto& kv : chronicles) {
-		if(dataBegin.contains(kv.second))
+		for (size_t i = 1; i < chronicles.size(); ++i)
 		{
 		{
-			chronicle = kv.first;
-			break;
+			QString chronicleName = chronicles.at(i);
+			QStringList chroniclesDirCandidates = appDir.entryList({chronicleName}, QDir::Filter::Dirs);
+
+			if (!chroniclesDirCandidates.empty())
+				return i;
 		}
 		}
 	}
 	}
-	if(!chronicle)
-	{
-		QMessageBox::critical(parent, tr("Invalid file selected"), tr("You have to select an chronicle installer file!"));
-		return 0;
-	}
-	return chronicle;
+	QMessageBox::critical(parent, tr("Invalid file selected"), tr("You have to select a Heroes Chronicles installer file!"));
+	return 0;
 }
 }
 
 
 bool ChroniclesExtractor::extractGogInstaller(QString file)
 bool ChroniclesExtractor::extractGogInstaller(QString file)
@@ -129,16 +117,14 @@ void ChroniclesExtractor::createBaseMod() const
 
 
 	for(auto & dataPath : VCMIDirs::get().dataPaths())
 	for(auto & dataPath : VCMIDirs::get().dataPaths())
 	{
 	{
-		auto file = dataPath / "config" / "heroes" / "portraitsChronicles.json";
+		auto file = pathToQString(dataPath / "config" / "heroes" / "portraitsChronicles.json");
 		auto destFolder = VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "content" / "config";
 		auto destFolder = VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "content" / "config";
-		if(boost::filesystem::exists(file))
+		auto destFile = pathToQString(destFolder / "portraitsChronicles.json");
+		if(QFile::exists(file))
 		{
 		{
-			boost::filesystem::create_directories(destFolder);
-#if BOOST_VERSION >= 107400
-			boost::filesystem::copy_file(file, destFolder / "portraitsChronicles.json", boost::filesystem::copy_options::overwrite_existing);
-#else
-			boost::filesystem::copy_file(file, destFolder / "portraitsChronicles.json", boost::filesystem::copy_option::overwrite_if_exists);
-#endif
+			QDir().mkpath(pathToQString(destFolder));
+			QFile::remove(destFile);
+			QFile::copy(file, destFile);
 		}
 		}
 	}
 	}
 }
 }
@@ -149,14 +135,13 @@ void ChroniclesExtractor::createChronicleMod(int no)
 	dir.removeRecursively();
 	dir.removeRecursively();
 	dir.mkpath(".");
 	dir.mkpath(".");
 
 
-	QByteArray tmpChronicles = chronicles.at(no);
-	tmpChronicles.replace('\0', "");
+	QString tmpChronicles = chronicles.at(no);
 
 
 	QJsonObject mod
 	QJsonObject mod
 	{
 	{
 		{ "modType", "Expansion" },
 		{ "modType", "Expansion" },
-		{ "name", QString::number(no) + " - " + QString(tmpChronicles) },
-		{ "description", tr("Heroes Chronicles") + " - " + QString::number(no) + " - " + QString(tmpChronicles) },
+		{ "name", QString("%1 - %2").arg(no).arg(tmpChronicles) },
+		{ "description", tr("Heroes Chronicles %1 - %2").arg(no).arg(tmpChronicles) },
 		{ "author", "3DO" },
 		{ "author", "3DO" },
 		{ "version", "1.0" },
 		{ "version", "1.0" },
 		{ "contact", "vcmi.eu" },
 		{ "contact", "vcmi.eu" },
@@ -173,8 +158,7 @@ void ChroniclesExtractor::createChronicleMod(int no)
 
 
 void ChroniclesExtractor::extractFiles(int no) const
 void ChroniclesExtractor::extractFiles(int no) const
 {
 {
-	QByteArray tmpChronicles = chronicles.at(no);
-	tmpChronicles.replace('\0', "");
+	QString tmpChronicles = chronicles.at(no);
 
 
 	std::string chroniclesDir = "chronicles_" + std::to_string(no);
 	std::string chroniclesDir = "chronicles_" + std::to_string(no);
 	QDir tmpDir = tempDir.filePath(tempDir.entryList({"app"}, QDir::Filter::Dirs).front());
 	QDir tmpDir = tempDir.filePath(tempDir.entryList({"app"}, QDir::Filter::Dirs).front());
@@ -230,26 +214,46 @@ void ChroniclesExtractor::extractFiles(int no) const
 
 
 void ChroniclesExtractor::installChronicles(QStringList exe)
 void ChroniclesExtractor::installChronicles(QStringList exe)
 {
 {
+	logGlobal->info("Installing Chronicles");
+
 	extractionFile = -1;
 	extractionFile = -1;
 	fileCount = exe.size();
 	fileCount = exe.size();
 	for(QString f : exe)
 	for(QString f : exe)
 	{
 	{
 		extractionFile++;
 		extractionFile++;
-		QFile file(f);
-
-		int chronicleNo = getChronicleNo(file);
-		if(!chronicleNo)
-			continue;
 
 
+		logGlobal->info("Creating temporary directory");
 		if(!createTempDir())
 		if(!createTempDir())
 			continue;
 			continue;
+		
+		logGlobal->info("Copying offline installer");
+		// FIXME: this is required at the moment for Android (and possibly iOS)
+		// Incoming file names are in content URI form, e.g. content://media/internal/chronicles.exe
+		// Qt can handle those like it does regular files
+		// however, innoextract fails to open such files
+		// so make a copy in directory to which vcmi always has full access and operate on it
+		QString filepath = tempDir.filePath("chr.exe");
+		QFile(f).copy(filepath);
+		QFile file(filepath);
+
+		logGlobal->info("Extracting offline installer");
+		if(!extractGogInstaller(filepath))
+			continue;
 
 
-		if(!extractGogInstaller(f))
+		logGlobal->info("Detecting Chronicle");
+		int chronicleNo = getChronicleNo();
+		if(!chronicleNo)
 			continue;
 			continue;
-		
+
+		logGlobal->info("Creating base Chronicle mod");
 		createBaseMod();
 		createBaseMod();
+
+		logGlobal->info("Creating Chronicle mod");
 		createChronicleMod(chronicleNo);
 		createChronicleMod(chronicleNo);
 
 
+		logGlobal->info("Removing temporary directory");
 		removeTempDir();
 		removeTempDir();
 	}
 	}
+
+	logGlobal->info("Chronicles installed");
 }
 }

+ 11 - 10
launcher/modManager/chroniclesextractor.h

@@ -24,21 +24,22 @@ class ChroniclesExtractor : public QObject
 
 
 	bool createTempDir();
 	bool createTempDir();
 	void removeTempDir();
 	void removeTempDir();
-	int getChronicleNo(QFile & file);
+	int getChronicleNo();
 	bool extractGogInstaller(QString filePath);
 	bool extractGogInstaller(QString filePath);
 	void createBaseMod() const;
 	void createBaseMod() const;
 	void createChronicleMod(int no);
 	void createChronicleMod(int no);
 	void extractFiles(int no) const;
 	void extractFiles(int no) const;
 
 
-	const std::map<int, QByteArray> chronicles = {
-		{1, QByteArray{reinterpret_cast<const char*>(u"Warlords of the Wasteland"), 50}},
-		{2, QByteArray{reinterpret_cast<const char*>(u"Conquest of the Underworld"), 52}},
-		{3, QByteArray{reinterpret_cast<const char*>(u"Masters of the Elements"), 46}},
-		{4, QByteArray{reinterpret_cast<const char*>(u"Clash of the Dragons"), 40}},
-		{5, QByteArray{reinterpret_cast<const char*>(u"The World Tree"), 28}},
-		{6, QByteArray{reinterpret_cast<const char*>(u"The Fiery Moon"), 28}},
-		{7, QByteArray{reinterpret_cast<const char*>(u"Revolt of the Beastmasters"), 52}},
-		{8, QByteArray{reinterpret_cast<const char*>(u"The Sword of Frost"), 36}}
+	const QStringList chronicles = {
+		{}, // fake	0th "chronicle", to create 1-based list
+		"Warlords of the Wasteland",
+		"Conquest of the Underworld",
+		"Masters of the Elements",
+		"Clash of the Dragons",
+		"The World Tree",
+		"The Fiery Moon",
+		"Revolt of the Beastmasters",
+		"The Sword of Frost",
 	};
 	};
 public:
 public:
 	void installChronicles(QStringList exe);
 	void installChronicles(QStringList exe);

+ 153 - 129
launcher/modManager/cmodlistview_moc.cpp

@@ -27,12 +27,13 @@
 #include "../vcmiqt/jsonutils.h"
 #include "../vcmiqt/jsonutils.h"
 #include "../helper.h"
 #include "../helper.h"
 
 
-#include "../../lib/VCMIDirs.h"
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CConfigHandler.h"
-#include "../../lib/texts/Languages.h"
-#include "../../lib/modding/CModVersion.h"
+#include "../../lib/VCMIDirs.h"
 #include "../../lib/filesystem/Filesystem.h"
 #include "../../lib/filesystem/Filesystem.h"
+#include "../../lib/json/JsonUtils.h"
+#include "../../lib/modding/CModVersion.h"
 #include "../../lib/texts/CGeneralTextHandler.h"
 #include "../../lib/texts/CGeneralTextHandler.h"
+#include "../../lib/texts/Languages.h"
 
 
 #include <future>
 #include <future>
 
 
@@ -43,7 +44,7 @@ void CModListView::setupModModel()
 
 
 	modStateModel = std::make_shared<ModStateModel>();
 	modStateModel = std::make_shared<ModStateModel>();
 	if (!cachedRepositoryData.isNull())
 	if (!cachedRepositoryData.isNull())
-		modStateModel->appendRepositories(cachedRepositoryData);
+		modStateModel->setRepositoryData(cachedRepositoryData);
 
 
 	modModel = new ModStateItemModel(modStateModel, this);
 	modModel = new ModStateItemModel(modStateModel, this);
 	manager = std::make_unique<ModStateController>(modStateModel);
 	manager = std::make_unique<ModStateController>(modStateModel);
@@ -54,35 +55,11 @@ void CModListView::changeEvent(QEvent *event)
 	if(event->type() == QEvent::LanguageChange)
 	if(event->type() == QEvent::LanguageChange)
 	{
 	{
 		ui->retranslateUi(this);
 		ui->retranslateUi(this);
-		modModel->reloadRepositories();
+		modModel->reloadViewModel();
 	}
 	}
 	QWidget::changeEvent(event);
 	QWidget::changeEvent(event);
 }
 }
 
 
-void CModListView::dragEnterEvent(QDragEnterEvent* event)
-{
-	if(event->mimeData()->hasUrls())
-		for(const auto & url : event->mimeData()->urls())
-			for(const auto & ending : QStringList({".zip", ".h3m", ".h3c", ".vmap", ".vcmp", ".json", ".exe"}))
-				if(url.fileName().endsWith(ending, Qt::CaseInsensitive))
-				{
-					event->acceptProposedAction();
-					return;
-				}
-}
-
-void CModListView::dropEvent(QDropEvent* event)
-{
-	const QMimeData* mimeData = event->mimeData();
-
-	if(mimeData->hasUrls())
-	{
-		const QList<QUrl> urlList = mimeData->urls();
-		for (const auto & url : urlList)
-			manualInstallFile(url.toLocalFile());
-	}
-}
-
 void CModListView::setupFilterModel()
 void CModListView::setupFilterModel()
 {
 {
 	filterModel = new CModFilterModel(modModel, this);
 	filterModel = new CModFilterModel(modModel, this);
@@ -134,8 +111,6 @@ CModListView::CModListView(QWidget * parent)
 {
 {
 	ui->setupUi(this);
 	ui->setupUi(this);
 
 
-	setAcceptDrops(true);
-
 	ui->uninstallButton->setIcon(QIcon{":/icons/mod-delete.png"});
 	ui->uninstallButton->setIcon(QIcon{":/icons/mod-delete.png"});
 	ui->enableButton->setIcon(QIcon{":/icons/mod-enabled.png"});
 	ui->enableButton->setIcon(QIcon{":/icons/mod-enabled.png"});
 	ui->disableButton->setIcon(QIcon{":/icons/mod-disabled.png"});
 	ui->disableButton->setIcon(QIcon{":/icons/mod-disabled.png"});
@@ -152,7 +127,7 @@ CModListView::CModListView(QWidget * parent)
 	ui->progressWidget->setVisible(false);
 	ui->progressWidget->setVisible(false);
 	dlManager = nullptr;
 	dlManager = nullptr;
 
 
-	modModel->reloadRepositories();
+	modModel->reloadViewModel();
 	if(settings["launcher"]["autoCheckRepositories"].Bool())
 	if(settings["launcher"]["autoCheckRepositories"].Bool())
 		loadRepositories();
 		loadRepositories();
 
 
@@ -169,8 +144,16 @@ CModListView::CModListView(QWidget * parent)
 #endif
 #endif
 }
 }
 
 
+void CModListView::reload()
+{
+	modStateModel->reloadLocalState();
+	modModel->reloadViewModel();
+}
+
 void CModListView::loadRepositories()
 void CModListView::loadRepositories()
 {
 {
+	accumulatedRepositoryData.clear();
+
 	QStringList repositories;
 	QStringList repositories;
 
 
 	if (settings["launcher"]["defaultRepositoryEnabled"].Bool())
 	if (settings["launcher"]["defaultRepositoryEnabled"].Bool())
@@ -461,9 +444,10 @@ void CModListView::selectMod(const QModelIndex & index)
 		Helper::enableScrollBySwiping(ui->modInfoBrowser);
 		Helper::enableScrollBySwiping(ui->modInfoBrowser);
 		Helper::enableScrollBySwiping(ui->changelogBrowser);
 		Helper::enableScrollBySwiping(ui->changelogBrowser);
 
 
-		QStringList notInstalledDependencies = this->getModsToInstall(modName);
-		QStringList unavailableDependencies = this->findUnavailableMods(notInstalledDependencies);
+		QStringList notInstalledDependencies = getModsToInstall(modName);
+		QStringList unavailableDependencies = findUnavailableMods(notInstalledDependencies);
 		bool translationMismatch = 	mod.isTranslation() && CGeneralTextHandler::getPreferredLanguage() != mod.getBaseLanguage().toStdString();
 		bool translationMismatch = 	mod.isTranslation() && CGeneralTextHandler::getPreferredLanguage() != mod.getBaseLanguage().toStdString();
+		bool modIsBeingDownloaded = enqueuedModDownloads.contains(mod.getID());
 
 
 		ui->disableButton->setVisible(modStateModel->isModInstalled(mod.getID()) && modStateModel->isModEnabled(mod.getID()));
 		ui->disableButton->setVisible(modStateModel->isModInstalled(mod.getID()) && modStateModel->isModEnabled(mod.getID()));
 		ui->enableButton->setVisible(modStateModel->isModInstalled(mod.getID()) && !modStateModel->isModEnabled(mod.getID()));
 		ui->enableButton->setVisible(modStateModel->isModInstalled(mod.getID()) && !modStateModel->isModEnabled(mod.getID()));
@@ -474,9 +458,9 @@ void CModListView::selectMod(const QModelIndex & index)
 		// Block buttons if action is not allowed at this time
 		// Block buttons if action is not allowed at this time
 		ui->disableButton->setEnabled(true);
 		ui->disableButton->setEnabled(true);
 		ui->enableButton->setEnabled(notInstalledDependencies.empty() && !translationMismatch);
 		ui->enableButton->setEnabled(notInstalledDependencies.empty() && !translationMismatch);
-		ui->installButton->setEnabled(unavailableDependencies.empty());
+		ui->installButton->setEnabled(unavailableDependencies.empty() && !modIsBeingDownloaded);
 		ui->uninstallButton->setEnabled(true);
 		ui->uninstallButton->setEnabled(true);
-		ui->updateButton->setEnabled(unavailableDependencies.empty());
+		ui->updateButton->setEnabled(unavailableDependencies.empty() && !modIsBeingDownloaded);
 
 
 		loadScreenshots();
 		loadScreenshots();
 	}
 	}
@@ -564,9 +548,6 @@ QStringList CModListView::getModsToInstall(QString mod)
 		candidates.pop_back();
 		candidates.pop_back();
 		processed.push_back(potentialToInstall);
 		processed.push_back(potentialToInstall);
 
 
-		if (modStateModel->isModExists(potentialToInstall) && modStateModel->isModInstalled(potentialToInstall))
-			continue;
-
 		if (modStateModel->isSubmod(potentialToInstall))
 		if (modStateModel->isSubmod(potentialToInstall))
 		{
 		{
 			QString topParent = modStateModel->getTopParent(potentialToInstall);
 			QString topParent = modStateModel->getTopParent(potentialToInstall);
@@ -580,7 +561,8 @@ QStringList CModListView::getModsToInstall(QString mod)
 				potentialToInstall = modStateModel->getTopParent(potentialToInstall);
 				potentialToInstall = modStateModel->getTopParent(potentialToInstall);
 		}
 		}
 
 
-		result.push_back(potentialToInstall);
+		if (modStateModel->isModExists(potentialToInstall) && !modStateModel->isModInstalled(potentialToInstall))
+			result.push_back(potentialToInstall);
 
 
 		if (modStateModel->isModExists(potentialToInstall))
 		if (modStateModel->isModExists(potentialToInstall))
 		{
 		{
@@ -599,17 +581,24 @@ QStringList CModListView::getModsToInstall(QString mod)
 void CModListView::on_updateButton_clicked()
 void CModListView::on_updateButton_clicked()
 {
 {
 	QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString();
 	QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString();
+	doUpdateMod(modName);
+
+	ui->updateButton->setEnabled(false);
+}
+
+void CModListView::doUpdateMod(const QString & modName)
+{
 	auto targetMod = modStateModel->getMod(modName);
 	auto targetMod = modStateModel->getMod(modName);
 
 
 	if(targetMod.isUpdateAvailable())
 	if(targetMod.isUpdateAvailable())
-		downloadFile(modName + ".zip", targetMod.getDownloadUrl(), modName, targetMod.getDownloadSizeBytes());
+		downloadMod(targetMod);
 
 
 	for(const auto & name : getModsToInstall(modName))
 	for(const auto & name : getModsToInstall(modName))
 	{
 	{
 		auto mod = modStateModel->getMod(name);
 		auto mod = modStateModel->getMod(name);
 		// update required mod, install missing (can be new dependency)
 		// update required mod, install missing (can be new dependency)
 		if(mod.isUpdateAvailable() || !mod.isInstalled())
 		if(mod.isUpdateAvailable() || !mod.isInstalled())
-			downloadFile(name + ".zip", mod.getDownloadUrl(), name, mod.getDownloadSizeBytes());
+			downloadMod(mod);
 	}
 	}
 }
 }
 
 
@@ -622,7 +611,7 @@ void CModListView::on_uninstallButton_clicked()
 		if(modStateModel->isModEnabled(modName))
 		if(modStateModel->isModEnabled(modName))
 			manager->disableMod(modName);
 			manager->disableMod(modName);
 		manager->uninstallMod(modName);
 		manager->uninstallMod(modName);
-		modModel->reloadRepositories();
+		reload();
 	}
 	}
 	
 	
 	checkManagerErrors();
 	checkManagerErrors();
@@ -632,81 +621,18 @@ void CModListView::on_installButton_clicked()
 {
 {
 	QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString();
 	QString modName = ui->allModsView->currentIndex().data(ModRoles::ModNameRole).toString();
 
 
-	for(const auto & name : getModsToInstall(modName))
-	{
-		auto mod = modStateModel->getMod(name);
-		if(mod.isAvailable())
-			downloadFile(name + ".zip", mod.getDownloadUrl(), name, mod.getDownloadSizeBytes());
-		else if(!modStateModel->isModEnabled(name))
-			enableModByName(name);
-	}
-}
+	doInstallMod(modName);
 
 
-void CModListView::on_installFromFileButton_clicked()
-{
-	// iOS can't display modal dialogs when called directly on button press
-	// https://bugreports.qt.io/browse/QTBUG-98651
-	QTimer::singleShot(0, this, [this]
-	{
-		QString filter = tr("All supported files") + " (*.h3m *.vmap *.h3c *.vcmp *.zip *.json *.exe);;" + 
-			tr("Maps") + " (*.h3m *.vmap);;" + 
-			tr("Campaigns") + " (*.h3c *.vcmp);;" + 
-			tr("Configs") + " (*.json);;" + 
-			tr("Mods") + " (*.zip);;" + 
-			tr("Gog files") + " (*.exe)";
-#if defined(VCMI_MOBILE)
-		filter = tr("All files (*.*)"); //Workaround for sometimes incorrect mime for some extensions (e.g. for exe)
-#endif
-		QStringList files = QFileDialog::getOpenFileNames(this, tr("Select files (configs, mods, maps, campaigns, gog files) to install..."), QDir::homePath(), filter);
-
-		for(const auto & file : files)
-		{
-			manualInstallFile(file);
-		}
-	});
+	ui->installButton->setEnabled(false);
 }
 }
 
 
-void CModListView::manualInstallFile(QString filePath)
+void CModListView::downloadMod(const ModState & mod)
 {
 {
-	QString fileName = QFileInfo{filePath}.fileName();
-	if(filePath.endsWith(".zip", Qt::CaseInsensitive))
-		downloadFile(fileName.toLower()
-			// mod name currently comes from zip file -> remove suffixes from github zip download
-			.replace(QRegularExpression("-[0-9a-f]{40}"), "")
-			.replace(QRegularExpression("-vcmi-.+\\.zip"), ".zip")
-			.replace("-main.zip", ".zip")
-			, QUrl::fromLocalFile(filePath), "mods");
-	else if(filePath.endsWith(".json", Qt::CaseInsensitive))
-	{
-		QDir configDir(QString::fromStdString(VCMIDirs::get().userConfigPath().string()));
-		QStringList configFile = configDir.entryList({fileName}, QDir::Filter::Files); // case insensitive check
-		if(!configFile.empty())
-		{
-			auto dialogResult = QMessageBox::warning(this, tr("Replace config file?"), tr("Do you want to replace %1?").arg(configFile[0]), QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
-			if(dialogResult == QMessageBox::Yes)
-			{
-				const auto configFilePath = configDir.filePath(configFile[0]);
-				QFile::remove(configFilePath);
-				QFile::copy(filePath, configFilePath);
-
-				// reload settings
-				Helper::loadSettings();
-				for(const auto widget : qApp->allWidgets())
-					if(auto settingsView = qobject_cast<CSettingsView *>(widget))
-						settingsView->loadSettings();
-
-				modStateModel->reloadLocalState();
-				modModel->reloadRepositories();
-			}
-		}
-	}
-	else
-		downloadFile(fileName, QUrl::fromLocalFile(filePath), fileName);
-}
+	if (enqueuedModDownloads.contains(mod.getID()))
+		return;
 
 
-void CModListView::downloadFile(QString file, QString url, QString description, qint64 sizeBytes)
-{
-	downloadFile(file, QUrl{url}, description, sizeBytes);
+	enqueuedModDownloads.push_back(mod.getID());
+	downloadFile(mod.getID() + ".zip", mod.getDownloadUrl(), mod.getName(), mod.getDownloadSizeBytes());
 }
 }
 
 
 void CModListView::downloadFile(QString file, QUrl url, QString description, qint64 sizeBytes)
 void CModListView::downloadFile(QString file, QUrl url, QString description, qint64 sizeBytes)
@@ -779,6 +705,7 @@ void CModListView::downloadFinished(QStringList savedFiles, QStringList failedFi
 		doInstallFiles = true;
 		doInstallFiles = true;
 	}
 	}
 
 
+	enqueuedModDownloads.clear();
 	dlManager->deleteLater();
 	dlManager->deleteLater();
 	dlManager = nullptr;
 	dlManager = nullptr;
 	
 	
@@ -807,7 +734,7 @@ void CModListView::installFiles(QStringList files)
 	QStringList maps;
 	QStringList maps;
 	QStringList images;
 	QStringList images;
 	QStringList exe;
 	QStringList exe;
-	JsonNode repository;
+	bool repositoryFilesEnqueued = false;
 
 
 	// TODO: some better way to separate zip's with mods and downloaded repository files
 	// TODO: some better way to separate zip's with mods and downloaded repository files
 	for(QString filename : files)
 	for(QString filename : files)
@@ -821,7 +748,7 @@ void CModListView::installFiles(QStringList files)
 		else if(filename.endsWith(".json", Qt::CaseInsensitive))
 		else if(filename.endsWith(".json", Qt::CaseInsensitive))
 		{
 		{
 			//download and merge additional files
 			//download and merge additional files
-			const auto &repoData = JsonUtils::jsonFromFile(filename);
+			JsonNode repoData = JsonUtils::jsonFromFile(filename);
 			if(repoData["name"].isNull())
 			if(repoData["name"].isNull())
 			{
 			{
 				// This is main repository index. Download all referenced mods
 				// This is main repository index. Download all referenced mods
@@ -830,9 +757,12 @@ void CModListView::installFiles(QStringList files)
 					auto modNameLower = boost::algorithm::to_lower_copy(modName);
 					auto modNameLower = boost::algorithm::to_lower_copy(modName);
 					auto modJsonUrl = modJson["mod"];
 					auto modJsonUrl = modJson["mod"];
 					if(!modJsonUrl.isNull())
 					if(!modJsonUrl.isNull())
+					{
 						downloadFile(QString::fromStdString(modName + ".json"), QString::fromStdString(modJsonUrl.String()), tr("mods repository index"));
 						downloadFile(QString::fromStdString(modName + ".json"), QString::fromStdString(modJsonUrl.String()), tr("mods repository index"));
+						repositoryFilesEnqueued = true;
+					}
 
 
-					repository[modNameLower] = modJson;
+					accumulatedRepositoryData[modNameLower] = modJson;
 				}
 				}
 			}
 			}
 			else
 			else
@@ -840,35 +770,46 @@ void CModListView::installFiles(QStringList files)
 				// This is json of a single mod. Extract name of mod and add it to repo
 				// This is json of a single mod. Extract name of mod and add it to repo
 				auto modName = QFileInfo(filename).baseName().toStdString();
 				auto modName = QFileInfo(filename).baseName().toStdString();
 				auto modNameLower = boost::algorithm::to_lower_copy(modName);
 				auto modNameLower = boost::algorithm::to_lower_copy(modName);
-				repository[modNameLower] = repoData;
+				JsonUtils::merge(accumulatedRepositoryData[modNameLower], repoData);
 			}
 			}
 		}
 		}
 		else if(filename.endsWith(".png", Qt::CaseInsensitive))
 		else if(filename.endsWith(".png", Qt::CaseInsensitive))
 			images.push_back(filename);
 			images.push_back(filename);
 	}
 	}
 
 
-	if (!repository.isNull())
+	if (!accumulatedRepositoryData.isNull() && !repositoryFilesEnqueued)
 	{
 	{
-		manager->appendRepositories(repository);
-		modModel->reloadRepositories();
+		logGlobal->info("Installing repository: started");
+		manager->setRepositoryData(accumulatedRepositoryData);
+		modModel->reloadViewModel();
+		accumulatedRepositoryData.clear();
 
 
 		static const QString repositoryCachePath = CLauncherDirs::downloadsPath() + "/repositoryCache.json";
 		static const QString repositoryCachePath = CLauncherDirs::downloadsPath() + "/repositoryCache.json";
 		JsonUtils::jsonToFile(repositoryCachePath, modStateModel->getRepositoryData());
 		JsonUtils::jsonToFile(repositoryCachePath, modStateModel->getRepositoryData());
+		logGlobal->info("Installing repository: ended");
 	}
 	}
 
 
 	if(!mods.empty())
 	if(!mods.empty())
 	{
 	{
+		logGlobal->info("Installing mods: started");
 		installMods(mods);
 		installMods(mods);
-		modStateModel->reloadLocalState();
-		modModel->reloadRepositories();
+		reload();
+		logGlobal->info("Installing mods: ended");
 	}
 	}
 
 
 	if(!maps.empty())
 	if(!maps.empty())
+	{
+		logGlobal->info("Installing maps: started");
 		installMaps(maps);
 		installMaps(maps);
+		logGlobal->info("Installing maps: ended");
+	}
 
 
 	if(!exe.empty())
 	if(!exe.empty())
 	{
 	{
-		ui->progressBar->setFormat(tr("Installing chronicles"));
+		logGlobal->info("Installing chronicles: started");
+		ui->progressBar->setFormat(tr("Installing Heroes Chronicles"));
+		ui->progressWidget->setVisible(true);
+		ui->pushButton->setEnabled(false);
 
 
 		float prog = 0.0;
 		float prog = 0.0;
 
 
@@ -876,6 +817,8 @@ void CModListView::installFiles(QStringList files)
 		{
 		{
 			ChroniclesExtractor ce(this, [&prog](float progress) { prog = progress; });
 			ChroniclesExtractor ce(this, [&prog](float progress) { prog = progress; });
 			ce.installChronicles(exe);
 			ce.installChronicles(exe);
+			reload();
+			enableModByName("chronicles");
 			return true;
 			return true;
 		});
 		});
 		
 		
@@ -887,10 +830,13 @@ void CModListView::installFiles(QStringList files)
 		
 		
 		if(futureExtract.get())
 		if(futureExtract.get())
 		{
 		{
+			hideProgressBar();
+			ui->pushButton->setEnabled(true);
+			ui->progressWidget->setVisible(false);
 			//update
 			//update
-			modStateModel->reloadLocalState();
-			modModel->reloadRepositories();
+			reload();
 		}
 		}
+		logGlobal->info("Installing chronicles: ended");
 	}
 	}
 
 
 	if(!images.empty())
 	if(!images.empty())
@@ -914,8 +860,9 @@ void CModListView::installMods(QStringList archives)
 	// uninstall old version of mod, if installed
 	// uninstall old version of mod, if installed
 	for(QString mod : modNames)
 	for(QString mod : modNames)
 	{
 	{
-		if(modStateModel->getMod(mod).isInstalled())
+		if(modStateModel->isModExists(mod) && modStateModel->getMod(mod).isInstalled())
 		{
 		{
+			logGlobal->info("Uninstalling old version of mod '%s'", mod.toStdString());
 			if (modStateModel->isModEnabled(mod))
 			if (modStateModel->isModEnabled(mod))
 				modsToEnable.push_back(mod);
 				modsToEnable.push_back(mod);
 
 
@@ -928,19 +875,29 @@ void CModListView::installMods(QStringList archives)
 		}
 		}
 	}
 	}
 
 
+	reload(); // FIXME: better way that won't reset selection
+
 	for(int i = 0; i < modNames.size(); i++)
 	for(int i = 0; i < modNames.size(); i++)
 	{
 	{
+		logGlobal->info("Installing mod '%s'", modNames[i].toStdString());
 		ui->progressBar->setFormat(tr("Installing mod %1").arg(modNames[i]));
 		ui->progressBar->setFormat(tr("Installing mod %1").arg(modNames[i]));
 		manager->installMod(modNames[i], archives[i]);
 		manager->installMod(modNames[i], archives[i]);
 	}
 	}
 
 
+	reload();
+
 	if (!modsToEnable.empty())
 	if (!modsToEnable.empty())
+	{
 		manager->enableMods(modsToEnable);
 		manager->enableMods(modsToEnable);
+	}
 
 
 	checkManagerErrors();
 	checkManagerErrors();
 
 
 	for(QString archive : archives)
 	for(QString archive : archives)
+	{
+		logGlobal->info("Erasing archive '%s'", archive.toStdString());
 		QFile::remove(archive);
 		QFile::remove(archive);
+	}
 }
 }
 
 
 void CModListView::installMaps(QStringList maps)
 void CModListView::installMaps(QStringList maps)
@@ -949,6 +906,7 @@ void CModListView::installMaps(QStringList maps)
 
 
 	for(QString map : maps)
 	for(QString map : maps)
 	{
 	{
+		logGlobal->info("Importing map '%s'", map.toStdString());
 		QFile(map).rename(destDir + map.section('/', -1, -1));
 		QFile(map).rename(destDir + map.section('/', -1, -1));
 	}
 	}
 }
 }
@@ -1036,11 +994,13 @@ void CModListView::on_screenshotsList_clicked(const QModelIndex & index)
 
 
 void CModListView::doInstallMod(const QString & modName)
 void CModListView::doInstallMod(const QString & modName)
 {
 {
-	for(const auto & name : modStateModel->getMod(modName).getDependencies())
+	for(const auto & name : getModsToInstall(modName))
 	{
 	{
 		auto mod = modStateModel->getMod(name);
 		auto mod = modStateModel->getMod(name);
-		if(!mod.isInstalled())
-			downloadFile(name + ".zip", mod.getDownloadUrl(), name, mod.getDownloadSizeBytes());
+		if(mod.isAvailable())
+			downloadMod(mod);
+		else if(!modStateModel->isModEnabled(name))
+			enableModByName(name);
 	}
 	}
 }
 }
 
 
@@ -1060,6 +1020,39 @@ bool CModListView::isModInstalled(const QString & modName)
 	return mod.isInstalled();
 	return mod.isInstalled();
 }
 }
 
 
+QStringList CModListView::getInstalledChronicles()
+{
+	QStringList result;
+
+	for(const auto & modName : modStateModel->getAllMods())
+	{
+		auto mod = modStateModel->getMod(modName);
+		if (!mod.isInstalled())
+			continue;
+
+		if (mod.getTopParentID() != "chronicles")
+			continue;
+
+		result += modName;
+	}
+
+	return result;
+}
+
+QStringList CModListView::getUpdateableMods()
+{
+	QStringList result;
+
+	for(const auto & modName : modStateModel->getAllMods())
+	{
+		auto mod = modStateModel->getMod(modName);
+		if (mod.isUpdateAvailable())
+			result.push_back(modName);
+	}
+
+	return result;
+}
+
 QString CModListView::getTranslationModName(const QString & language)
 QString CModListView::getTranslationModName(const QString & language)
 {
 {
 	for(const auto & modName : modStateModel->getAllMods())
 	for(const auto & modName : modStateModel->getAllMods())
@@ -1123,3 +1116,34 @@ void CModListView::on_allModsView_doubleClicked(const QModelIndex &index)
 		return;
 		return;
 	}
 	}
 }
 }
+
+void CModListView::createNewPreset(const QString & presetName)
+{
+	modStateModel->createNewPreset(presetName);
+}
+
+void CModListView::deletePreset(const QString & presetName)
+{
+	modStateModel->deletePreset(presetName);
+}
+
+void CModListView::activatePreset(const QString & presetName)
+{
+	modStateModel->activatePreset(presetName);
+	reload();
+}
+
+void CModListView::renamePreset(const QString & oldPresetName, const QString & newPresetName)
+{
+	modStateModel->renamePreset(oldPresetName, newPresetName);
+}
+
+QStringList CModListView::getAllPresets() const
+{
+	return modStateModel->getAllPresets();
+}
+
+QString CModListView::getActivePreset() const
+{
+	return modStateModel->getActivePreset();
+}

+ 30 - 21
launcher/modManager/cmodlistview_moc.h

@@ -36,6 +36,9 @@ class CModListView : public QWidget
 	ModStateItemModel * modModel;
 	ModStateItemModel * modModel;
 	CModFilterModel * filterModel;
 	CModFilterModel * filterModel;
 	CDownloadManager * dlManager;
 	CDownloadManager * dlManager;
+	JsonNode accumulatedRepositoryData;
+
+	QStringList enqueuedModDownloads;
 
 
 	void setupModModel();
 	void setupModModel();
 	void setupFilterModel();
 	void setupFilterModel();
@@ -52,20 +55,13 @@ class CModListView : public QWidget
 	// find mods unknown to mod list (not present in repo and not installed)
 	// find mods unknown to mod list (not present in repo and not installed)
 	QStringList findUnavailableMods(QStringList candidates);
 	QStringList findUnavailableMods(QStringList candidates);
 
 
-	void manualInstallFile(QString filePath);
-	void downloadFile(QString file, QString url, QString description, qint64 sizeBytes = 0);
-	void downloadFile(QString file, QUrl url, QString description, qint64 sizeBytes = 0);
-
 	void installMods(QStringList archives);
 	void installMods(QStringList archives);
 	void installMaps(QStringList maps);
 	void installMaps(QStringList maps);
-	void installFiles(QStringList mods);
 
 
 	QString genChangelogText(const ModState & mod);
 	QString genChangelogText(const ModState & mod);
 	QString genModInfoText(const ModState & mod);
 	QString genModInfoText(const ModState & mod);
 
 
 	void changeEvent(QEvent *event) override;
 	void changeEvent(QEvent *event) override;
-	void dragEnterEvent(QDragEnterEvent* event) override;
-	void dropEvent(QDropEvent *event) override;
 
 
 public:
 public:
 	explicit CModListView(QWidget * parent = nullptr);
 	explicit CModListView(QWidget * parent = nullptr);
@@ -74,6 +70,8 @@ public:
 	void loadScreenshots();
 	void loadScreenshots();
 	void loadRepositories();
 	void loadRepositories();
 
 
+	void reload();
+
 	void disableModInfo();
 	void disableModInfo();
 
 
 	void selectMod(const QModelIndex & index);
 	void selectMod(const QModelIndex & index);
@@ -83,18 +81,43 @@ public:
 	/// install mod by name
 	/// install mod by name
 	void doInstallMod(const QString & modName);
 	void doInstallMod(const QString & modName);
 
 
+	/// update mod by name
+	void doUpdateMod(const QString & modName);
+
 	/// returns true if mod is available in repository and can be installed
 	/// returns true if mod is available in repository and can be installed
 	bool isModAvailable(const QString & modName);
 	bool isModAvailable(const QString & modName);
 
 
 	/// finds translation mod for specified languages. Returns empty string on error
 	/// finds translation mod for specified languages. Returns empty string on error
 	QString getTranslationModName(const QString & language);
 	QString getTranslationModName(const QString & language);
 
 
+	/// finds all already imported Heroes Chronicles mods (if any)
+	QStringList getInstalledChronicles();
+
+	/// finds all mods that can be updated
+	QStringList getUpdateableMods();
+
+	void createNewPreset(const QString & presetName);
+
+	void deletePreset(const QString & presetName);
+
+	void activatePreset(const QString & presetName);
+
+	void renamePreset(const QString & oldPresetName, const QString & newPresetName);
+
+	QStringList getAllPresets() const;
+
+	QString getActivePreset() const;
+
 	/// returns true if mod is currently enabled
 	/// returns true if mod is currently enabled
 	bool isModEnabled(const QString & modName);
 	bool isModEnabled(const QString & modName);
 
 
 	/// returns true if mod is currently installed
 	/// returns true if mod is currently installed
 	bool isModInstalled(const QString & modName);
 	bool isModInstalled(const QString & modName);
 
 
+	void downloadMod(const ModState & mod);
+	void downloadFile(QString file, QUrl url, QString description, qint64 sizeBytes = 0);
+	void installFiles(QStringList mods);
+
 public slots:
 public slots:
 	void enableModByName(QString modName);
 	void enableModByName(QString modName);
 	void disableModByName(QString modName);
 	void disableModByName(QString modName);
@@ -109,31 +132,17 @@ private slots:
 	void hideProgressBar();
 	void hideProgressBar();
 
 
 	void on_lineEdit_textChanged(const QString & arg1);
 	void on_lineEdit_textChanged(const QString & arg1);
-
 	void on_comboBox_currentIndexChanged(int index);
 	void on_comboBox_currentIndexChanged(int index);
-
 	void on_enableButton_clicked();
 	void on_enableButton_clicked();
-
 	void on_disableButton_clicked();
 	void on_disableButton_clicked();
-
 	void on_updateButton_clicked();
 	void on_updateButton_clicked();
-
 	void on_uninstallButton_clicked();
 	void on_uninstallButton_clicked();
-
 	void on_installButton_clicked();
 	void on_installButton_clicked();
-
-	void on_installFromFileButton_clicked();
-
 	void on_pushButton_clicked();
 	void on_pushButton_clicked();
-
 	void on_refreshButton_clicked();
 	void on_refreshButton_clicked();
-
 	void on_allModsView_activated(const QModelIndex & index);
 	void on_allModsView_activated(const QModelIndex & index);
-
 	void on_tabWidget_currentChanged(int index);
 	void on_tabWidget_currentChanged(int index);
-
 	void on_screenshotsList_clicked(const QModelIndex & index);
 	void on_screenshotsList_clicked(const QModelIndex & index);
-
 	void on_allModsView_doubleClicked(const QModelIndex &index);
 	void on_allModsView_doubleClicked(const QModelIndex &index);
 
 
 private:
 private:

+ 9 - 36
launcher/modManager/cmodlistview_moc.ui

@@ -42,6 +42,9 @@
        <property name="placeholderText">
        <property name="placeholderText">
         <string>Filter</string>
         <string>Filter</string>
        </property>
        </property>
+       <property name="clearButtonEnabled">
+        <bool>true</bool>
+       </property>
       </widget>
       </widget>
      </item>
      </item>
      <item>
      <item>
@@ -191,7 +194,9 @@
 &lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;meta charset=&quot;utf-8&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
 &lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;meta charset=&quot;utf-8&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
 p, li { white-space: pre-wrap; }
 p, li { white-space: pre-wrap; }
 hr { height: 1px; border-width: 0; }
 hr { height: 1px; border-width: 0; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'.AppleSystemUIFont'; font-size:13pt; font-weight:400; font-style:normal;&quot;&gt;
+li.unchecked::marker { content: &quot;\2610&quot;; }
+li.checked::marker { content: &quot;\2612&quot;; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;&quot;&gt;
 &lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Ubuntu'; font-size:11pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
 &lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Ubuntu'; font-size:11pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
           </property>
           </property>
           <property name="openExternalLinks">
           <property name="openExternalLinks">
@@ -317,6 +322,9 @@ hr { height: 1px; border-width: 0; }
         <property name="value">
         <property name="value">
          <number>0</number>
          <number>0</number>
         </property>
         </property>
+        <property name="alignment">
+         <set>Qt::AlignCenter</set>
+        </property>
         <property name="textVisible">
         <property name="textVisible">
          <bool>true</bool>
          <bool>true</bool>
         </property>
         </property>
@@ -349,41 +357,6 @@ hr { height: 1px; border-width: 0; }
      <property name="spacing">
      <property name="spacing">
       <number>6</number>
       <number>6</number>
      </property>
      </property>
-     <item>
-      <widget class="QPushButton" name="installFromFileButton">
-       <property name="sizePolicy">
-        <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
-         <horstretch>0</horstretch>
-         <verstretch>0</verstretch>
-        </sizepolicy>
-       </property>
-       <property name="minimumSize">
-        <size>
-         <width>51</width>
-         <height>0</height>
-        </size>
-       </property>
-       <property name="maximumSize">
-        <size>
-         <width>170</width>
-         <height>16777215</height>
-        </size>
-       </property>
-       <property name="text">
-        <string>Install from file</string>
-       </property>
-       <property name="icon">
-        <iconset>
-         <normaloff>icons:mod-download.png</normaloff>icons:mod-download.png</iconset>
-       </property>
-       <property name="iconSize">
-        <size>
-         <width>20</width>
-         <height>20</height>
-        </size>
-       </property>
-      </widget>
-     </item>
      <item>
      <item>
       <spacer name="modButtonSpacer">
       <spacer name="modButtonSpacer">
        <property name="orientation">
        <property name="orientation">

+ 7 - 11
launcher/modManager/modstatecontroller.cpp

@@ -72,9 +72,9 @@ ModStateController::ModStateController(std::shared_ptr<ModStateModel> modList)
 
 
 ModStateController::~ModStateController() = default;
 ModStateController::~ModStateController() = default;
 
 
-void ModStateController::appendRepositories(const JsonNode & repomap)
+void ModStateController::setRepositoryData(const JsonNode & repomap)
 {
 {
-	modList->appendRepositories(repomap);
+	modList->setRepositoryData(repomap);
 }
 }
 
 
 bool ModStateController::addError(QString modname, QString message)
 bool ModStateController::addError(QString modname, QString message)
@@ -120,6 +120,9 @@ bool ModStateController::disableMod(QString modname)
 
 
 bool ModStateController::canInstallMod(QString modname)
 bool ModStateController::canInstallMod(QString modname)
 {
 {
+	if (!modList->isModExists(modname))
+		return true; // for installation of unknown mods, e.g. via "Install from file" option
+
 	auto mod = modList->getMod(modname);
 	auto mod = modList->getMod(modname);
 
 
 	if(mod.isSubmod())
 	if(mod.isSubmod())
@@ -155,7 +158,7 @@ bool ModStateController::canEnableMod(QString modname)
 
 
 	//check for compatibility
 	//check for compatibility
 	if(!mod.isCompatible())
 	if(!mod.isCompatible())
-		return addError(modname, tr("Mod is not compatible, please update VCMI and checkout latest mod revisions"));
+		return addError(modname, tr("Mod is not compatible, please update VCMI and check the latest mod revisions"));
 
 
 	if (mod.isTranslation() && CGeneralTextHandler::getPreferredLanguage() != mod.getBaseLanguage().toStdString())
 	if (mod.isTranslation() && CGeneralTextHandler::getPreferredLanguage() != mod.getBaseLanguage().toStdString())
 		return addError(modname, tr("Can not enable translation mod for a different language!"));
 		return addError(modname, tr("Can not enable translation mod for a different language!"));
@@ -189,9 +192,6 @@ bool ModStateController::doInstallMod(QString modname, QString archivePath)
 	if(!QFile(archivePath).exists())
 	if(!QFile(archivePath).exists())
 		return addError(modname, tr("Mod archive is missing"));
 		return addError(modname, tr("Mod archive is missing"));
 
 
-	if(localMods.contains(modname))
-		return addError(modname, tr("Mod with such name is already installed"));
-
 	std::vector<std::string> filesToExtract;
 	std::vector<std::string> filesToExtract;
 	QString modDirName = ::detectModArchive(archivePath, modname, filesToExtract);
 	QString modDirName = ::detectModArchive(archivePath, modname, filesToExtract);
 	if(!modDirName.size())
 	if(!modDirName.size())
@@ -234,8 +234,6 @@ bool ModStateController::doInstallMod(QString modname, QString archivePath)
 	QString upperLevel = modDirName.section('/', 0, 0);
 	QString upperLevel = modDirName.section('/', 0, 0);
 	if(upperLevel != modDirName)
 	if(upperLevel != modDirName)
 		removeModDir(destDir + upperLevel);
 		removeModDir(destDir + upperLevel);
-	
-	modList->reloadLocalState();
 
 
 	return true;
 	return true;
 }
 }
@@ -251,9 +249,7 @@ bool ModStateController::doUninstallMod(QString modname)
 
 
 	QDir modFullDir(modDir);
 	QDir modFullDir(modDir);
 	if(!removeModDir(modDir))
 	if(!removeModDir(modDir))
-		return addError(modname, tr("Mod is located in protected directory, please remove it manually:\n") + modFullDir.absolutePath());
-
-	modList->reloadLocalState();
+		return addError(modname, tr("Mod is located in a protected directory, please remove it manually:\n") + modFullDir.absolutePath());
 
 
 	return true;
 	return true;
 }
 }

+ 1 - 3
launcher/modManager/modstatecontroller.h

@@ -27,8 +27,6 @@ class ModStateController : public QObject, public boost::noncopyable
 	bool doInstallMod(QString mod, QString archivePath);
 	bool doInstallMod(QString mod, QString archivePath);
 	bool doUninstallMod(QString mod);
 	bool doUninstallMod(QString mod);
 
 
-	QVariantMap localMods;
-
 	QStringList recentErrors;
 	QStringList recentErrors;
 	bool addError(QString modname, QString message);
 	bool addError(QString modname, QString message);
 	bool removeModDir(QString mod);
 	bool removeModDir(QString mod);
@@ -37,7 +35,7 @@ public:
 	ModStateController(std::shared_ptr<ModStateModel> modList);
 	ModStateController(std::shared_ptr<ModStateModel> modList);
 	~ModStateController();
 	~ModStateController();
 
 
-	void appendRepositories(const JsonNode & repositoriesList);
+	void setRepositoryData(const JsonNode & repositoriesList);
 
 
 	QStringList getErrors();
 	QStringList getErrors();
 
 

+ 23 - 26
launcher/modManager/modstateitemmodel_moc.cpp

@@ -32,34 +32,31 @@ QString ModStateItemModel::modIndexToName(const QModelIndex & index) const
 
 
 QString ModStateItemModel::modTypeName(QString modTypeID) const
 QString ModStateItemModel::modTypeName(QString modTypeID) const
 {
 {
-	static const QMap<QString, QString> modTypes = {
-		{"Translation", tr("Translation")},
-		{"Town",        tr("Town")       },
-		{"Test",        tr("Test")       },
-		{"Templates",   tr("Templates")  },
-		{"Spells",      tr("Spells")     },
-		{"Music",       tr("Music")      },
-		{"Maps",        tr("Maps")       },
-		{"Sounds",      tr("Sounds")     },
-		{"Skills",      tr("Skills")     },
-		{"Other",       tr("Other")      },
-		{"Objects",     tr("Objects")    },
-		{"Mechanical",  tr("Mechanics")  },
-		{"Mechanics",   tr("Mechanics")  },
-		{"Themes",      tr("Interface")  },
-		{"Interface",   tr("Interface")  },
-		{"Heroes",      tr("Heroes")     },
-		{"Graphic",     tr("Graphical")  },
-		{"Graphical",   tr("Graphical")  },
-		{"Expansion",   tr("Expansion")  },
-		{"Creatures",   tr("Creatures")  },
-		{"Compatibility", tr("Compatibility") },
-		{"Artifacts",   tr("Artifacts")  },
-		{"AI",          tr("AI")         },
+	static const QStringList modTypes = {
+		QT_TR_NOOP("Translation"),
+		QT_TR_NOOP("Town"),
+		QT_TR_NOOP("Test"),
+		QT_TR_NOOP("Templates"),
+		QT_TR_NOOP("Spells"),
+		QT_TR_NOOP("Music"),
+		QT_TR_NOOP("Maps"),
+		QT_TR_NOOP("Sounds"),
+		QT_TR_NOOP("Skills"),
+		QT_TR_NOOP("Other"),
+		QT_TR_NOOP("Objects"),
+		QT_TR_NOOP("Mechanics"),
+		QT_TR_NOOP("Interface"),
+		QT_TR_NOOP("Heroes"),
+		QT_TR_NOOP("Graphical"),
+		QT_TR_NOOP("Expansion"),
+		QT_TR_NOOP("Creatures"),
+		QT_TR_NOOP("Compatibility") ,
+		QT_TR_NOOP("Artifacts"),
+		QT_TR_NOOP("AI"),
 	};
 	};
 
 
 	if (modTypes.contains(modTypeID))
 	if (modTypes.contains(modTypeID))
-		return modTypes[modTypeID];
+		return tr(modTypeID.toStdString().c_str());
 	return tr("Other");
 	return tr("Other");
 }
 }
 
 
@@ -198,7 +195,7 @@ QVariant ModStateItemModel::headerData(int section, Qt::Orientation orientation,
 	return QVariant();
 	return QVariant();
 }
 }
 
 
-void ModStateItemModel::reloadRepositories()
+void ModStateItemModel::reloadViewModel()
 {
 {
 	beginResetModel();
 	beginResetModel();
 	endResetModel();
 	endResetModel();

+ 1 - 1
launcher/modManager/modstateitemmodel_moc.h

@@ -72,7 +72,7 @@ public:
 	explicit ModStateItemModel(std::shared_ptr<ModStateModel> model, QObject * parent);
 	explicit ModStateItemModel(std::shared_ptr<ModStateModel> model, QObject * parent);
 
 
 	/// CModListContainer overrides
 	/// CModListContainer overrides
-	void reloadRepositories();
+	void reloadViewModel();
 	void modChanged(QString modID);
 	void modChanged(QString modID);
 
 
 	QVariant data(const QModelIndex & index, int role) const override;
 	QVariant data(const QModelIndex & index, int role) const override;

+ 33 - 4
launcher/modManager/modstatemodel.cpp

@@ -11,7 +11,6 @@
 #include "modstatemodel.h"
 #include "modstatemodel.h"
 
 
 #include "../../lib/filesystem/Filesystem.h"
 #include "../../lib/filesystem/Filesystem.h"
-#include "../../lib/json/JsonUtils.h"
 #include "../../lib/modding/ModManager.h"
 #include "../../lib/modding/ModManager.h"
 
 
 ModStateModel::ModStateModel()
 ModStateModel::ModStateModel()
@@ -22,10 +21,9 @@ ModStateModel::ModStateModel()
 
 
 ModStateModel::~ModStateModel() = default;
 ModStateModel::~ModStateModel() = default;
 
 
-void ModStateModel::appendRepositories(const JsonNode & repositoriesList)
+void ModStateModel::setRepositoryData(const JsonNode & repositoriesList)
 {
 {
-	JsonUtils::mergeCopy(*repositoryData, repositoriesList);
-
+	*repositoryData = repositoriesList;
 	modManager = std::make_unique<ModManager>(*repositoryData);
 	modManager = std::make_unique<ModManager>(*repositoryData);
 }
 }
 
 
@@ -128,3 +126,34 @@ QString ModStateModel::getTopParent(QString modname) const
 	else
 	else
 		return "";
 		return "";
 }
 }
+
+void ModStateModel::createNewPreset(const QString & presetName)
+{
+	modManager->createNewPreset(presetName.toStdString());
+}
+
+void ModStateModel::deletePreset(const QString & presetName)
+{
+	modManager->deletePreset(presetName.toStdString());
+}
+
+void ModStateModel::activatePreset(const QString & presetName)
+{
+	modManager->activatePreset(presetName.toStdString());
+}
+
+void ModStateModel::renamePreset(const QString & oldPresetName, const QString & newPresetName)
+{
+	modManager->renamePreset(oldPresetName.toStdString(), newPresetName.toStdString());
+}
+
+QStringList ModStateModel::getAllPresets() const
+{
+	auto result = modManager->getAllPresets();
+	return stringListStdToQt(result);
+}
+
+QString ModStateModel::getActivePreset() const
+{
+	return QString::fromStdString(modManager->getActivePreset());
+}

+ 9 - 1
launcher/modManager/modstatemodel.h

@@ -27,7 +27,7 @@ public:
 	ModStateModel();
 	ModStateModel();
 	~ModStateModel();
 	~ModStateModel();
 
 
-	void appendRepositories(const JsonNode & repositoriesList);
+	void setRepositoryData(const JsonNode & repositoriesList);
 	void reloadLocalState();
 	void reloadLocalState();
 	const JsonNode & getRepositoryData() const;
 	const JsonNode & getRepositoryData() const;
 
 
@@ -49,4 +49,12 @@ public:
 
 
 	bool isSubmod(QString modname);
 	bool isSubmod(QString modname);
 	QString getTopParent(QString modname) const;
 	QString getTopParent(QString modname) const;
+
+	void createNewPreset(const QString & presetName);
+	void deletePreset(const QString & presetName);
+	void activatePreset(const QString & presetName);
+	void renamePreset(const QString & oldPresetName, const QString & newPresetName);
+
+	QStringList getAllPresets() const;
+	QString getActivePreset() const;
 };
 };

+ 7 - 17
launcher/settingsView/csettingsview_moc.cpp

@@ -517,36 +517,26 @@ void CSettingsView::loadTranslation()
 	if (!mainWindow)
 	if (!mainWindow)
 		return;
 		return;
 
 
-	QString languageName = QString::fromStdString(settings["general"]["language"].String());
-	QString modName = mainWindow->getModView()->getTranslationModName(languageName);
-	bool translationExists = !modName.isEmpty();
-	bool translationNeeded = languageName != baseLanguage;
-	bool showTranslation = translationNeeded && translationExists;
+	auto translationStatus = mainWindow->getTranslationStatus();
+	bool showTranslation = translationStatus == ETranslationStatus::DISABLED || translationStatus == ETranslationStatus::NOT_INSTALLLED;
 
 
 	ui->labelTranslation->setVisible(showTranslation);
 	ui->labelTranslation->setVisible(showTranslation);
 	ui->labelTranslationStatus->setVisible(showTranslation);
 	ui->labelTranslationStatus->setVisible(showTranslation);
 	ui->pushButtonTranslation->setVisible(showTranslation);
 	ui->pushButtonTranslation->setVisible(showTranslation);
+	ui->pushButtonTranslation->setVisible(translationStatus != ETranslationStatus::ACTIVE);
 
 
-	if (!translationExists || !translationNeeded)
-		return;
-
-	bool translationAvailable = mainWindow->getModView()->isModAvailable(modName);
-	bool translationEnabled = mainWindow->getModView()->isModEnabled(modName);
-
-	ui->pushButtonTranslation->setVisible(!translationEnabled);
-
-	if (translationEnabled)
+	if (translationStatus == ETranslationStatus::ACTIVE)
 	{
 	{
 		ui->labelTranslationStatus->setText(tr("Active"));
 		ui->labelTranslationStatus->setText(tr("Active"));
 	}
 	}
 
 
-	if (!translationEnabled && !translationAvailable)
+	if (translationStatus == ETranslationStatus::DISABLED)
 	{
 	{
 		ui->labelTranslationStatus->setText(tr("Disabled"));
 		ui->labelTranslationStatus->setText(tr("Disabled"));
 		ui->pushButtonTranslation->setText(tr("Enable"));
 		ui->pushButtonTranslation->setText(tr("Enable"));
 	}
 	}
 
 
-	if (translationAvailable)
+	if (translationStatus == ETranslationStatus::NOT_INSTALLLED)
 	{
 	{
 		ui->labelTranslationStatus->setText(tr("Not Installed"));
 		ui->labelTranslationStatus->setText(tr("Not Installed"));
 		ui->pushButtonTranslation->setText(tr("Install"));
 		ui->pushButtonTranslation->setText(tr("Install"));
@@ -614,7 +604,7 @@ void CSettingsView::on_lineEditRepositoryExtra_textEdited(const QString &arg1)
 void CSettingsView::on_spinBoxInterfaceScaling_valueChanged(int arg1)
 void CSettingsView::on_spinBoxInterfaceScaling_valueChanged(int arg1)
 {
 {
 	Settings node = settings.write["video"]["resolution"]["scaling"];
 	Settings node = settings.write["video"]["resolution"]["scaling"];
-	node->Float() = arg1;
+	node->Float() = ui->buttonScalingAuto->isChecked() ? 0 : arg1;
 }
 }
 
 
 void CSettingsView::on_refreshRepositoriesButton_clicked()
 void CSettingsView::on_refreshRepositoriesButton_clicked()

+ 5 - 5
launcher/settingsView/csettingsview_moc.ui

@@ -47,7 +47,7 @@
       <property name="geometry">
       <property name="geometry">
        <rect>
        <rect>
         <x>0</x>
         <x>0</x>
-        <y>-797</y>
+        <y>0</y>
         <width>729</width>
         <width>729</width>
         <height>1503</height>
         <height>1503</height>
        </rect>
        </rect>
@@ -1179,13 +1179,13 @@
        <item row="11" column="1" colspan="5">
        <item row="11" column="1" colspan="5">
         <widget class="QComboBox" name="comboBoxFullScreen">
         <widget class="QComboBox" name="comboBoxFullScreen">
          <property name="toolTip">
          <property name="toolTip">
-          <string>Select display mode for game
+          <string>Select a display mode for the game
 
 
-Windowed - game will run inside a window that covers part of your screen
+Windowed - the game will run inside a window that covers part of your screen.
 
 
-Borderless Windowed Mode - game will run in a window that covers entirely of your screen, using same resolution as your screen.
+Borderless Windowed Mode - the game will run in a full-screen window, matching your screen's resolution.
 
 
-Fullscreen Exclusive Mode - game will cover entirety of your screen and will use selected resolution.</string>
+Fullscreen Exclusive Mode - the game will cover the entirety of your screen and will use selected resolution.</string>
          </property>
          </property>
          <property name="currentIndex">
          <property name="currentIndex">
           <number>0</number>
           <number>0</number>

+ 431 - 0
launcher/startGame/StartGameTab.cpp

@@ -0,0 +1,431 @@
+/*
+ * StartGameTab.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 "StartGameTab.h"
+#include "ui_StartGameTab.h"
+
+#include "../mainwindow_moc.h"
+#include "../main.h"
+#include "../updatedialog_moc.h"
+
+#include "../modManager/cmodlistview_moc.h"
+
+#include "../../lib/filesystem/Filesystem.h"
+#include "../../lib/VCMIDirs.h"
+
+void StartGameTab::changeEvent(QEvent *event)
+{
+	if(event->type() == QEvent::LanguageChange)
+	{
+		ui->retranslateUi(this);
+		refreshState();
+	}
+
+	QWidget::changeEvent(event);
+}
+
+StartGameTab::StartGameTab(QWidget * parent)
+	: QWidget(parent)
+	, ui(new Ui::StartGameTab)
+{
+	ui->setupUi(this);
+
+	ui->buttonGameResume->setIcon(QIcon{":/icons/menu-game.png"}); //TODO: different icon?
+	ui->buttonGameStart->setIcon(QIcon{":/icons/menu-game.png"});
+	ui->buttonGameEditor->setIcon(QIcon{":/icons/menu-editor.png"});
+
+	refreshState();
+
+	ui->buttonGameResume->setVisible(false); // TODO: implement
+	ui->buttonPresetExport->setVisible(false); // TODO: implement
+	ui->buttonPresetImport->setVisible(false); // TODO: implement
+
+#ifndef ENABLE_EDITOR
+	ui->buttonGameEditor->hide();
+#endif
+}
+
+StartGameTab::~StartGameTab()
+{
+	delete ui;
+}
+
+MainWindow * StartGameTab::getMainWindow()
+{
+	foreach(QWidget *w, qApp->allWidgets())
+		if(QMainWindow* mainWin = qobject_cast<QMainWindow*>(w))
+			return dynamic_cast<MainWindow *>(mainWin);
+	return nullptr;
+}
+
+void StartGameTab::refreshState()
+{
+	refreshGameData();
+	refreshUpdateStatus(EGameUpdateStatus::NOT_CHECKED);//TODO - follow automatic check on startup setting
+	refreshTranslation(getMainWindow()->getTranslationStatus());
+	refreshPresets();
+	refreshMods();
+}
+
+void StartGameTab::refreshPresets()
+{
+	QSignalBlocker blocker(ui->comboBoxModPresets);
+
+	QStringList allPresets = getMainWindow()->getModView()->getAllPresets();
+	ui->comboBoxModPresets->clear();
+	ui->comboBoxModPresets->addItems(allPresets);
+	ui->comboBoxModPresets->setCurrentText(getMainWindow()->getModView()->getActivePreset());
+	ui->buttonPresetDelete->setVisible(allPresets.size() > 1);
+}
+
+void StartGameTab::refreshGameData()
+{
+	// Some players are using pirated version of the game with some of the files missing
+	// leading to broken town hall menu (and possibly other dialogs)
+	// Provide diagnostics to indicate problem with chair-monitor adaptor layer and not with VCMI
+	static constexpr std::array potentiallyMissingFiles = {
+		"Data/TpThBkDg.bmp",
+		"Data/TpThBkFr.bmp",
+		"Data/TpThBkIn.bmp",
+		"Data/TpThBkNc.bmp",
+		"Data/TpThBkSt.bmp",
+		"Data/TpThBRrm.bmp",
+		"Data/TpThBkCs.bmp",
+		"Data/TpThBkRm.bmp",
+		"Data/TpThBkTw.bmp",
+	};
+
+	// Some players for some reason don't have AB expansion campaign files
+	static constexpr std::array armaggedonBladeCampaigns = {
+		"DATA/AB",
+		"DATA/BLOOD",
+		"DATA/SLAYER",
+		"DATA/FESTIVAL",
+		"DATA/FIRE",
+		"DATA/FOOL",
+	};
+
+	bool missingSoundtrack = !CResourceHandler::get()->existsResource(AudioPath::builtin("Music/MainMenu"));
+	bool missingVideoFiles = !CResourceHandler::get()->existsResource(VideoPath::builtin("Video/H3Intro")) && !CResourceHandler::get()->existsResource(ResourcePath("Video/H3Intro", EResType::VIDEO_LOW_QUALITY));
+	bool missingGameFiles = false;
+	bool missingCampaings = false;
+
+	for (const auto & filename : potentiallyMissingFiles)
+		missingGameFiles &= !CResourceHandler::get()->existsResource(ImagePath::builtin(filename));
+
+	for (const auto & filename : armaggedonBladeCampaigns)
+		missingCampaings &= !CResourceHandler::get()->existsResource(ResourcePath(filename, EResType::CAMPAIGN));
+
+	ui->labelMissingCampaigns->setVisible(missingCampaings);
+	ui->labelMissingFiles->setVisible(missingGameFiles);
+	ui->labelMissingVideo->setVisible(missingVideoFiles);
+	ui->labelMissingSoundtrack->setVisible(missingSoundtrack);
+
+	ui->buttonMissingCampaignsHelp->setVisible(missingCampaings);
+	ui->buttonMissingFilesHelp->setVisible(missingGameFiles);
+	ui->buttonMissingVideoHelp->setVisible(missingVideoFiles);
+	ui->buttonMissingSoundtrackHelp->setVisible(missingSoundtrack);
+}
+
+void StartGameTab::refreshTranslation(ETranslationStatus status)
+{
+	ui->buttonInstallTranslation->setVisible(status == ETranslationStatus::NOT_INSTALLLED);
+	ui->buttonInstallTranslationHelp->setVisible(status == ETranslationStatus::NOT_INSTALLLED);
+
+	ui->buttonActivateTranslation->setVisible(status == ETranslationStatus::NOT_INSTALLLED);
+	ui->buttonActivateTranslationHelp->setVisible(status == ETranslationStatus::NOT_INSTALLLED);
+}
+
+void StartGameTab::refreshMods()
+{
+	constexpr int chroniclesCount = 8;
+	QStringList updateableMods = getMainWindow()->getModView()->getUpdateableMods();
+	QStringList chroniclesMods = getMainWindow()->getModView()->getInstalledChronicles();
+
+	ui->buttonUpdateMods->setText(tr("Update %n mods", "", updateableMods.size()));
+	ui->buttonUpdateMods->setVisible(!updateableMods.empty());
+	ui->buttonUpdateModsHelp->setVisible(!updateableMods.empty());
+
+	ui->labelChronicles->setText(tr("Heroes Chronicles:\n%n/%1 installed", "", chroniclesMods.size()).arg(chroniclesCount));
+	ui->labelChronicles->setVisible(chroniclesMods.size() != chroniclesCount);
+	ui->buttonChroniclesHelp->setVisible(chroniclesMods.size() != chroniclesCount);
+}
+
+void StartGameTab::refreshUpdateStatus(EGameUpdateStatus status)
+{
+	QString availableVersion; // TODO
+
+	ui->labelTitleEngine->setText("VCMI " VCMI_VERSION_STRING);
+	ui->buttonUpdateCheck->setVisible(status == EGameUpdateStatus::NOT_CHECKED);
+	ui->labelUpdateNotFound->setVisible(status == EGameUpdateStatus::NO_UPDATE);
+	ui->labelUpdateAvailable->setVisible(status == EGameUpdateStatus::UPDATE_AVAILABLE);
+	ui->buttonOpenChangelog->setVisible(status == EGameUpdateStatus::UPDATE_AVAILABLE);
+	ui->buttonOpenDownloads->setVisible(status == EGameUpdateStatus::UPDATE_AVAILABLE);
+
+	if (status == EGameUpdateStatus::UPDATE_AVAILABLE)
+		ui->labelUpdateAvailable->setText(tr("Update to %1 available").arg(availableVersion));
+}
+
+void StartGameTab::on_buttonGameStart_clicked()
+{
+	getMainWindow()->hide();
+	startGame({});
+}
+
+void StartGameTab::on_buttonOpenChangelog_clicked()
+{
+	QDesktopServices::openUrl(QUrl("https://vcmi.eu/ChangeLog/"));
+}
+
+void StartGameTab::on_buttonOpenDownloads_clicked()
+{
+	QDesktopServices::openUrl(QUrl("https://vcmi.eu/download/"));
+}
+
+void StartGameTab::on_buttonUpdateCheck_clicked()
+{
+	UpdateDialog::showUpdateDialog(true);
+}
+
+void StartGameTab::on_buttonGameEditor_clicked()
+{
+	getMainWindow()->hide();
+	startEditor({});
+}
+
+void StartGameTab::on_buttonImportFiles_clicked()
+{
+	const auto & importFunctor = [this]
+	{
+#ifndef VCMI_MOBILE
+		QString filter =
+			tr("All supported files") + " (*.h3m *.vmap *.h3c *.vcmp *.zip *.json *.exe);;" +
+			tr("Maps") + " (*.h3m *.vmap);;" +
+			tr("Campaigns") + " (*.h3c *.vcmp);;" +
+			tr("Configs") + " (*.json);;" +
+			tr("Mods") + " (*.zip);;" +
+			tr("Gog files") + " (*.exe)";
+#else
+		//Workaround for sometimes incorrect mime for some extensions (e.g. for exe)
+		QString filter = tr("All files (*.*)");
+#endif
+		QStringList files = QFileDialog::getOpenFileNames(this, tr("Select files (configs, mods, maps, campaigns, gog files) to install..."), QDir::homePath(), filter);
+
+		for(const auto & file : files)
+		{
+			logGlobal->info("Importing file %s", file.toStdString());
+			getMainWindow()->manualInstallFile(file);
+		}
+	};
+
+	// iOS can't display modal dialogs when called directly on button press
+	// https://bugreports.qt.io/browse/QTBUG-98651
+	QTimer::singleShot(0, this, importFunctor);
+}
+
+void StartGameTab::on_buttonInstallTranslation_clicked()
+{
+	if (getMainWindow()->getTranslationStatus() == ETranslationStatus::NOT_INSTALLLED)
+	{
+		QString preferredlanguage = QString::fromStdString(settings["general"]["language"].String());
+		QString modName = getMainWindow()->getModView()->getTranslationModName(preferredlanguage);
+		getMainWindow()->getModView()->doInstallMod(modName);
+	}
+}
+
+void StartGameTab::on_buttonActivateTranslation_clicked()
+{
+	QString preferredlanguage = QString::fromStdString(settings["general"]["language"].String());
+	QString modName = getMainWindow()->getModView()->getTranslationModName(preferredlanguage);
+	getMainWindow()->getModView()->enableModByName(modName);
+}
+
+void StartGameTab::on_buttonUpdateMods_clicked()
+{
+	QStringList updateableMods = getMainWindow()->getModView()->getUpdateableMods();
+
+	getMainWindow()->switchToModsTab();
+
+	for (const auto & modName : updateableMods)
+		getMainWindow()->getModView()->doUpdateMod(modName);
+}
+
+void StartGameTab::on_buttonHelpImportFiles_clicked()
+{
+	QString message = tr(
+		"This option allows you to import additional data files into your VCMI installation. "
+		"At the moment, following options are supported:\n\n"
+		" - Heroes III Maps (.h3m or .vmap).\n"
+		" - Heroes III Campaigns (.h3c or .vcmp).\n"
+		" - Heroes III Chronicles using offline backup installer from GOG.com (.exe).\n"
+		" - VCMI mods in zip format (.zip)\n"
+		" - VCMI configuration files (.json)\n"
+	);
+
+	QMessageBox::information(this, ui->buttonImportFiles->text(), message);
+}
+
+void StartGameTab::on_buttonInstallTranslationHelp_clicked()
+{
+	QString message = tr(
+		"Your Heroes III version uses different language. "
+		"VCMI provides translations of the game into various languages that you can use. "
+		"Use this option to automatically install such translation to your language."
+	);
+	QMessageBox::information(this, ui->buttonInstallTranslation->text(), message);
+}
+
+void StartGameTab::on_buttonActivateTranslationHelp_clicked()
+{
+	QString message = tr(
+		"Translation of Heroes III into your language is installed, but has been turned off. "
+		"Use this option to enable it."
+	);
+
+	QMessageBox::information(this, ui->buttonActivateTranslation->text(), message);
+}
+
+void StartGameTab::on_buttonUpdateModsHelp_clicked()
+{
+	QString message = tr(
+		"A new version of some of the mods that you have installed is now available in mod repository. "
+		"Use this option to automatically update all your mods to latest version.\n\n"
+		"WARNING: In some cases, updated versions of mods may not be compatible with your existing saves. "
+		"You many want to postpone mod update until you finish any of your ongoing games."
+		);
+
+	QMessageBox::information(this, ui->buttonUpdateMods->text(), message);
+}
+
+void StartGameTab::on_buttonChroniclesHelp_clicked()
+{
+	QString message = tr(
+		"If you own Heroes Chronicles on gog.com, you can use offline backup installers provided by gog "
+		"to import Heroes Chronicles data into VCMI as custom campaigns.\n"
+		"To import Heroes Chronicles, download offline backup installer of each chronicle that you wish to install, "
+		"select 'Import files' option and select downloaded file. "
+		"This will generate and install mod for VCMI that contains imported chronicles"
+	);
+
+	QMessageBox::information(this, ui->labelChronicles->text(), message);
+}
+
+void StartGameTab::on_buttonMissingSoundtrackHelp_clicked()
+{
+	QString message = tr(
+		"VCMI has detected that Heroes III music files are missing from your installation. "
+		"VCMI will run, but in-game music will not be available.\n\n"
+		"To resolve this problem, please copy missing mp3 files from Heroes III to VCMI data files directory manually "
+		"or reinstall VCMI and re-import Heroes III data files"
+	);
+	QMessageBox::information(this, ui->labelMissingSoundtrack->text(), message);
+}
+
+void StartGameTab::on_buttonMissingVideoHelp_clicked()
+{
+	QString message = tr(
+		"VCMI has detected that Heroes III video files are missing from your installation. "
+		"VCMI will run, but in-game cutscenes will not be available.\n\n"
+		"To resolve this problem, please copy VIDEO.VID file from Heroes III to VCMI data files directory manually "
+		"or reinstall VCMI and re-import Heroes III data files"
+		);
+	QMessageBox::information(this, ui->labelMissingVideo->text(), message);
+}
+
+void StartGameTab::on_buttonMissingFilesHelp_clicked()
+{
+	QString message = tr(
+		"VCMI has detected that some of Heroes III data files are missing from your installation. "
+		"You may attempt to run VCMI, but game may not work as expected or crash.\n\n"
+		"To resolve this problem, please reinstall game and reimport data files using supported version of Heroes III. "
+		"VCMI requires Heroes III: Shadow of Death or Complete Edition to run, which you can get (for example) from gog.com"
+	);
+	QMessageBox::information(this, ui->labelMissingFiles->text(), message);
+}
+
+void StartGameTab::on_buttonMissingCampaignsHelp_clicked()
+{
+	QString message = tr(
+		"VCMI has detected that some of Heroes III: Armageddon's Blade data files are missing from your installation. "
+		"VCMI will work, but Armageddon's Blade campaigns will not be available.\n\n"
+		"To resolve this problem, please copy missing data files from Heroes III to VCMI data files directory manually "
+		"or reinstall VCMI and re-import Heroes III data files"
+	);
+	QMessageBox::information(this, ui->labelMissingCampaigns->text(), message);
+}
+
+void StartGameTab::on_buttonPresetExport_clicked()
+{
+	// TODO
+}
+
+void StartGameTab::on_buttonPresetImport_clicked()
+{
+	// TODO
+}
+
+void StartGameTab::on_buttonPresetNew_clicked()
+{
+	bool ok;
+	QString presetName = QInputDialog::getText(
+		this,
+		ui->buttonPresetNew->text(),
+		tr("Enter preset name:"),
+		QLineEdit::Normal,
+		QString(),
+		&ok);
+
+	if (ok && !presetName.isEmpty())
+	{
+		getMainWindow()->getModView()->createNewPreset(presetName);
+		getMainWindow()->getModView()->activatePreset(presetName);
+		refreshPresets();
+	}
+}
+
+void StartGameTab::on_buttonPresetDelete_clicked()
+{
+	QString activePresetBefore = getMainWindow()->getModView()->getActivePreset();
+	QStringList allPresets = getMainWindow()->getModView()->getAllPresets();
+
+	allPresets.removeAll(activePresetBefore);
+	if (!allPresets.empty())
+	{
+		getMainWindow()->getModView()->activatePreset(allPresets.front());
+		getMainWindow()->getModView()->deletePreset(activePresetBefore);
+		refreshPresets();
+	}
+}
+
+void StartGameTab::on_comboBoxModPresets_currentTextChanged(const QString &presetName)
+{
+	getMainWindow()->getModView()->activatePreset(presetName);
+}
+
+void StartGameTab::on_buttonPresetRename_clicked()
+{
+	QString currentName = getMainWindow()->getModView()->getActivePreset();
+
+	bool ok;
+	QString newName = QInputDialog::getText(
+		this,
+		ui->buttonPresetNew->text(),
+		tr("Rename preset '%1' to:").arg(currentName),
+		QLineEdit::Normal,
+		currentName,
+		&ok);
+
+	if (ok && !newName.isEmpty())
+	{
+		getMainWindow()->getModView()->renamePreset(currentName, newName);
+		refreshPresets();
+	}
+}
+

+ 83 - 0
launcher/startGame/StartGameTab.h

@@ -0,0 +1,83 @@
+/*
+ * StartGameTab.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
+ *
+ */
+#pragma once
+
+#include <QWidget>
+
+namespace Ui
+{
+class StartGameTab;
+}
+
+enum class EGameUpdateStatus : int8_t
+{
+	NOT_CHECKED,
+	NO_UPDATE,
+	UPDATE_AVAILABLE
+};
+
+enum class ETranslationStatus : int8_t;
+
+class MainWindow;
+
+class StartGameTab : public QWidget
+{
+	Q_OBJECT
+
+	MainWindow * getMainWindow();
+
+	void refreshUpdateStatus(EGameUpdateStatus status);
+	void refreshTranslation(ETranslationStatus status);
+	void refreshMods();
+	void refreshPresets();
+	void refreshGameData();
+
+	void changeEvent(QEvent *event) override;
+public:
+	explicit StartGameTab(QWidget * parent = nullptr);
+	~StartGameTab();
+
+	void refreshState();
+
+private slots:
+	void on_buttonGameStart_clicked();
+	void on_buttonOpenChangelog_clicked();
+	void on_buttonOpenDownloads_clicked();
+	void on_buttonUpdateCheck_clicked();
+	void on_buttonGameEditor_clicked();
+	void on_buttonImportFiles_clicked();
+	void on_buttonInstallTranslation_clicked();
+	void on_buttonActivateTranslation_clicked();
+	void on_buttonUpdateMods_clicked();
+	void on_buttonHelpImportFiles_clicked();
+	void on_buttonInstallTranslationHelp_clicked();
+	void on_buttonActivateTranslationHelp_clicked();
+	void on_buttonUpdateModsHelp_clicked();
+	void on_buttonChroniclesHelp_clicked();
+	void on_buttonMissingSoundtrackHelp_clicked();
+	void on_buttonMissingVideoHelp_clicked();
+	void on_buttonMissingFilesHelp_clicked();
+	void on_buttonMissingCampaignsHelp_clicked();
+
+	void on_buttonPresetExport_clicked();
+
+	void on_buttonPresetImport_clicked();
+
+	void on_buttonPresetNew_clicked();
+
+	void on_buttonPresetDelete_clicked();
+
+	void on_comboBoxModPresets_currentTextChanged(const QString &arg1);
+
+	void on_buttonPresetRename_clicked();
+
+private:
+	Ui::StartGameTab * ui;
+};

+ 857 - 0
launcher/startGame/StartGameTab.ui

@@ -0,0 +1,857 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>StartGameTab</class>
+ <widget class="QWidget" name="StartGameTab">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>757</width>
+    <height>372</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string notr="true"/>
+  </property>
+  <layout class="QGridLayout" name="gridLayout_3" columnstretch="1,1,1">
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item row="0" column="0">
+    <widget class="QLabel" name="labelTitleDataFiles">
+     <property name="font">
+      <font>
+       <bold>true</bold>
+      </font>
+     </property>
+     <property name="text">
+      <string>Game Data Files</string>
+     </property>
+     <property name="alignment">
+      <set>Qt::AlignCenter</set>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="1">
+    <widget class="QLabel" name="labelTitleModPreset">
+     <property name="font">
+      <font>
+       <bold>true</bold>
+      </font>
+     </property>
+     <property name="text">
+      <string>Mod Preset</string>
+     </property>
+     <property name="alignment">
+      <set>Qt::AlignCenter</set>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="2">
+    <widget class="QLabel" name="labelTitleEngine">
+     <property name="font">
+      <font>
+       <bold>true</bold>
+      </font>
+     </property>
+     <property name="text">
+      <string/>
+     </property>
+     <property name="alignment">
+      <set>Qt::AlignCenter</set>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="0">
+    <widget class="QScrollArea" name="scrollArea_2">
+     <property name="frameShadow">
+      <enum>QFrame::Sunken</enum>
+     </property>
+     <property name="horizontalScrollBarPolicy">
+      <enum>Qt::ScrollBarAlwaysOff</enum>
+     </property>
+     <property name="widgetResizable">
+      <bool>true</bool>
+     </property>
+     <widget class="QWidget" name="scrollAreaWidgetContents_2">
+      <property name="geometry">
+       <rect>
+        <x>0</x>
+        <y>0</y>
+        <width>246</width>
+        <height>350</height>
+       </rect>
+      </property>
+      <layout class="QGridLayout" name="gridLayout_2">
+       <item row="7" column="0">
+        <widget class="QLabel" name="labelMissingFiles">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>16777215</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>Unsupported or corrupted game data detected!</string>
+         </property>
+         <property name="wordWrap">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+       <item row="0" column="1">
+        <widget class="QPushButton" name="buttonHelpImportFiles">
+         <property name="minimumSize">
+          <size>
+           <width>40</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>40</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>?</string>
+         </property>
+        </widget>
+       </item>
+       <item row="7" column="1">
+        <widget class="QPushButton" name="buttonMissingFilesHelp">
+         <property name="minimumSize">
+          <size>
+           <width>40</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>40</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>?</string>
+         </property>
+        </widget>
+       </item>
+       <item row="4" column="0">
+        <widget class="QLabel" name="labelChronicles">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>16777215</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string/>
+         </property>
+         <property name="wordWrap">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+       <item row="3" column="1">
+        <widget class="QPushButton" name="buttonUpdateModsHelp">
+         <property name="minimumSize">
+          <size>
+           <width>40</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>40</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>?</string>
+         </property>
+        </widget>
+       </item>
+       <item row="1" column="1">
+        <widget class="QPushButton" name="buttonInstallTranslationHelp">
+         <property name="minimumSize">
+          <size>
+           <width>40</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>40</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>?</string>
+         </property>
+        </widget>
+       </item>
+       <item row="1" column="0">
+        <widget class="QPushButton" name="buttonInstallTranslation">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>16777215</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>Install Translation</string>
+         </property>
+        </widget>
+       </item>
+       <item row="8" column="0">
+        <widget class="QLabel" name="labelMissingCampaigns">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>16777215</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>Armaggedon's Blade campaigns are missing!</string>
+         </property>
+         <property name="wordWrap">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+       <item row="9" column="0">
+        <spacer name="verticalSpacer_2">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>0</width>
+           <height>0</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+       <item row="6" column="0">
+        <widget class="QLabel" name="labelMissingVideo">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>16777215</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>No video files detected!</string>
+         </property>
+         <property name="wordWrap">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+       <item row="0" column="0">
+        <widget class="QPushButton" name="buttonImportFiles">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>16777215</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>Import files</string>
+         </property>
+        </widget>
+       </item>
+       <item row="3" column="0">
+        <widget class="QPushButton" name="buttonUpdateMods">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>16777215</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string/>
+         </property>
+        </widget>
+       </item>
+       <item row="5" column="1">
+        <widget class="QPushButton" name="buttonMissingSoundtrackHelp">
+         <property name="minimumSize">
+          <size>
+           <width>40</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>40</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>?</string>
+         </property>
+        </widget>
+       </item>
+       <item row="8" column="1">
+        <widget class="QPushButton" name="buttonMissingCampaignsHelp">
+         <property name="minimumSize">
+          <size>
+           <width>40</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>40</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>?</string>
+         </property>
+        </widget>
+       </item>
+       <item row="5" column="0">
+        <widget class="QLabel" name="labelMissingSoundtrack">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>16777215</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>No soundtrack detected!</string>
+         </property>
+         <property name="wordWrap">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+       <item row="6" column="1">
+        <widget class="QPushButton" name="buttonMissingVideoHelp">
+         <property name="minimumSize">
+          <size>
+           <width>40</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>40</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>?</string>
+         </property>
+        </widget>
+       </item>
+       <item row="2" column="0">
+        <widget class="QPushButton" name="buttonActivateTranslation">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>16777215</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>Activate Translation</string>
+         </property>
+        </widget>
+       </item>
+       <item row="4" column="1">
+        <widget class="QPushButton" name="buttonChroniclesHelp">
+         <property name="minimumSize">
+          <size>
+           <width>40</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>40</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>?</string>
+         </property>
+        </widget>
+       </item>
+       <item row="2" column="1">
+        <widget class="QPushButton" name="buttonActivateTranslationHelp">
+         <property name="minimumSize">
+          <size>
+           <width>40</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>40</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>?</string>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+   <item row="1" column="1">
+    <widget class="QScrollArea" name="scrollArea">
+     <property name="horizontalScrollBarPolicy">
+      <enum>Qt::ScrollBarAlwaysOff</enum>
+     </property>
+     <property name="widgetResizable">
+      <bool>true</bool>
+     </property>
+     <widget class="QWidget" name="scrollAreaWidgetContents">
+      <property name="geometry">
+       <rect>
+        <x>0</x>
+        <y>0</y>
+        <width>247</width>
+        <height>350</height>
+       </rect>
+      </property>
+      <layout class="QGridLayout" name="gridLayout" rowstretch="1,0,0,0,0,0,0,0,0,0">
+       <item row="2" column="0">
+        <widget class="QPushButton" name="buttonPresetExport">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>16777215</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>Export to Clipboard</string>
+         </property>
+        </widget>
+       </item>
+       <item row="4" column="0">
+        <widget class="QPushButton" name="buttonPresetNew">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>16777215</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>Create New Preset</string>
+         </property>
+        </widget>
+       </item>
+       <item row="0" column="0">
+        <widget class="QComboBox" name="comboBoxModPresets">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>16777215</width>
+           <height>30</height>
+          </size>
+         </property>
+        </widget>
+       </item>
+       <item row="7" column="0">
+        <widget class="QPushButton" name="buttonPresetDelete">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>16777215</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>Delete Current Preset</string>
+         </property>
+        </widget>
+       </item>
+       <item row="8" column="0">
+        <spacer name="verticalSpacer">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>0</width>
+           <height>0</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+       <item row="3" column="0">
+        <widget class="QPushButton" name="buttonPresetImport">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>16777215</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>Import from Clipboard</string>
+         </property>
+        </widget>
+       </item>
+       <item row="6" column="0">
+        <widget class="QPushButton" name="buttonPresetRename">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>16777215</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>Rename Current Preset</string>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+   <item row="1" column="2">
+    <widget class="QScrollArea" name="scrollArea_3">
+     <property name="horizontalScrollBarPolicy">
+      <enum>Qt::ScrollBarAlwaysOff</enum>
+     </property>
+     <property name="widgetResizable">
+      <bool>true</bool>
+     </property>
+     <widget class="QWidget" name="scrollAreaWidgetContents_3">
+      <property name="geometry">
+       <rect>
+        <x>0</x>
+        <y>0</y>
+        <width>272</width>
+        <height>350</height>
+       </rect>
+      </property>
+      <layout class="QVBoxLayout" name="verticalLayout">
+       <item>
+        <widget class="QLabel" name="labelUpdateNotFound">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>16777215</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>You are using the latest version</string>
+         </property>
+         <property name="wordWrap">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLabel" name="labelUpdateAvailable">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>16777215</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string/>
+         </property>
+         <property name="wordWrap">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPushButton" name="buttonUpdateCheck">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>16777215</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>Check For Updates</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPushButton" name="buttonOpenDownloads">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>16777215</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>Go to Downloads Page</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QPushButton" name="buttonOpenChangelog">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="maximumSize">
+          <size>
+           <width>16777215</width>
+           <height>30</height>
+          </size>
+         </property>
+         <property name="text">
+          <string>Go to Changelog Page</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <spacer name="verticalSpacer_3">
+         <property name="orientation">
+          <enum>Qt::Vertical</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>0</width>
+           <height>0</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout">
+         <item>
+          <widget class="QToolButton" name="buttonGameResume">
+           <property name="sizePolicy">
+            <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+             <horstretch>0</horstretch>
+             <verstretch>0</verstretch>
+            </sizepolicy>
+           </property>
+           <property name="minimumSize">
+            <size>
+             <width>80</width>
+             <height>80</height>
+            </size>
+           </property>
+           <property name="text">
+            <string>Resume</string>
+           </property>
+           <property name="iconSize">
+            <size>
+             <width>64</width>
+             <height>64</height>
+            </size>
+           </property>
+           <property name="toolButtonStyle">
+            <enum>Qt::ToolButtonTextUnderIcon</enum>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QToolButton" name="buttonGameEditor">
+           <property name="sizePolicy">
+            <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+             <horstretch>0</horstretch>
+             <verstretch>0</verstretch>
+            </sizepolicy>
+           </property>
+           <property name="minimumSize">
+            <size>
+             <width>80</width>
+             <height>80</height>
+            </size>
+           </property>
+           <property name="text">
+            <string>Editor</string>
+           </property>
+           <property name="iconSize">
+            <size>
+             <width>64</width>
+             <height>64</height>
+            </size>
+           </property>
+           <property name="toolButtonStyle">
+            <enum>Qt::ToolButtonTextUnderIcon</enum>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QToolButton" name="buttonGameStart">
+           <property name="sizePolicy">
+            <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+             <horstretch>0</horstretch>
+             <verstretch>0</verstretch>
+            </sizepolicy>
+           </property>
+           <property name="minimumSize">
+            <size>
+             <width>80</width>
+             <height>80</height>
+            </size>
+           </property>
+           <property name="font">
+            <font>
+             <bold>true</bold>
+            </font>
+           </property>
+           <property name="text">
+            <string>Play</string>
+           </property>
+           <property name="iconSize">
+            <size>
+             <width>64</width>
+             <height>64</height>
+            </size>
+           </property>
+           <property name="toolButtonStyle">
+            <enum>Qt::ToolButtonTextUnderIcon</enum>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>

ファイルの差分が大きいため隠しています
+ 592 - 137
launcher/translation/chinese.ts


ファイルの差分が大きいため隠しています
+ 554 - 236
launcher/translation/czech.ts


ファイルの差分が大きいため隠しています
+ 487 - 210
launcher/translation/english.ts


ファイルの差分が大きいため隠しています
+ 532 - 149
launcher/translation/french.ts


ファイルの差分が大きいため隠しています
+ 349 - 177
launcher/translation/german.ts


ファイルの差分が大きいため隠しています
+ 317 - 151
launcher/translation/polish.ts


ファイルの差分が大きいため隠しています
+ 551 - 241
launcher/translation/portuguese.ts


ファイルの差分が大きいため隠しています
+ 503 - 197
launcher/translation/russian.ts


ファイルの差分が大きいため隠しています
+ 504 - 182
launcher/translation/spanish.ts


ファイルの差分が大きいため隠しています
+ 541 - 120
launcher/translation/swedish.ts


ファイルの差分が大きいため隠しています
+ 330 - 179
launcher/translation/ukrainian.ts


ファイルの差分が大きいため隠しています
+ 501 - 185
launcher/translation/vietnamese.ts


+ 1 - 1
launcher/updatedialog_moc.cpp

@@ -98,7 +98,7 @@ void UpdateDialog::loadFromJson(const JsonNode & node)
 	   node["changeLog"].getType() != JsonNode::JsonType::DATA_STRING ||
 	   node["changeLog"].getType() != JsonNode::JsonType::DATA_STRING ||
 	   node["downloadLinks"].getType() != JsonNode::JsonType::DATA_STRUCT) //we need at least one link - other are optional
 	   node["downloadLinks"].getType() != JsonNode::JsonType::DATA_STRUCT) //we need at least one link - other are optional
 	{
 	{
-		ui->plainTextEdit->setPlainText(tr("Cannot read JSON from url or incorrect JSON data"));
+		ui->plainTextEdit->setPlainText(tr("Cannot read JSON from URL or incorrect JSON data"));
 		return;
 		return;
 	}
 	}
 	
 	

+ 9 - 3
lib/CCreatureHandler.cpp

@@ -454,8 +454,14 @@ void CCreatureHandler::loadCommanders()
 
 
 	for (auto ability : config["abilityRequirements"].Vector())
 	for (auto ability : config["abilityRequirements"].Vector())
 	{
 	{
-		std::pair <std::shared_ptr<Bonus>, std::pair <ui8, ui8> > a;
-		a.first = JsonUtils::parseBonus (ability["ability"].Vector());
+		std::pair <std::vector<std::shared_ptr<Bonus> >, std::pair <ui8, ui8> > a;
+		JsonVector & abilities = ability["ability"].Vector();
+		a.first = std::vector<std::shared_ptr<Bonus> >();
+		if (abilities[0].isVector()) 
+			for (int i = 0; i < abilities.size(); i++) 
+				a.first.push_back(JsonUtils::parseBonus(abilities[i].Vector()));
+		else 
+			a.first.push_back(JsonUtils::parseBonus(ability["ability"].Vector()));
 		a.second.first =  static_cast<ui8>(ability["skills"].Vector()[0].Float());
 		a.second.first =  static_cast<ui8>(ability["skills"].Vector()[0].Float());
 		a.second.second = static_cast<ui8>(ability["skills"].Vector()[1].Float());
 		a.second.second = static_cast<ui8>(ability["skills"].Vector()[1].Float());
 		skillRequirements.push_back (a);
 		skillRequirements.push_back (a);
@@ -1312,7 +1318,7 @@ int CCreatureHandler::stringToNumber(std::string & s) const
 CCreatureHandler::~CCreatureHandler()
 CCreatureHandler::~CCreatureHandler()
 {
 {
 	for(auto & p : skillRequirements)
 	for(auto & p : skillRequirements)
-		p.first = nullptr;
+		p.first.clear();
 }
 }
 
 
 CreatureID CCreatureHandler::pickRandomMonster(vstd::RNG & rand, int tier) const
 CreatureID CCreatureHandler::pickRandomMonster(vstd::RNG & rand, int tier) const

+ 1 - 1
lib/CCreatureHandler.h

@@ -224,7 +224,7 @@ public:
 	//Commanders
 	//Commanders
 	BonusList commanderLevelPremy; //bonus values added with each level-up
 	BonusList commanderLevelPremy; //bonus values added with each level-up
 	std::vector< std::vector <ui8> > skillLevels; //how much of a bonus will be given to commander with every level. SPELL_POWER also gives CASTS and RESISTANCE
 	std::vector< std::vector <ui8> > skillLevels; //how much of a bonus will be given to commander with every level. SPELL_POWER also gives CASTS and RESISTANCE
-	std::vector <std::pair <std::shared_ptr<Bonus>, std::pair <ui8, ui8> > > skillRequirements; // first - Bonus, second - which two skills are needed to use it
+	std::vector <std::pair <std::vector<std::shared_ptr<Bonus> >, std::pair <ui8, ui8> > > skillRequirements; // first - Bonus, second - which two skills are needed to use it
 
 
 	CreatureID pickRandomMonster(vstd::RNG & rand, int tier = -1) const; //tier <1 - CREATURES_PER_TOWN> or -1 for any
 	CreatureID pickRandomMonster(vstd::RNG & rand, int tier = -1) const; //tier <1 - CREATURES_PER_TOWN> or -1 for any
 
 

+ 7 - 2
lib/gameState/CGameState.cpp

@@ -1400,8 +1400,10 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio
 		case EventCondition::TRANSPORT:
 		case EventCondition::TRANSPORT:
 		{
 		{
 			const auto * t = getTown(condition.objectID);
 			const auto * t = getTown(condition.objectID);
-			return (t->visitingHero && t->visitingHero->getOwner() == player && t->visitingHero->hasArt(condition.objectType.as<ArtifactID>())) ||
-				   (t->garrisonHero && t->garrisonHero->getOwner() == player && t->garrisonHero->hasArt(condition.objectType.as<ArtifactID>()));
+			bool garrisonedWon = t->garrisonHero && t->garrisonHero->getOwner() == player && t->garrisonHero->hasArt(condition.objectType.as<ArtifactID>());
+			bool visitingWon = t->visitingHero && t->visitingHero->getOwner() == player && t->visitingHero->hasArt(condition.objectType.as<ArtifactID>());
+
+			return garrisonedWon || visitingWon;
 		}
 		}
 		case EventCondition::DAYS_PASSED:
 		case EventCondition::DAYS_PASSED:
 		{
 		{
@@ -1436,6 +1438,9 @@ PlayerColor CGameState::checkForStandardWin() const
 	TeamID winnerTeam = TeamID::NO_TEAM;
 	TeamID winnerTeam = TeamID::NO_TEAM;
 	for(const auto & elem : players)
 	for(const auto & elem : players)
 	{
 	{
+		if(elem.second.status == EPlayerStatus::WINNER)
+			return elem.second.color;
+
 		if(elem.second.status == EPlayerStatus::INGAME && elem.first.isValidPlayer())
 		if(elem.second.status == EPlayerStatus::INGAME && elem.first.isValidPlayer())
 		{
 		{
 			if(supposedWinner == PlayerColor::NEUTRAL)
 			if(supposedWinner == PlayerColor::NEUTRAL)

+ 2 - 1
lib/mapObjects/CGHeroInstance.cpp

@@ -1923,7 +1923,8 @@ int CGHeroInstance::getBasePrimarySkillValue(PrimarySkill which) const
 {
 {
 	std::string cachingStr = "type_PRIMARY_SKILL_base_" + std::to_string(static_cast<int>(which));
 	std::string cachingStr = "type_PRIMARY_SKILL_base_" + std::to_string(static_cast<int>(which));
 	auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(which)).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL));
 	auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(which)).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL));
-	return valOfBonuses(selector, cachingStr);
+	auto minSkillValue = VLC->engineSettings()->getVector(EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS)[which.getNum()];
+	return std::max(valOfBonuses(selector, cachingStr), minSkillValue);
 }
 }
 
 
 VCMI_LIB_NAMESPACE_END
 VCMI_LIB_NAMESPACE_END

+ 1 - 2
lib/modding/ModDescription.cpp

@@ -120,8 +120,7 @@ const JsonNode & ModDescription::getLocalizedValue(const std::string & keyName)
 
 
 const JsonNode & ModDescription::getValue(const std::string & keyName) const
 const JsonNode & ModDescription::getValue(const std::string & keyName) const
 {
 {
-	const JsonNode & localValue = getLocalValue(keyName);
-	if (localValue.isNull())
+	if (!isInstalled() || isUpdateAvailable())
 		return getRepositoryValue(keyName);
 		return getRepositoryValue(keyName);
 	else
 	else
 		return getLocalValue(keyName);
 		return getLocalValue(keyName);

+ 95 - 2
lib/modding/ModManager.cpp

@@ -163,7 +163,7 @@ ModsPresetState::ModsPresetState()
 		CResourceHandler::get("local")->createResource(settingsPath.getOriginalName() + ".json");
 		CResourceHandler::get("local")->createResource(settingsPath.getOriginalName() + ".json");
 	}
 	}
 
 
-	if(modConfig["presets"].isNull())
+	if(modConfig["presets"].isNull() || modConfig["presets"].Struct().empty())
 	{
 	{
 		modConfig["activePreset"] = JsonNode("default");
 		modConfig["activePreset"] = JsonNode("default");
 		if(modConfig["activeMods"].isNull())
 		if(modConfig["activeMods"].isNull())
@@ -171,6 +171,10 @@ ModsPresetState::ModsPresetState()
 		else
 		else
 			importInitialPreset(); // 1.5 format import
 			importInitialPreset(); // 1.5 format import
 	}
 	}
+
+	auto allPresets = getAllPresets();
+	if (!vstd::contains(allPresets, modConfig["activePreset"].String()))
+		modConfig["activePreset"] = JsonNode(allPresets.front());
 }
 }
 
 
 void ModsPresetState::createInitialPreset()
 void ModsPresetState::createInitialPreset()
@@ -326,6 +330,61 @@ void ModsPresetState::saveConfigurationState() const
 	file << modConfig.toCompactString();
 	file << modConfig.toCompactString();
 }
 }
 
 
+void ModsPresetState::createNewPreset(const std::string & presetName)
+{
+	if (modConfig["presets"][presetName].isNull())
+		modConfig["presets"][presetName]["mods"].Vector().emplace_back("vcmi");
+}
+
+void ModsPresetState::deletePreset(const std::string & presetName)
+{
+	if (modConfig["presets"].Struct().size() < 2)
+		throw std::runtime_error("Unable to delete last preset!");
+
+	modConfig["presets"].Struct().erase(presetName);
+}
+
+void ModsPresetState::activatePreset(const std::string & presetName)
+{
+	if (modConfig["presets"].Struct().count(presetName) == 0)
+		throw std::runtime_error("Unable to activate non-exinsting preset!");
+
+	modConfig["activePreset"].String() = presetName;
+}
+
+void ModsPresetState::renamePreset(const std::string & oldPresetName, const std::string & newPresetName)
+{
+	if (oldPresetName == newPresetName)
+		throw std::runtime_error("Unable to rename preset to the same name!");
+
+	if (modConfig["presets"].Struct().count(oldPresetName) == 0)
+		throw std::runtime_error("Unable to rename non-existing last preset!");
+
+	if (modConfig["presets"].Struct().count(newPresetName) != 0)
+		throw std::runtime_error("Unable to rename preset - preset with such name already exists!");
+
+	modConfig["presets"][newPresetName] = modConfig["presets"][oldPresetName];
+	modConfig["presets"].Struct().erase(oldPresetName);
+
+	if (modConfig["activePreset"].String() == oldPresetName)
+		modConfig["activePreset"].String() = newPresetName;
+}
+
+std::vector<std::string> ModsPresetState::getAllPresets() const
+{
+	std::vector<std::string> presets;
+
+	for (const auto & preset : modConfig["presets"].Struct())
+		presets.push_back(preset.first);
+
+	return presets;
+}
+
+std::string ModsPresetState::getActivePreset() const
+{
+	return modConfig["activePreset"].String();
+}
+
 ModsStorage::ModsStorage(const std::vector<TModID> & modsToLoad, const JsonNode & repositoryList)
 ModsStorage::ModsStorage(const std::vector<TModID> & modsToLoad, const JsonNode & repositoryList)
 {
 {
 	JsonNode coreModConfig(JsonPath::builtin("config/gameConfig.json"));
 	JsonNode coreModConfig(JsonPath::builtin("config/gameConfig.json"));
@@ -595,7 +654,7 @@ void ModManager::updatePreset(const ModDependenciesResolver & testResolver)
 	for (const auto & modID : newBrokenMods)
 	for (const auto & modID : newBrokenMods)
 	{
 	{
 		const auto & mod = getModDescription(modID);
 		const auto & mod = getModDescription(modID);
-		if (vstd::contains(newActiveMods, mod.getTopParentID()))
+		if (mod.getTopParentID().empty() || vstd::contains(newActiveMods, mod.getTopParentID()))
 			modsPreset->setModActive(modID, false);
 			modsPreset->setModActive(modID, false);
 	}
 	}
 
 
@@ -703,4 +762,38 @@ void ModDependenciesResolver::tryAddMods(TModList modsToResolve, const ModsStora
 	brokenMods.insert(brokenMods.end(), modsToResolve.begin(), modsToResolve.end());
 	brokenMods.insert(brokenMods.end(), modsToResolve.begin(), modsToResolve.end());
 }
 }
 
 
+void ModManager::createNewPreset(const std::string & presetName)
+{
+	modsPreset->createNewPreset(presetName);
+	modsPreset->saveConfigurationState();
+}
+
+void ModManager::deletePreset(const std::string & presetName)
+{
+	modsPreset->deletePreset(presetName);
+	modsPreset->saveConfigurationState();
+}
+
+void ModManager::activatePreset(const std::string & presetName)
+{
+	modsPreset->activatePreset(presetName);
+	modsPreset->saveConfigurationState();
+}
+
+void ModManager::renamePreset(const std::string & oldPresetName, const std::string & newPresetName)
+{
+	modsPreset->renamePreset(oldPresetName, newPresetName);
+	modsPreset->saveConfigurationState();
+}
+
+std::vector<std::string> ModManager::getAllPresets() const
+{
+	return modsPreset->getAllPresets();
+}
+
+std::string ModManager::getActivePreset() const
+{
+	return modsPreset->getActivePreset();
+}
+
 VCMI_LIB_NAMESPACE_END
 VCMI_LIB_NAMESPACE_END

+ 16 - 0
lib/modding/ModManager.h

@@ -50,6 +50,14 @@ class ModsPresetState : boost::noncopyable
 public:
 public:
 	ModsPresetState();
 	ModsPresetState();
 
 
+	void createNewPreset(const std::string & presetName);
+	void deletePreset(const std::string & presetName);
+	void activatePreset(const std::string & presetName);
+	void renamePreset(const std::string & oldPresetName, const std::string & newPresetName);
+
+	std::vector<std::string> getAllPresets() const;
+	std::string getActivePreset() const;
+
 	void setModActive(const TModID & modName, bool isActive);
 	void setModActive(const TModID & modName, bool isActive);
 
 
 	void addRootMod(const TModID & modName);
 	void addRootMod(const TModID & modName);
@@ -139,6 +147,14 @@ public:
 
 
 	void tryEnableMods(const TModList & modList);
 	void tryEnableMods(const TModList & modList);
 	void tryDisableMod(const TModID & modName);
 	void tryDisableMod(const TModID & modName);
+
+	void createNewPreset(const std::string & presetName);
+	void deletePreset(const std::string & presetName);
+	void activatePreset(const std::string & presetName);
+	void renamePreset(const std::string & oldPresetName, const std::string & newPresetName);
+
+	std::vector<std::string> getAllPresets() const;
+	std::string getActivePreset() const;
 };
 };
 
 
 VCMI_LIB_NAMESPACE_END
 VCMI_LIB_NAMESPACE_END

+ 4 - 0
lib/network/NetworkConnection.cpp

@@ -199,7 +199,11 @@ void NetworkConnection::close()
 {
 {
 	boost::system::error_code ec;
 	boost::system::error_code ec;
 	socket->close(ec);
 	socket->close(ec);
+#if BOOST_VERSION >= 108700
+	timer->cancel();
+#else
 	timer->cancel(ec);
 	timer->cancel(ec);
+#endif
 
 
 	//NOTE: ignoring error code, intended
 	//NOTE: ignoring error code, intended
 }
 }

+ 4 - 0
lib/network/NetworkDefines.h

@@ -15,7 +15,11 @@
 
 
 VCMI_LIB_NAMESPACE_BEGIN
 VCMI_LIB_NAMESPACE_BEGIN
 
 
+#if BOOST_VERSION >= 108700
+using NetworkContext = boost::asio::io_context;
+#else
 using NetworkContext = boost::asio::io_service;
 using NetworkContext = boost::asio::io_service;
+#endif
 using NetworkSocket = boost::asio::ip::tcp::socket;
 using NetworkSocket = boost::asio::ip::tcp::socket;
 using NetworkAcceptor = boost::asio::ip::tcp::acceptor;
 using NetworkAcceptor = boost::asio::ip::tcp::acceptor;
 using NetworkBuffer = boost::asio::streambuf;
 using NetworkBuffer = boost::asio::streambuf;

+ 11 - 12
lib/networkPacks/NetPacksLib.cpp

@@ -1193,6 +1193,17 @@ void RemoveObject::applyGs(CGameState *gs)
 	if (initiator.isValidPlayer())
 	if (initiator.isValidPlayer())
 		gs->getPlayerState(initiator)->destroyedObjects.insert(objectID);
 		gs->getPlayerState(initiator)->destroyedObjects.insert(objectID);
 
 
+	if(obj->getOwner().isValidPlayer())
+	{
+		gs->getPlayerState(obj->getOwner())->removeOwnedObject(obj); //object removed via map event or hero got beaten
+
+		FlaggableMapObject* flaggableObject = dynamic_cast<FlaggableMapObject*>(obj);
+		if(flaggableObject)
+		{
+			flaggableObject->markAsDeleted();
+		}
+	}
+
 	if(obj->ID == Obj::HERO) //remove beaten hero
 	if(obj->ID == Obj::HERO) //remove beaten hero
 	{
 	{
 		auto * beatenHero = dynamic_cast<CGHeroInstance *>(obj);
 		auto * beatenHero = dynamic_cast<CGHeroInstance *>(obj);
@@ -1251,18 +1262,6 @@ void RemoveObject::applyGs(CGameState *gs)
 		}
 		}
 	}
 	}
 
 
-	if(obj->getOwner().isValidPlayer())
-	{
-		gs->getPlayerState(obj->getOwner())->removeOwnedObject(obj); //object removed via map event or hero got beaten
-
-		FlaggableMapObject* flaggableObject = dynamic_cast<FlaggableMapObject*>(obj);
-		if(flaggableObject)
-		{
-			flaggableObject->markAsDeleted();
-		}
-	}
-
-
 	gs->map->instanceNames.erase(obj->instanceName);
 	gs->map->instanceNames.erase(obj->instanceName);
 	gs->map->objects[objectID.getNum()].dellNull();
 	gs->map->objects[objectID.getNum()].dellNull();
 	gs->map->calculateGuardingGreaturePositions();//FIXME: excessive, update only affected tiles
 	gs->map->calculateGuardingGreaturePositions();//FIXME: excessive, update only affected tiles

BIN
mapeditor/icons/document-open-recent.png


この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません