Browse Source

Merge pull request #5161 from vcmi/beta

Merge beta -> master
Ivan Savenko 10 months ago
parent
commit
b91be7769c
100 changed files with 2666 additions and 4567 deletions
  1. 1 1
      .github/workflows/github.yml
  2. 27 6
      AI/Nullkiller/AIGateway.cpp
  3. 6 7
      AI/Nullkiller/Analyzers/BuildAnalyzer.cpp
  4. 2 2
      AI/Nullkiller/Analyzers/HeroManager.cpp
  5. 4 0
      AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp
  6. 14 3
      AI/Nullkiller/Engine/Nullkiller.cpp
  7. 2 0
      AI/Nullkiller/Engine/Nullkiller.h
  8. 4 2
      AI/Nullkiller/Engine/Settings.cpp
  9. 4 2
      AI/Nullkiller/Engine/Settings.h
  10. 0 2
      AI/Nullkiller/Pathfinding/AINodeStorage.h
  11. 2 2
      AI/Nullkiller/Pathfinding/AIPathfinder.cpp
  12. 1 1
      AI/VCAI/BuildingManager.cpp
  13. 2 1
      AI/VCAI/Goals/GatherTroops.cpp
  14. 22 4
      AI/VCAI/VCAI.cpp
  15. 1 1
      CMakePresets.json
  16. 41 0
      ChangeLog.md
  17. 3 0
      Global.h
  18. BIN
      Mods/vcmi/Content/Sprites/portalBidirectional.png
  19. BIN
      Mods/vcmi/Content/Sprites/portalEntrance.png
  20. BIN
      Mods/vcmi/Content/Sprites/portalExit.png
  21. BIN
      Mods/vcmi/Content/Sprites2x/portalBidirectional.png
  22. BIN
      Mods/vcmi/Content/Sprites2x/portalEntrance.png
  23. BIN
      Mods/vcmi/Content/Sprites2x/portalExit.png
  24. 43 23
      Mods/vcmi/Content/config/czech.json
  25. 23 3
      Mods/vcmi/Content/config/english.json
  26. 25 5
      Mods/vcmi/Content/config/german.json
  27. 3 3
      Mods/vcmi/Content/config/polish.json
  28. 30 30
      Mods/vcmi/Content/config/portuguese.json
  29. 25 3
      Mods/vcmi/Content/config/swedish.json
  30. 20 2
      Mods/vcmi/Content/config/ukrainian.json
  31. 2 0
      android/AndroidManifest.xml
  32. 4 2
      android/vcmi-app/build.gradle
  33. 1 0
      android/vcmi-app/src/main/res/values-de/strings.xml
  34. 1 0
      android/vcmi-app/src/main/res/values/strings.xml
  35. 13 0
      android/vcmi-app/src/main/res/xml/shortcuts.xml
  36. 13 0
      android/vcmi-app/src/main/res/xml/shortcutsdaily.xml
  37. 13 0
      android/vcmi-app/src/main/res/xml/shortcutsdebug.xml
  38. 1 1
      client/CPlayerInterface.h
  39. 7 5
      client/adventureMap/CMinimap.cpp
  40. 2 2
      client/adventureMap/CMinimap.h
  41. 1 1
      client/battle/BattleActionsController.cpp
  42. 9 3
      client/mainmenu/CHighScoreScreen.cpp
  43. 1 0
      client/mainmenu/CHighScoreScreen.h
  44. 1 1
      client/mapView/MapRenderer.cpp
  45. 21 5
      client/render/CDefFile.cpp
  46. 1 0
      client/render/CDefFile.h
  47. 10 3
      client/renderSDL/RenderHandler.cpp
  48. 14 7
      client/renderSDL/SDL_Extensions.cpp
  49. 13 0
      client/renderSDL/ScreenHandler.cpp
  50. 3 2
      client/widgets/CGarrisonInt.cpp
  51. 5 5
      client/windows/CCastleInterface.cpp
  52. 1 1
      client/windows/CCastleInterface.h
  53. 14 6
      client/windows/CCreatureWindow.cpp
  54. 1 1
      client/windows/CCreatureWindow.h
  55. 1 1
      client/windows/CHeroWindow.cpp
  56. 28 22
      client/windows/GUIClasses.cpp
  57. 2 1
      client/windows/GUIClasses.h
  58. 85 0
      client/windows/InfoWindows.cpp
  59. 23 0
      client/windows/InfoWindows.h
  60. 1 1
      cmake_modules/VersionDefinition.cmake
  61. 29 24
      config/ai/nkai/nkai-settings.json
  62. 12 0
      config/bonuses.json
  63. 6 0
      debian/changelog
  64. 1 0
      docs/Readme.md
  65. 2 1
      include/vcmi/Creature.h
  66. 13 9
      include/vstd/CLoggerBase.h
  67. 1 0
      launcher/eu.vcmi.VCMI.metainfo.xml
  68. 8 6
      launcher/firstLaunch/firstlaunch_moc.cpp
  69. 1 1
      launcher/firstLaunch/firstlaunch_moc.h
  70. 15 4
      launcher/mainwindow_moc.cpp
  71. 1 1
      launcher/mainwindow_moc.h
  72. 2 2
      launcher/modManager/cdownloadmanager_moc.cpp
  73. 34 5
      launcher/modManager/cmodlistview_moc.cpp
  74. 4 4
      launcher/modManager/cmodlistview_moc.h
  75. 1 1
      launcher/modManager/modstatecontroller.cpp
  76. 1 1
      launcher/modManager/modstateitemmodel_moc.cpp
  77. 14 1
      launcher/modManager/modstatemodel.cpp
  78. 3 0
      launcher/modManager/modstatemodel.h
  79. 92 45
      launcher/startGame/StartGameTab.cpp
  80. 1 6
      launcher/startGame/StartGameTab.h
  81. 104 436
      launcher/translation/chinese.ts
  82. 167 189
      launcher/translation/czech.ts
  83. 176 193
      launcher/translation/english.ts
  84. 114 434
      launcher/translation/french.ts
  85. 114 454
      launcher/translation/german.ts
  86. 105 436
      launcher/translation/polish.ts
  87. 177 295
      launcher/translation/portuguese.ts
  88. 178 276
      launcher/translation/russian.ts
  89. 149 322
      launcher/translation/spanish.ts
  90. 170 451
      launcher/translation/swedish.ts
  91. 114 430
      launcher/translation/ukrainian.ts
  92. 150 315
      launcher/translation/vietnamese.ts
  93. 33 36
      lib/BasicTypes.cpp
  94. 10 1
      lib/CBonusTypeHandler.cpp
  95. 2 0
      lib/CConsoleHandler.cpp
  96. 1 1
      lib/CGameInfoCallback.cpp
  97. 2 2
      lib/CGameInfoCallback.h
  98. 1 0
      lib/CMakeLists.txt
  99. 48 8
      lib/CPlayerState.cpp
  100. 13 4
      lib/CPlayerState.h

+ 1 - 1
.github/workflows/github.yml

@@ -248,7 +248,7 @@ jobs:
             cmake -DENABLE_CCACHE:BOOL=ON -DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14 --preset ${{ matrix.preset }}
         elif [[ (${{matrix.preset}} == android-conan-ninja-release) && (${{github.ref}} != 'refs/heads/master') ]]
         then
-            cmake -DENABLE_CCACHE:BOOL=ON -DANDROID_GRADLE_PROPERTIES="applicationIdSuffix=.daily;signingConfig=dailySigning;applicationLabel=VCMI daily" --preset ${{ matrix.preset }}
+            cmake -DENABLE_CCACHE:BOOL=ON -DANDROID_GRADLE_PROPERTIES="applicationIdSuffix=.daily;signingConfig=dailySigning;applicationLabel=VCMI daily;applicationVariant=daily" --preset ${{ matrix.preset }}
         elif [[ ${{startsWith(matrix.platform, 'msvc') }} ]]
         then
             cmake --preset ${{ matrix.preset }}

+ 27 - 6
AI/Nullkiller/AIGateway.cpp

@@ -19,6 +19,7 @@
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/IGameSettings.h"
 #include "../../lib/gameState/CGameState.h"
+#include "../../lib/gameState/UpgradeInfo.h"
 #include "../../lib/serializer/CTypeList.h"
 #include "../../lib/networkPacks/PacksForClient.h"
 #include "../../lib/networkPacks/PacksForClientBattle.h"
@@ -96,6 +97,8 @@ void AIGateway::heroMoved(const TryMoveHero & details, bool verbose)
 	if(!hero)
 		validateObject(details.id); //enemy hero may have left visible area
 
+	nullkiller->invalidatePathfinderData();
+
 	const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));
 	const int3 to   = hero ? hero->convertToVisitablePos(details.end)   : (details.end   - int3(0,1,0));
 
@@ -357,6 +360,7 @@ void AIGateway::newObject(const CGObjectInstance * obj)
 {
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;
+	nullkiller->invalidatePathfinderData();
 	if(obj->isVisitable())
 		addVisitableObj(obj);
 }
@@ -581,6 +585,7 @@ void AIGateway::yourTurn(QueryID queryID)
 {
 	LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID);
 	NET_EVENT_HANDLER;
+	nullkiller->invalidatePathfinderData();
 	status.addQuery(queryID, "YourTurn");
 	requestActionASAP([=](){ answerQuery(queryID, 0); });
 	status.startedTurn();
@@ -788,14 +793,30 @@ bool AIGateway::makePossibleUpgrades(const CArmedInstance * obj)
 	{
 		if(const CStackInstance * s = obj->getStackPtr(SlotID(i)))
 		{
-			UpgradeInfo ui;
-			myCb->fillUpgradeInfo(obj, SlotID(i), ui);
-			if(ui.oldID != CreatureID::NONE && nullkiller->getFreeResources().canAfford(ui.cost[0] * s->count))
+			UpgradeInfo upgradeInfo(s->getId());
+			do
 			{
-				myCb->upgradeCreature(obj, SlotID(i), ui.newID[0]);
-				upgraded = true;
-				logAi->debug("Upgraded %d %s to %s", s->count, ui.oldID.toCreature()->getNamePluralTranslated(), ui.newID[0].toCreature()->getNamePluralTranslated());
+				myCb->fillUpgradeInfo(obj, SlotID(i), upgradeInfo);
+
+				if(upgradeInfo.hasUpgrades())
+				{
+					// creature at given slot might have alternative upgrades, pick best one
+					CreatureID upgID = *vstd::maxElementByFun(upgradeInfo.getAvailableUpgrades(), [](const CreatureID & id)
+						{
+							return id.toCreature()->getAIValue();
+						});
+					if(nullkiller->getFreeResources().canAfford(upgradeInfo.getUpgradeCostsFor(upgID) * s->count))
+					{
+						myCb->upgradeCreature(obj, SlotID(i), upgID);
+						upgraded = true;
+						logAi->debug("Upgraded %d %s to %s", s->count, upgradeInfo.oldID.toCreature()->getNamePluralTranslated(), 
+							upgradeInfo.getUpgrade().toCreature()->getNamePluralTranslated());
+					}
+					else
+						break;
+				}
 			}
+			while(upgradeInfo.hasUpgrades());
 		}
 	}
 

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

@@ -36,7 +36,7 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
 		logAi->trace("Checking dwelling level %d", level);
 		BuildingInfo nextToBuild = BuildingInfo();
 
-		for(int upgradeIndex : {1, 0})
+		for(int upgradeIndex : {2, 1, 0})
 		{
 			BuildingID building = BuildingID(BuildingID::getDwellingFromLevel(level, upgradeIndex));
 			if(!vstd::contains(buildings, building))
@@ -212,7 +212,7 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
 	int creatureLevel = -1;
 	int creatureUpgrade = 0;
 
-	if(BuildingID::DWELL_FIRST <= toBuild && toBuild <= BuildingID::DWELL_UP_LAST)
+	if(toBuild.IsDwelling())
 	{
 		creatureLevel = BuildingID::getLevelFromDwelling(toBuild);
 		creatureUpgrade = BuildingID::getUpgradedFromDwelling(toBuild);
@@ -239,8 +239,7 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
 
 	auto info = BuildingInfo(buildPtr, creature, baseCreatureID, town, ai);
 
-	logAi->trace("checking %s", info.name);
-	logAi->trace("buildInfo %s", info.toString());
+	//logAi->trace("checking %s buildInfo %s", info.name, info.toString());
 
 	int highestFort = 0;
 	for (auto twn : ai->cb->getTownsInfo())
@@ -258,7 +257,7 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
 		}
 		else if(canBuild == EBuildingState::NO_RESOURCES)
 		{
-			logAi->trace("cant build. Not enough resources. Need %s", info.buildCost.toString());
+			//logAi->trace("cant build. Not enough resources. Need %s", info.buildCost.toString());
 			info.notEnoughRes = true;
 		}
 		else if(canBuild == EBuildingState::PREREQUIRES)
@@ -271,7 +270,7 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
 
 			auto otherDwelling = [](const BuildingID & id) -> bool
 			{
-				return BuildingID::DWELL_FIRST <= id && id <= BuildingID::DWELL_UP_LAST;
+				return id.IsDwelling();
 			};
 
 			if(vstd::contains_if(missingBuildings, otherDwelling))
@@ -420,7 +419,7 @@ BuildingInfo::BuildingInfo(
 		}
 		else
 		{
-			if(BuildingID::DWELL_FIRST <= id && id <= BuildingID::DWELL_UP_LAST)
+			if(id.IsDwelling())
 			{
 				creatureGrows = creature->getGrowth();
 

+ 2 - 2
AI/Nullkiller/Analyzers/HeroManager.cpp

@@ -72,8 +72,8 @@ float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const
 {
 	auto heroSpecial = Selector::source(BonusSource::HERO_SPECIAL, BonusSourceID(hero->getHeroTypeID()));
 	auto secondarySkillBonus = Selector::targetSourceType()(BonusSource::SECONDARY_SKILL);
-	auto specialSecondarySkillBonuses = hero->getBonuses(heroSpecial.And(secondarySkillBonus));
-	auto secondarySkillBonuses = hero->getBonuses(Selector::sourceTypeSel(BonusSource::SECONDARY_SKILL));
+	auto specialSecondarySkillBonuses = hero->getBonuses(heroSpecial.And(secondarySkillBonus), "HeroManager::evaluateSpeciality");
+	auto secondarySkillBonuses = hero->getBonusesFrom(BonusSource::SECONDARY_SKILL);
 	float specialityScore = 0.0f;
 
 	for(auto bonus : *secondarySkillBonuses)

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

@@ -97,6 +97,10 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
 
 			for(auto hero : availableHeroes)
 			{
+				if ((town->visitingHero || town->garrisonHero) 
+					&& closestThreat < 1
+					&& hero->getArmyCost() < GameConstants::HERO_GOLD_COST / 3.0)
+					continue;
 				auto score = ai->heroManager->evaluateHero(hero);
 				if(score > minScoreToHireMain)
 				{

+ 14 - 3
AI/Nullkiller/Engine/Nullkiller.cpp

@@ -37,6 +37,7 @@ Nullkiller::Nullkiller()
 	: activeHero(nullptr)
 	, scanDepth(ScanDepth::MAIN_FULL)
 	, useHeroChain(true)
+	, pathfinderInvalidated(false)
 	, memory(std::make_unique<AIMemory>())
 {
 
@@ -239,6 +240,11 @@ void Nullkiller::resetAiState()
 	}
 }
 
+void Nullkiller::invalidatePathfinderData()
+{
+	pathfinderInvalidated = true;
+}
+
 void Nullkiller::updateAiState(int pass, bool fast)
 {
 	boost::this_thread::interruption_point();
@@ -253,7 +259,10 @@ void Nullkiller::updateAiState(int pass, bool fast)
 	decomposer->reset();
 	buildAnalyzer->update();
 
-	if(!fast)
+	if (!pathfinderInvalidated)
+		logAi->trace("Skipping paths regeneration - up to date");
+
+	if(!fast && pathfinderInvalidated)
 	{
 		memory->removeInvisibleObjects(cb.get());
 
@@ -304,11 +313,13 @@ void Nullkiller::updateAiState(int pass, bool fast)
 		boost::this_thread::interruption_point();
 
 		objectClusterizer->clusterize();
+
+		pathfinderInvalidated = false;
 	}
 
 	armyManager->update();
 
-	logAi->debug("AI state updated in %ld", timeElapsed(start));
+	logAi->debug("AI state updated in %ld ms", timeElapsed(start));
 }
 
 bool Nullkiller::isHeroLocked(const CGHeroInstance * hero) const
@@ -379,7 +390,7 @@ void Nullkiller::makeTurn()
 
 		Goals::TTask bestTask = taskptr(Goals::Invalid());
 
-		while(true)
+		for(int j = 1; j <= settings->getMaxPriorityPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; j++)
 		{
 			bestTasks.clear();
 

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

@@ -78,6 +78,7 @@ private:
 	AIGateway * gateway;
 	bool openMap;
 	bool useObjectGraph;
+	bool pathfinderInvalidated;
 
 public:
 	static std::unique_ptr<ObjectGraph> baseGraph;
@@ -121,6 +122,7 @@ public:
 	bool isOpenMap() const { return openMap; }
 	bool isObjectGraphAllowed() const { return useObjectGraph; }
 	bool handleTrading();
+	void invalidatePathfinderData();
 
 private:
 	void resetAiState();

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

@@ -32,7 +32,8 @@ namespace NKAI
 		retreatThresholdRelative(0.3),
 		retreatThresholdAbsolute(10000),
 		safeAttackRatio(1.1),
-		maxpass(10),
+		maxPass(10),
+		maxPriorityPass(10),
 		pathfinderBucketsCount(1),
 		pathfinderBucketSize(32),
 		allowObjectGraph(true),
@@ -48,7 +49,8 @@ namespace NKAI
 		maxRoamingHeroes = node["maxRoamingHeroes"].Integer();
 		mainHeroTurnDistanceLimit = node["mainHeroTurnDistanceLimit"].Integer();
 		scoutHeroTurnDistanceLimit = node["scoutHeroTurnDistanceLimit"].Integer();
-		maxpass = node["maxpass"].Integer();
+		maxPass = node["maxPass"].Integer();
+		maxPriorityPass = node["maxPriorityPass"].Integer();
 		pathfinderBucketsCount = node["pathfinderBucketsCount"].Integer();
 		pathfinderBucketSize = node["pathfinderBucketSize"].Integer();
 		maxGoldPressure = node["maxGoldPressure"].Float();

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

@@ -24,7 +24,8 @@ namespace NKAI
 		int maxRoamingHeroes;
 		int mainHeroTurnDistanceLimit;
 		int scoutHeroTurnDistanceLimit;
-		int maxpass;
+		int maxPass;
+		int maxPriorityPass;
 		int pathfinderBucketsCount;
 		int pathfinderBucketSize;
 		float maxGoldPressure;
@@ -41,7 +42,8 @@ namespace NKAI
 	public:
 		explicit Settings(int difficultyLevel);
 
-		int getMaxPass() const { return maxpass; }
+		int getMaxPass() const { return maxPass; }
+		int getMaxPriorityPass() const { return maxPriorityPass; }
 		float getMaxGoldPressure() const { return maxGoldPressure; }
 		float getRetreatThresholdRelative() const { return retreatThresholdRelative; }
 		float getRetreatThresholdAbsolute() const { return retreatThresholdAbsolute; }

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

@@ -23,8 +23,6 @@ constexpr int NKAI_GRAPH_TRACE_LEVEL = 0;
 #include "Actions/SpecialAction.h"
 #include "Actors.h"
 
-#include <boost/container/small_vector.hpp>
-
 namespace NKAI
 {
 namespace AIPathfinding

+ 2 - 2
AI/Nullkiller/Pathfinding/AIPathfinder.cpp

@@ -106,7 +106,7 @@ void AIPathfinder::updatePaths(const std::map<const CGHeroInstance *, HeroRole>
 
 	if(!pathfinderSettings.useHeroChain)
 	{
-		logAi->trace("Recalculated paths in %ld", timeElapsed(start));
+		logAi->trace("Recalculated paths in %ld ms", timeElapsed(start));
 
 		return;
 	}
@@ -141,7 +141,7 @@ void AIPathfinder::updatePaths(const std::map<const CGHeroInstance *, HeroRole>
 		}
 	} while(storage->increaseHeroChainTurnLimit());
 
-	logAi->trace("Recalculated paths in %ld", timeElapsed(start));
+	logAi->trace("Recalculated paths in %ld ms", timeElapsed(start));
 }
 
 void AIPathfinder::updateGraphs(

+ 1 - 1
AI/VCAI/BuildingManager.cpp

@@ -222,7 +222,7 @@ bool BuildingManager::getBuildingOptions(const CGTownInstance * t)
 	std::vector<BuildingID> extraBuildings;
 	for (auto buildingInfo : t->getTown()->buildings)
 	{
-		if (buildingInfo.first > BuildingID::DWELL_UP2_FIRST)
+		if (buildingInfo.first.IsDwelling() && BuildingID::getUpgradedFromDwelling(buildingInfo.first) > 1)
 			extraBuildings.push_back(buildingInfo.first);
 	}
 	return tryBuildAnyStructure(t, extraBuildings);

+ 2 - 1
AI/VCAI/Goals/GatherTroops.cpp

@@ -109,7 +109,8 @@ TGoalVec GatherTroops::getAllPossibleSubgoals()
 			if(upgradeNumber < 0)
 				continue;
 
-			BuildingID bid(BuildingID::DWELL_FIRST + creature->getLevel() - 1 + upgradeNumber * t->getTown()->creatures.size());
+			BuildingID bid(BuildingID::getDwellingFromLevel(creature->getLevel(), upgradeNumber));
+
 			if(t->hasBuilt(bid) && ai->ah->freeResources().canAfford(creature->getFullRecruitCost())) //this assumes only creatures with dwellings are assigned to faction
 			{
 				solutions.push_back(sptr(BuyArmy(t, creature->getAIValue() * this->value).setobjid(objid)));

+ 22 - 4
AI/VCAI/VCAI.cpp

@@ -22,6 +22,7 @@
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/IGameSettings.h"
 #include "../../lib/gameState/CGameState.h"
+#include "../../lib/gameState/UpgradeInfo.h"
 #include "../../lib/bonuses/Limiters.h"
 #include "../../lib/bonuses/Updaters.h"
 #include "../../lib/bonuses/Propagators.h"
@@ -754,12 +755,29 @@ void makePossibleUpgrades(const CArmedInstance * obj)
 	{
 		if(const CStackInstance * s = obj->getStackPtr(SlotID(i)))
 		{
-			UpgradeInfo ui;
-			cb->fillUpgradeInfo(obj, SlotID(i), ui);
-			if(ui.oldID != CreatureID::NONE && cb->getResourceAmount().canAfford(ui.cost[0] * s->count))
+			UpgradeInfo upgradeInfo(s->getId());
+			do
 			{
-				cb->upgradeCreature(obj, SlotID(i), ui.newID[0]);
+				cb->fillUpgradeInfo(obj, SlotID(i), upgradeInfo);
+
+				if(upgradeInfo.hasUpgrades())
+				{
+					// creature at given slot might have alternative upgrades, pick best one
+					CreatureID upgID = *vstd::maxElementByFun(upgradeInfo.getAvailableUpgrades(), [](const CreatureID & id)
+						{
+							return id.toCreature()->getAIValue();
+						});
+					if(cb->getResourceAmount().canAfford(upgradeInfo.getUpgradeCostsFor(upgID) * s->count))
+					{
+						cb->upgradeCreature(obj, SlotID(i), upgID);
+						logAi->debug("Upgraded %d %s to %s", s->count, upgradeInfo.oldID.toCreature()->getNamePluralTranslated(), 
+							upgradeInfo.getUpgrade().toCreature()->getNamePluralTranslated());
+					}
+					else
+						break;
+				}
 			}
+			while(upgradeInfo.hasUpgrades());
 		}
 	}
 }

+ 1 - 1
CMakePresets.json

@@ -317,7 +317,7 @@
             "description": "VCMI Android daily build",
             "inherits": "android-conan-ninja-release",
             "cacheVariables": {
-                "ANDROID_GRADLE_PROPERTIES": "applicationIdSuffix=.daily;signingConfig=dailySigning;applicationLabel=VCMI daily"
+                "ANDROID_GRADLE_PROPERTIES": "applicationIdSuffix=.daily;signingConfig=dailySigning;applicationLabel=VCMI daily;applicationVariant=daily"
             }
         }
     ],

+ 41 - 0
ChangeLog.md

@@ -1,5 +1,46 @@
 # VCMI Project Changelog
 
+## 1.6.0 -> 1.6.1
+
+### General
+
+* Right-click popup for Monoliths, Subterranean Gates and Whirlpools now shows location of all known entrances and exits.
+* Added support for importing and exporting mod presets in the launcher.
+* Added option to VCMI popup menu on Android to skip launcher and start game immediately.
+* Fixed defeat music not stopping when skipping defeat movie playback.
+* Launcher will now open start game tab instead of mods tab after initial setup if no mods were chosen for install
+
+### Performance
+
+* xbrz is no longer auto-selected on mobile platforms, and only xbrz2 can be auto-selected on PC platforms. Manual selection is unaffected.
+* Fixed a performance regression that more than doubled the time of random map generation.
+* Improved performance of the Nullkiller AI, which should now take turns up to twice as fast.
+* Minor xbrz performance improvements.
+
+### Stability
+
+* Fixed application freeze when clicking buttons with popup message on some versions of iOS
+* Fixed crash when trying to install a mod when no mod is selected
+* Fixed possible crash when trying to load non-existing frame from .def file
+* Fixed crash when right-clicking on modded market items, such as HotA's Junkman.
+* Fixed crash when trying to activate mod with recursive dependencies
+* Fixed crash when trying to activate Chronicles mod after failed import
+* Fixed crash when attempting to access a non-installed mod when repository checkout is off.
+* Show error message on failure to load filesystem instead of crashing in launcher
+* Added workaround for crash on attempt to delete non-existent save/map
+
+### Adventure AI
+
+* AI can now use all possible upgrades on Hill Fort, including alternative upgrades.
+* AI will now correctly identify and use 8th creature dwelling (e.g. Factory)
+* AI will now correctly identify 2nd level upgrades to dwellings (e.g. Cove) and score them accordingly.
+
+### Interface
+
+* Fixed poor alignment of hero primary skill descriptions in the Thieves' Guild menu.
+* Fixed missing description for spell school immunity in creature window
+* Fixed bonuses such as OPENING_BATTLE_SPELL not appearing in creature window.
+
 ## 1.5.7 -> 1.6.0
 
 ### Major changes

+ 3 - 0
Global.h

@@ -134,6 +134,7 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 #include <random>
 #include <regex>
 #include <set>
+#include <shared_mutex>
 #include <sstream>
 #include <string>
 #include <unordered_map>
@@ -168,6 +169,8 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 #include <boost/algorithm/string.hpp>
 #include <boost/crc.hpp>
 #include <boost/current_function.hpp>
+#include <boost/container/small_vector.hpp>
+#include <boost/container/static_vector.hpp>
 #include <boost/date_time/posix_time/posix_time_types.hpp>
 #include <boost/date_time/posix_time/time_formatters.hpp>
 #include <boost/filesystem.hpp>

BIN
Mods/vcmi/Content/Sprites/portalBidirectional.png


BIN
Mods/vcmi/Content/Sprites/portalEntrance.png


BIN
Mods/vcmi/Content/Sprites/portalExit.png


BIN
Mods/vcmi/Content/Sprites2x/portalBidirectional.png


BIN
Mods/vcmi/Content/Sprites2x/portalEntrance.png


BIN
Mods/vcmi/Content/Sprites2x/portalExit.png


+ 43 - 23
Mods/vcmi/Content/config/czech.json

@@ -43,25 +43,25 @@
 	"vcmi.capitalColors.5" : "Fialový",
 	"vcmi.capitalColors.6" : "Tyrkysový",
 	"vcmi.capitalColors.7" : "Růžový",
-
+	
 	"vcmi.heroOverview.startingArmy" : "Počáteční jednotky",
 	"vcmi.heroOverview.warMachine" : "Bojové stroje",
 	"vcmi.heroOverview.secondarySkills" : "Druhotné schopnosti",
 	"vcmi.heroOverview.spells" : "Kouzla",
-
+	
 	"vcmi.quickExchange.moveUnit" : "Přesunout jednotku",
 	"vcmi.quickExchange.moveAllUnits" : "Přesunout všechny jednotky",
 	"vcmi.quickExchange.swapAllUnits" : "Vyměnit armády",
 	"vcmi.quickExchange.moveAllArtifacts" : "Přesunout všechny artefakty",
 	"vcmi.quickExchange.swapAllArtifacts" : "Vyměnit artefakty",
-
+	
 	"vcmi.radialWheel.mergeSameUnit" : "Sloučit stejné jednotky",
 	"vcmi.radialWheel.fillSingleUnit" : "Vyplnit jednou jednotkou",
 	"vcmi.radialWheel.splitSingleUnit" : "Rozdělit jedinou jednotku",
 	"vcmi.radialWheel.splitUnitEqually" : "Rozdělit jednotky rovnoměrně",
 	"vcmi.radialWheel.moveUnit" : "Přesunout jednotky do jiného oddílu",
 	"vcmi.radialWheel.splitUnit" : "Rozdělit jednotku do jiné pozice",
-
+	
 	"vcmi.radialWheel.heroGetArmy" : "Získat armádu jiného hrdiny",
 	"vcmi.radialWheel.heroSwapArmy" : "Vyměnit armádu s jiným hrdinou",
 	"vcmi.radialWheel.heroExchange" : "Otevřít výměnu hrdinů",
@@ -73,7 +73,7 @@
 	"vcmi.radialWheel.moveUp" : "Posunout výše",
 	"vcmi.radialWheel.moveDown" : "Posunout níže",
 	"vcmi.radialWheel.moveBottom" : "Přesunout dolů",
-
+	
 	"vcmi.randomMap.description" : "Mapa vytvořená Generátorem náhodných map.\nŠablona: %s, rozměry: %dx%d, úroveň: %d, hráči: %d, AI hráči: %d, množství vody: %s, síla jednotek: %s, VCMI mapa",
 	"vcmi.randomMap.description.isHuman" : ", %s je lidský hráč",
 	"vcmi.randomMap.description.townChoice" : ", volba města pro %s je %s",
@@ -230,7 +230,7 @@
 	"vcmi.server.errors.unknownEntity" : "Nelze načíst uloženou pozici! Neznámá entita '%s' nalezena v uložené pozici! Uložná pozice nemusí být kompatibilní s aktuálními verzemi modifikací!",
 	"vcmi.server.errors.wrongIdentified" : "Byli jste identifikováni jako hráč %s, zatímco byl očekáván hráč %s.",
 	"vcmi.server.errors.notAllowed" : "Nemáte oprávnění provést tuto akci!",
-
+	
 	"vcmi.dimensionDoor.seaToLandError" : "Pomocí dimenzní brány není možné se teleportovat z moře na pevninu nebo naopak.",
 
 	"vcmi.settingsMainWindow.generalTab.hover" : "Obecné",
@@ -325,9 +325,9 @@
 	"vcmi.adventureOptions.smoothDragging.help" : "{Plynulé posouvání mapy}\n\nPokud je tato možnost aktivována, posouvání mapy bude plynulé.",
 	"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Přeskočit efekty mizení",
 	"vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Přeskočit efekty mizení}\n\nKdyž je povoleno, přeskočí se efekty mizení objektů a podobné efekty (sběr surovin, nalodění atd.). V některých případech zrychlí uživatelské rozhraní na úkor estetiky. Obzvláště užitečné v PvP hrách. Pro maximální rychlost pohybu je toto nastavení aktivní bez ohledu na další volby.",
-	"vcmi.adventureOptions.mapScrollSpeed1.hover" : "",
-	"vcmi.adventureOptions.mapScrollSpeed5.hover" : "",
-	"vcmi.adventureOptions.mapScrollSpeed6.hover" : "",
+	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed1.help" : "Nastavit posouvání mapy na velmi pomalé",
 	"vcmi.adventureOptions.mapScrollSpeed5.help" : "Nastavit posouvání mapy na velmi rychlé",
 	"vcmi.adventureOptions.mapScrollSpeed6.help" : "Nastavit posouvání mapy na okamžité",
@@ -336,16 +336,16 @@
 
 	"vcmi.battleOptions.queueSizeLabel.hover" : "Zobrazit frontu pořadí tahů",
 	"vcmi.battleOptions.queueSizeNoneButton.hover" : "VYPNUTO",
-	"vcmi.battleOptions.queueSizeAutoButton.hover" : "AUTO",
+	"vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO",
 	"vcmi.battleOptions.queueSizeSmallButton.hover" : "MALÁ",
 	"vcmi.battleOptions.queueSizeBigButton.hover" : "VELKÁ",
 	"vcmi.battleOptions.queueSizeNoneButton.help" : "Nezobrazovat frontu pořadí tahů.",
 	"vcmi.battleOptions.queueSizeAutoButton.help" : "Nastavit automaticky velikost fronty pořadí tahů podle rozlišení obrazovky hry (Při výšce herního rozlišení menší než 700 pixelů je použita velikost MALÁ, jinak velikost VELKÁ)",
 	"vcmi.battleOptions.queueSizeSmallButton.help" : "Zobrazit MALOU frontu pořadí tahů.",
 	"vcmi.battleOptions.queueSizeBigButton.help" : "Zobrazit VELKOU frontu pořadí tahů (není podporováno, pokud výška rozlišení hry není alespoň 700 pixelů).",
-	"vcmi.battleOptions.animationsSpeed1.hover" : "",
-	"vcmi.battleOptions.animationsSpeed5.hover" : "",
-	"vcmi.battleOptions.animationsSpeed6.hover" : "",
+	"vcmi.battleOptions.animationsSpeed1.hover": "",
+	"vcmi.battleOptions.animationsSpeed5.hover": "",
+	"vcmi.battleOptions.animationsSpeed6.hover": "",
 	"vcmi.battleOptions.animationsSpeed1.help" : "Nastavit rychlost animací na velmi pomalé.",
 	"vcmi.battleOptions.animationsSpeed5.help" : "Nastavit rychlost animací na velmi rychlé.",
 	"vcmi.battleOptions.animationsSpeed6.help" : "Nastavit rychlost animací na okamžité.",
@@ -376,13 +376,13 @@
 	"vcmi.battleWindow.damageEstimation.damage.1" : "%d poškození",
 	"vcmi.battleWindow.damageEstimation.kills" : "%d zahyne",
 	"vcmi.battleWindow.damageEstimation.kills.1" : "%d zahyne",
-
+	
 	"vcmi.battleWindow.damageRetaliation.will" : "Provede odvetu ",
 	"vcmi.battleWindow.damageRetaliation.may" : "Může provést odvetu",
 	"vcmi.battleWindow.damageRetaliation.never" : "Neprovede odvetu.",
 	"vcmi.battleWindow.damageRetaliation.damage" : "(%DAMAGE).",
 	"vcmi.battleWindow.damageRetaliation.damageKills" : "(%DAMAGE, %KILLS).",
-
+	
 	"vcmi.battleWindow.killed" : "Zabito",
 	"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s bylo zabito přesnými zásahy!",
 	"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s byl zabit přesným zásahem!",
@@ -497,7 +497,7 @@
 	"vcmi.optionsTab.simturns.blocked1"       : "Souběžně: 1 týden, setkání zablokována",
 	"vcmi.optionsTab.simturns.blocked2"       : "Souběžně: 2 týdny, setkání zablokována",
 	"vcmi.optionsTab.simturns.blocked4"       : "Souběžně: 1 měsíc, setkání zablokována",
-
+	
 	// Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language
 	// Using this information, VCMI will automatically select correct plural form for every possible amount
 	"vcmi.optionsTab.simturns.days.0" : " %d dní",
@@ -541,7 +541,7 @@
 	"vcmi.stackExperience.rank.8" : "Elitní",
 	"vcmi.stackExperience.rank.9" : "Mistr",
 	"vcmi.stackExperience.rank.10" : "Eso",
-
+	
 	// Strings for HotA Seer Hut / Quest Guards
 	"core.seerhut.quest.heroClass.complete.0" : "Ah, vy jste %s. Tady máte dárek. Přijmete ho?",
 	"core.seerhut.quest.heroClass.complete.1" : "Ah, vy jste %s. Tady máte dárek. Přijmete ho?",
@@ -573,7 +573,7 @@
 	"core.seerhut.quest.heroClass.visit.3" : "Stráže zde povolí průchod pouze %s.",
 	"core.seerhut.quest.heroClass.visit.4" : "Stráže zde povolí průchod pouze %s.",
 	"core.seerhut.quest.heroClass.visit.5" : "Stráže zde povolí průchod pouze %s.",
-
+	
 	"core.seerhut.quest.reachDate.complete.0" : "Jsem nyní volný. Tady máte, co jsem pro vás měl. Přijmete to?",
 	"core.seerhut.quest.reachDate.complete.1" : "Jsem nyní volný. Tady máte, co jsem pro vás měl. Přijmete to?",
 	"core.seerhut.quest.reachDate.complete.2" : "Jsem nyní volný. Tady máte, co jsem pro vás měl. Přijmete to?",
@@ -604,9 +604,9 @@
 	"core.seerhut.quest.reachDate.visit.3" : "Zavřeno do %s.",
 	"core.seerhut.quest.reachDate.visit.4" : "Zavřeno do %s.",
 	"core.seerhut.quest.reachDate.visit.5" : "Zavřeno do %s.",
-
+	
 	"mapObject.core.hillFort.object.description" : "Zde můžeš vylepšit jednotky. Vylepšení jednotek úrovně 1 až 4 je zde levnější než v jejich domovském městě.",
-
+	
 	"core.bonus.ADDITIONAL_ATTACK.name" : "Dvojitý útok",
 	"core.bonus.ADDITIONAL_ATTACK.description" : "Útočí dvakrát",
 	"core.bonus.ADDITIONAL_RETALIATION.name" : "Další odvetné útoky",
@@ -729,8 +729,6 @@
 	"core.bonus.SPELL_AFTER_ATTACK.description" : "Má ${val}% šanci seslat ${subtype.spell} po útoku",
 	"core.bonus.SPELL_BEFORE_ATTACK.name" : "Sesílá před útokem",
 	"core.bonus.SPELL_BEFORE_ATTACK.description" : "Má ${val}% šanci seslat ${subtype.spell} před útokem",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name" : "Magická odolnost",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description" : "Poškození kouzly sníženo o ${val}%.",
 	"core.bonus.SPELL_IMMUNITY.name" : "Imunita vůči kouzlům",
 	"core.bonus.SPELL_IMMUNITY.description" : "Imunní vůči ${subtype.spell}",
 	"core.bonus.SPELL_LIKE_ATTACK.name" : "Útok kouzlem",
@@ -763,7 +761,29 @@
 	"core.bonus.MECHANICAL.name" : "Mechanický",
 	"core.bonus.PRISM_HEX_ATTACK_BREATH.name" : "Trojitý dech",
 	"core.bonus.PRISM_HEX_ATTACK_BREATH.description" : "Útok trojitým dechem (útok přes 3 směry)",
-
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "Odolnost vůči kouzlům",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.air": "Odolnost vůči kouzlům vzduchu",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.fire": "Odolnost vůči kouzlům ohně",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.water": "Odolnost vůči kouzlům vody",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.earth": "Odolnost vůči kouzlům země",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description": "Poškození ze všech kouzel sníženo o ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.air": "Poškození kouzel magie vzduchu sníženo o ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.fire": "Poškození kouzel magie ohně sníženo o ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.water": "Poškození kouzel magie vody sníženo o ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.earth": "Poškození kouzel magie země sníženo o ${val}%.",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name": "Imunita vůči kouzlům",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.air": "Vzdušná imunita",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.fire": "Ohnivá imunita",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.water": "Vodní imunita",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.earth": "Zemská imunita",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description": "Jednotka je imunní vůči všem kouzlům.",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.air": "Jednotka je imunní vůči všem kouzlům magie vzduchu.",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.fire": "Jednotka je imunní vůči všem kouzlům magie ohně.",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.water": "Jednotka je imunní vůči všem kouzlům magie vody.",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.earth": "Jednotka je imunní vůči všem kouzlům magie země.",
+	"core.bonus.OPENING_BATTLE_SPELL.name": "Začíná kouzlem",
+	"core.bonus.OPENING_BATTLE_SPELL.description": "Sesílá ${subtype.spell} na začátku bitvy.",
+	
 	"spell.core.castleMoat.name" : "Hradní příkop",
 	"spell.core.castleMoatTrigger.name" : "Hradní příkop",
 	"spell.core.catapultShot.name" : "Výstřel z katapultu",

+ 23 - 3
Mods/vcmi/Content/config/english.json

@@ -729,8 +729,6 @@
 	"core.bonus.SPELL_AFTER_ATTACK.description": "Has a ${val}% chance to cast ${subtype.spell} after it attacks",
 	"core.bonus.SPELL_BEFORE_ATTACK.name": "Cast Before Attack",
 	"core.bonus.SPELL_BEFORE_ATTACK.description": "Has a ${val}% chance to cast ${subtype.spell} before it attacks",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "Spell Resistance",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description": "Damage from spells reduced by ${val}%.",
 	"core.bonus.SPELL_IMMUNITY.name": "Spell immunity",
 	"core.bonus.SPELL_IMMUNITY.description": "Immune to ${subtype.spell}",
 	"core.bonus.SPELL_LIKE_ATTACK.name": "Spell-like attack",
@@ -762,5 +760,27 @@
 	"core.bonus.MECHANICAL.name": "Mechanical",
 	"core.bonus.MECHANICAL.description": "Immunity to many effects, repairable",
 	"core.bonus.PRISM_HEX_ATTACK_BREATH.name": "Prism Breath",
-	"core.bonus.PRISM_HEX_ATTACK_BREATH.description": "Prism Breath Attack (three directions)"
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.description": "Prism Breath Attack (three directions)",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "Spell Resistance",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.air": "Air Spells Resistance",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.fire": "Fire Spells Resistance",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.water": "Water Spells Resistance",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.earth": "Earth Spells Resistance",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description": "Damage from all spells reduced by ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.air": "Damage from all Air spells reduced by ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.fire": "Damage from all Fire spells reduced by ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.water": "Damage from all Water spells reduced by ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.earth": "Damage from all Earth spells reduced by ${val}%.",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name": "Spell immunity",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.air": "Air immunity",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.fire": "Fire immunity",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.water": "Water immunity",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.earth": "Earth immunity",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description": "This unit is immune to all spells",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.air": "This unit is immune to all Air school spells",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.fire": "This unit is immune to all Fire school spells",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.water": "This unit is immune to all Water school spells",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.earth": "This unit is immune to all Earth school spells",
+	"core.bonus.OPENING_BATTLE_SPELL.name": "Starts with spell",
+	"core.bonus.OPENING_BATTLE_SPELL.description": "Casts ${subtype.spell} on battle start"
 }

+ 25 - 5
Mods/vcmi/Content/config/german.json

@@ -606,7 +606,7 @@
 	"core.seerhut.quest.reachDate.visit.5" : "Geschlossen bis %s.",
 	
 	"mapObject.core.hillFort.object.description" : "Aufwertungen von Kreaturen. Die Stufen 1 - 4 sind billiger als in der zugehörigen Stadt.",
-	
+
 	"core.bonus.ADDITIONAL_ATTACK.name": "Doppelschlag",
 	"core.bonus.ADDITIONAL_ATTACK.description": "Greift zweimal an",
 	"core.bonus.ADDITIONAL_RETALIATION.name": "Zusätzliche Vergeltungsmaßnahmen",
@@ -678,7 +678,7 @@
 	"core.bonus.JOUSTING.name": "Champion Charge",
 	"core.bonus.JOUSTING.description": "+${val}% Schaden pro zurückgelegtem Feld",
 	"core.bonus.KING.name": "König",
-	"core.bonus.KING.description": "Anfällig für SLAYER Level ${val} oder höher",
+	"core.bonus.KING.description": "Anfällig für Drachentöter Level ${val} oder höher",
 	"core.bonus.LEVEL_SPELL_IMMUNITY.name": "Zauberimmunität 1-${val}",
 	"core.bonus.LEVEL_SPELL_IMMUNITY.description": "Immun gegen Zaubersprüche der Stufen 1-${val}",
 	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Begrenzte Schussweite",
@@ -729,8 +729,6 @@
 	"core.bonus.SPELL_AFTER_ATTACK.description": "${val}%, um ${subtype.spell} nach dem Angriff zu wirken",
 	"core.bonus.SPELL_BEFORE_ATTACK.name": "Zauber vor Angriff",
 	"core.bonus.SPELL_BEFORE_ATTACK.description": "${val}% um ${subtype.spell} vor dem Angriff zu wirken",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "Zauberwiderstand",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description": "Schaden von Zaubern reduziert ${val}%.",
 	"core.bonus.SPELL_IMMUNITY.name": "Zauberimmunität",
 	"core.bonus.SPELL_IMMUNITY.description": "Immun gegen ${subtype.spell}",
 	"core.bonus.SPELL_LIKE_ATTACK.name": "zauberähnlicher Angriff",
@@ -762,5 +760,27 @@
 	"core.bonus.MECHANICAL.name": "Mechanisch",
 	"core.bonus.MECHANICAL.description": "Immunität gegen viele Effekte, reparierbar",
 	"core.bonus.PRISM_HEX_ATTACK_BREATH.name": "Prisma-Atem",
-	"core.bonus.PRISM_HEX_ATTACK_BREATH.description": "Prisma-Atem-Angriff (drei Richtungen)"
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.description": "Prisma-Atem-Angriff (drei Richtungen)",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name": "Zauber-Immunität",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description": "Immunität gegen alle Zauber-Schulen",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.air": "Luft-Immunität",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.fire": "Feuer-Immunität",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.water": "Wasser-Immunität",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.earth": "Erde-Immunität",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.air": "Immunität gegen Zauber der Luft-Schule",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.fire": "Immunität gegen Zauber der Feuer-Schule",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.water": "Immunität gegen Zauber der Wasser-Schule",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.earth": "Immunität gegen Zauber der Erde-Schule",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "Zauberwiderstand",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.air": "Luft-Zauberwiderstand",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.fire": "Feuer-Zauberwiderstand",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.water": "Wasser-Zauberwiderstand",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.earth": "Erde-Zauberwiderstand",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description": "Schaden von allen Zaubern um ${val}% reduziert.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.air": "Schaden von Luft-Zaubern um ${val}% reduziert.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.fire": "Schaden von Feuer-Zaubern um ${val}% reduziert.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.water": "Schaden von Wasser-Zaubern um ${val}% reduziert.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.earth": "Schaden von Erde-Zaubern um ${val}% reduziert.",
+	"core.bonus.OPENING_BATTLE_SPELL.name": "Startet mit Zauber",
+	"core.bonus.OPENING_BATTLE_SPELL.description": "Wirkt ${subtype.spell} beim Start des Kampfes"
 }

+ 3 - 3
Mods/vcmi/Content/config/polish.json

@@ -317,9 +317,9 @@
 	"vcmi.adventureOptions.borderScroll.help" : "{Przewijanie na brzegu mapy}\n\nPrzewijanie mapy przygody gdy kursor najeżdża na brzeg okna gry. Może być wyłączone poprzez przytrzymanie klawisza CTRL.",
 	"vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Zarządzanie armią w panelu informacyjnym",
 	"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Zarządzanie armią w panelu informacyjnym}\n\nPozwala zarządzać jednostkami w panelu informacyjnym, zamiast przełączać między domyślnymi informacjami.",
-	"vcmi.adventureOptions.leftButtonDrag.hover" : "Przeciąganie lewym",
+	"vcmi.adventureOptions.leftButtonDrag.hover" : "Przeciąganie LPM",
 	"vcmi.adventureOptions.leftButtonDrag.help" : "{Przeciąganie mapy lewym kliknięciem}\n\nUmożliwia przesuwanie mapy przygody poprzez przeciąganie myszy z wciśniętym lewym przyciskiem.",
-	"vcmi.adventureOptions.rightButtonDrag.hover" : "Przeciąganie prawym",
+	"vcmi.adventureOptions.rightButtonDrag.hover" : "Przeciąganie PPM",
 	"vcmi.adventureOptions.rightButtonDrag.help" : "{Przeciąganie mapy prawym kliknięciem}\n\nUmożliwia przesuwanie mapy przygody poprzez przeciąganie myszy z wciśniętym prawym przyciskiem.",
 	"vcmi.adventureOptions.smoothDragging.hover" : "'Pływające' przeciąganie mapy",
 	"vcmi.adventureOptions.smoothDragging.help" : "{'Pływające' przeciąganie mapy}\n\nPrzeciąganie mapy następuje ze stopniowo zanikającym przyspieszeniem.",
@@ -357,7 +357,7 @@
 	"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Pokaż trwale statystyki bohaterów}\n\nWłącza trwałe okna statystyk bohaterów pokazujące umiejętności pierwszorzędne i punkty magii.",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Pomiń czekanie startowe",
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Pomiń czekanie startowe}\n\n Pomija konieczność czekania podczas muzyki startowej, która jest odtwarzana na początku każdej bitwy przed rozpoczęciem akcji.",
-	"vcmi.battleOptions.endWithAutocombat.hover": "Natychmiastowe auto-walki",
+	"vcmi.battleOptions.endWithAutocombat.hover": "Natychm. auto-walki",
 	"vcmi.battleOptions.endWithAutocombat.help": "{Natychmiastowe auto-walki}\n\nAuto-walka natychmiastowo toczy walkę do samego końca",
 	"vcmi.battleOptions.showQuickSpell.hover": "Szybki dostęp do magii",
 	"vcmi.battleOptions.showQuickSpell.help": "{Szybki dostęp do magii}\n\nPokazuje panel szybkiego dostępu do czarów",

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

@@ -55,10 +55,10 @@
 	"vcmi.quickExchange.moveAllArtifacts" : "Mover Todos os Artefatos",
 	"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.splitUnit" : "Dividir criatura para outro espaço",
 	
@@ -74,8 +74,8 @@
 	"vcmi.radialWheel.moveDown" : "Mover para baixo",
 	"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.water.none" : "nenhuma",
 	"vcmi.randomMap.description.water.normal" : "normal",
@@ -107,11 +107,11 @@
 	"vcmi.lobby.mapPreview" : "Prévia do mapa",
 	"vcmi.lobby.noPreview" : "sem prévia",
 	"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.author" : "Autor",
 	"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.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?",
@@ -123,16 +123,16 @@
 
 	"vcmi.broadcast.failedLoadGame" : "Falha ao carregar o jogo",
 	"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.gameTerminated" : "o jogo foi encerrado",
 	"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.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.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",
@@ -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.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.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.yes" : "sim",
 	"vcmi.broadcast.vote.no" : "não",
 	"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.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.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.help"      : "Muda a duração do intervalo de toque longo.",
 	"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.hapticFeedbackButton.hover"  : "Resposta Tátil",
 	"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.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.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",
@@ -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.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.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.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": "",
@@ -354,7 +354,7 @@
 	"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.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.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",
@@ -674,7 +674,7 @@
 	"core.bonus.HEALER.name" : "Curandeiro",
 	"core.bonus.HEALER.description" : "Cura unidades aliadas",
 	"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.description" : "+${val}% de dano para cada hexágono percorrido",
 	"core.bonus.KING.name" : "Rei",
@@ -705,7 +705,7 @@
 	"core.bonus.NO_WALL_PENALTY.description" : "Causa dano total\ndurante cerco",
 	"core.bonus.NON_LIVING.name" : "Não Vivo",
 	"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.RANGED_RETALIATION.name" : "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.4" : "Orange",
 	"vcmi.capitalColors.5" : "Lila",
-	"vcmi.capitalColors.6" : "Grönblå",
+	"vcmi.capitalColors.6" : "Turkos",
 	"vcmi.capitalColors.7" : "Rosa",
 
 	"vcmi.heroOverview.startingArmy"    : "Startarmé",
@@ -70,7 +70,7 @@
 	"vcmi.radialWheel.heroDismiss"       : "Avfärda hjälten",
 
 	"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.moveBottom" : "Flytta längst ner",
 
@@ -762,5 +762,27 @@
 	"core.bonus.MECHANICAL.name"                         : "Mekanisk",
 	"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.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"
 }

+ 20 - 2
Mods/vcmi/Content/config/ukrainian.json

@@ -722,8 +722,6 @@
 	"core.bonus.SPELL_AFTER_ATTACK.description" : "Застосовує ${subtype.spell} з вірогідністю ${val}% після атаки",
 	"core.bonus.SPELL_BEFORE_ATTACK.name" : "закляття перед атакою",
 	"core.bonus.SPELL_BEFORE_ATTACK.description" : "Застосовує ${subtype.spell} з вірогідністю ${val}% перед атакою",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name" : "Стійкість до заклять",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description" : "Шкода від заклять зменшується на ${val}%.",
 	"core.bonus.SPELL_IMMUNITY.name" : "Імунітет до заклять",
 	"core.bonus.SPELL_IMMUNITY.description" : "Імунітет до ${subtype.spell}",
 	"core.bonus.SPELL_LIKE_ATTACK.name" : "Атака, схожа на закляття",
@@ -762,6 +760,26 @@
 	"core.bonus.MECHANICAL.name" : "Механічний",
 	"core.bonus.PRISM_HEX_ATTACK_BREATH.description" : "Атака подихом у трьох напрямах",
 	"core.bonus.PRISM_HEX_ATTACK_BREATH.name" : "Призматична атака",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "Стійкість до заклять",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.air": "Стійкість до Повітря",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.fire": "Стійкість до Вогню",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.water": "Стійкість до Води",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.earth": "Стійкість до Землі",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description": "Шкода від усіх заклять зменшується на ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.air": "Шкода від усіх заклять школи Повітря зменшується на ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.fire": "Шкода від усіх заклять школи Вогню зменшується на ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.water": "Шкода від усіх заклять школи Води зменшується на ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.earth": "Шкода від усіх заклять школи Землі зменшується на ${val}%.",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name": "Імунітет до усіх заклять",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.air": "Імунітет до Повітря",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.fire": "Імунітет до Вогню",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.water": "Імунітет до Води",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.earth": "Імунітет до Землі",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description": "На цей загін не діють жодні заклинання",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.air": "На цей загін не діють жодні закляття школи Повітря",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.fire": "На цей загін не діють жодні закляття школи Вогню",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.water": "На цей загін не діють жодні закляття школи Води",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.earth": "На цей загін не діють жодні закляття школи Землі",
 	"core.bonus.REVENGE.description" : "Завдає додаткової шкоди залежно від втраченого здоров'я в бою",
 	"core.bonus.REVENGE.name" : "Помста"
 }

+ 2 - 0
android/AndroidManifest.xml

@@ -30,6 +30,8 @@
 				<category android:name="android.intent.category.LAUNCHER"/>
 			</intent-filter>
 
+			<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts${applicationVariant}" /> 
+
 			<meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
 			<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
 			<meta-data android:name="android.app.repository" android:value="default"/>

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

@@ -26,8 +26,8 @@ android {
 		minSdk = qtMinSdkVersion as Integer
 		targetSdk = qtTargetSdkVersion as Integer // ANDROID_TARGET_SDK_VERSION in the CMake project
 
-		versionCode 1610
-		versionName "1.6.0"
+		versionCode 1612
+		versionName "1.6.1"
 
 		setProperty("archivesBaseName", "vcmi")
 	}
@@ -57,6 +57,7 @@ android {
 			applicationIdSuffix '.debug'
 			manifestPlaceholders = [
 				applicationLabel: 'VCMI debug',
+				applicationVariant: 'debug',
 			]
 			ndk {
 				debugSymbolLevel 'full'
@@ -70,6 +71,7 @@ android {
 			proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
 			manifestPlaceholders = [
 				applicationLabel: project.findProperty('applicationLabel') ?: 'VCMI',
+				applicationVariant: project.findProperty('applicationVariant') ?: '',
 			]
 			ndk {
 				debugSymbolLevel 'full'

+ 1 - 0
android/vcmi-app/src/main/res/values-de/strings.xml

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

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

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

+ 13 - 0
android/vcmi-app/src/main/res/xml/shortcuts.xml

@@ -0,0 +1,13 @@
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
+	<shortcut
+		android:shortcutId="play"
+		android:enabled="true"
+		android:icon="@mipmap/ic_launcher"
+		android:shortcutShortLabel="@string/shortcut_play"
+		android:shortcutLongLabel="@string/shortcut_play">
+		<intent
+			android:action="android.intent.action.VIEW"
+			android:targetPackage="is.xyz.vcmi"
+			android:targetClass="eu.vcmi.vcmi.VcmiSDLActivity" />
+	</shortcut>
+</shortcuts>

+ 13 - 0
android/vcmi-app/src/main/res/xml/shortcutsdaily.xml

@@ -0,0 +1,13 @@
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
+	<shortcut
+		android:shortcutId="play"
+		android:enabled="true"
+		android:icon="@mipmap/ic_launcher"
+		android:shortcutShortLabel="@string/shortcut_play"
+		android:shortcutLongLabel="@string/shortcut_play">
+		<intent
+			android:action="android.intent.action.VIEW"
+			android:targetPackage="is.xyz.vcmi.daily"
+			android:targetClass="eu.vcmi.vcmi.VcmiSDLActivity" />
+	</shortcut>
+</shortcuts>

+ 13 - 0
android/vcmi-app/src/main/res/xml/shortcutsdebug.xml

@@ -0,0 +1,13 @@
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
+	<shortcut
+		android:shortcutId="play"
+		android:enabled="true"
+		android:icon="@mipmap/ic_launcher"
+		android:shortcutShortLabel="@string/shortcut_play"
+		android:shortcutLongLabel="@string/shortcut_play">
+		<intent
+			android:action="android.intent.action.VIEW"
+			android:targetPackage="is.xyz.vcmi.debug"
+			android:targetClass="eu.vcmi.vcmi.VcmiSDLActivity" />
+	</shortcut>
+</shortcuts>

+ 1 - 1
client/CPlayerInterface.h

@@ -24,7 +24,7 @@ class CCreature;
 struct CGPath;
 class CCreatureSet;
 class CGObjectInstance;
-struct UpgradeInfo;
+class UpgradeInfo;
 class ConditionalWait;
 struct CPathsInfo;
 

+ 7 - 5
client/adventureMap/CMinimap.cpp

@@ -71,16 +71,18 @@ void CMinimapInstance::redrawMinimap()
 			minimap->drawPoint(Point(x, y), getTileColor(int3(x, y, level)));
 }
 
-CMinimapInstance::CMinimapInstance(CMinimap *Parent, int Level):
-	parent(Parent),
+CMinimapInstance::CMinimapInstance(const Point & position, const Point & dimensions, int Level):
 	minimap(new Canvas(Point(LOCPLINT->cb->getMapSize().x, LOCPLINT->cb->getMapSize().y), CanvasScalingPolicy::IGNORE)),
 	level(Level)
 {
-	pos.w = parent->pos.w;
-	pos.h = parent->pos.h;
+	pos += position;
+	pos.w = dimensions.x;
+	pos.h = dimensions.y;
 	redrawMinimap();
 }
 
+CMinimapInstance::~CMinimapInstance() = default;
+
 void CMinimapInstance::showAll(Canvas & to)
 {
 	to.drawScaled(*minimap, pos.topLeft(), pos.dimensions());
@@ -203,7 +205,7 @@ void CMinimap::update()
 		return;
 
 	OBJECT_CONSTRUCTION;
-	minimap = std::make_shared<CMinimapInstance>(this, level);
+	minimap = std::make_shared<CMinimapInstance>(Point(0,0), pos.dimensions(), level);
 	redraw();
 }
 

+ 2 - 2
client/adventureMap/CMinimap.h

@@ -20,7 +20,6 @@ class CMinimap;
 
 class CMinimapInstance : public CIntObject
 {
-	CMinimap * parent;
 	std::unique_ptr<Canvas> minimap;
 	int level;
 
@@ -29,7 +28,8 @@ class CMinimapInstance : public CIntObject
 
 	void redrawMinimap();
 public:
-	CMinimapInstance(CMinimap * parent, int level);
+	CMinimapInstance(const Point & position, const Point & dimensions, int level);
+	~CMinimapInstance();
 
 	void showAll(Canvas & to) override;
 	void refreshTile(const int3 & pos);

+ 1 - 1
client/battle/BattleActionsController.cpp

@@ -957,7 +957,7 @@ void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterS
 			creatureSpells.push_back(spellToCast.toSpell());
 	}
 
-	TConstBonusListPtr bl = casterStack->getBonuses(Selector::type()(BonusType::SPELLCASTER));
+	TConstBonusListPtr bl = casterStack->getBonusesOfType(BonusType::SPELLCASTER);
 
 	for(const auto & bonus : *bl)
 	{

+ 9 - 3
client/mainmenu/CHighScoreScreen.cpp

@@ -216,11 +216,17 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc
 	}
 }
 
-void CHighScoreInputScreen::onVideoPlaybackFinished()
+void CHighScoreInputScreen::stopMusicAndClose()
 {
+	CMM->playMusic();
 	close();
 }
 
+void CHighScoreInputScreen::onVideoPlaybackFinished()
+{
+	stopMusicAndClose();
+}
+
 int CHighScoreInputScreen::addEntry(std::string text) {
 	std::vector<JsonNode> baseNode = persistentStorage["highscore"][calc.isCampaign ? "campaign" : "scenario"].Vector();
 	
@@ -275,7 +281,7 @@ void CHighScoreInputScreen::clickPressed(const Point & cursorPosition)
 
 	if(!won)
 	{
-		close();
+		stopMusicAndClose();
 		return;
 	}
 
@@ -291,7 +297,7 @@ void CHighScoreInputScreen::clickPressed(const Point & cursorPosition)
 				GH.windows().createAndPushWindow<CHighScoreScreen>(calc.isCampaign ? CHighScoreScreen::HighScorePage::CAMPAIGN : CHighScoreScreen::HighScorePage::SCENARIO, pos);
 			}
 			else
-				close();
+				stopMusicAndClose();
 		});
 	}
 }

+ 1 - 0
client/mainmenu/CHighScoreScreen.h

@@ -85,6 +85,7 @@ class CHighScoreInputScreen : public CWindowObject, public IVideoHolder
 	HighScoreCalculation calc;
 	StatisticDataSet stat;
 
+	void stopMusicAndClose();
 	void onVideoPlaybackFinished() override;
 public:
 	CHighScoreInputScreen(bool won, HighScoreCalculation calc, const StatisticDataSet & statistic);

+ 1 - 1
client/mapView/MapRenderer.cpp

@@ -135,7 +135,7 @@ int MapTileStorage::groupCount(size_t fileIndex, size_t rotationIndex, size_t im
 	const auto & animation = animations[fileIndex][rotationIndex];
 	if (animation)
 		for(int i = 0;; i++)
-			if(!animation->getImage(imageIndex, i, false))
+			if(animation->size(i) <= imageIndex)
 				return i;
 	return 1;
 }

+ 21 - 5
client/render/CDefFile.cpp

@@ -83,13 +83,12 @@ CDefFile::CDefFile(const AnimationPath & Name):
 
 void CDefFile::loadFrame(size_t frame, size_t group, IImageLoader &loader) const
 {
-	std::map<size_t, std::vector <size_t> >::const_iterator it;
-	it = offset.find(group);
-	assert (it != offset.end());
+	assert(hasFrame(frame, group));		// hasFrame() should be called before calling loadFrame()
 
-	const ui8 * FDef = data.get()+it->second[frame];
+	const ui8 * FDef = data.get() + offset.at(group)[frame];
+
+	const SSpriteDef sd = *reinterpret_cast<const SSpriteDef *>(FDef);
 
-	const SSpriteDef sd = * reinterpret_cast<const SSpriteDef *>(FDef);
 	SSpriteDef sprite;
 
 	sprite.format = read_le_u32(&sd.format);
@@ -229,6 +228,23 @@ void CDefFile::loadFrame(size_t frame, size_t group, IImageLoader &loader) const
 	}
 }
 
+bool CDefFile::hasFrame(size_t frame, size_t group) const
+{
+	std::map<size_t, std::vector <size_t> >::const_iterator it;
+	it = offset.find(group);
+	if(it == offset.end())
+	{
+		return false;
+	}
+
+	if(frame >= it->second.size())
+	{
+		return false;
+	}
+
+	return true;
+}
+
 CDefFile::~CDefFile() = default;
 
 const std::map<size_t, size_t > CDefFile::getEntries() const

+ 1 - 0
client/render/CDefFile.h

@@ -45,6 +45,7 @@ public:
 
 	//load frame as SDL_Surface
 	void loadFrame(size_t frame, size_t group, IImageLoader &loader) const;
+	bool hasFrame(size_t frame, size_t group) const;
 
 	const std::map<size_t, size_t> getEntries() const;
 };

+ 10 - 3
client/renderSDL/RenderHandler.cpp

@@ -239,13 +239,13 @@ std::shared_ptr<const ISharedImage> RenderHandler::loadImageImpl(const ImageLoca
 
 std::shared_ptr<const ISharedImage> RenderHandler::loadImageFromFileUncached(const ImageLocator & locator)
 {
-	if (locator.image)
+	if(locator.image)
 	{
 		// TODO: create EmptySharedImage class that will be instantiated if image does not exists or fails to load
 		return std::make_shared<SDLImageShared>(*locator.image, locator.preScaledFactor);
 	}
 
-	if (locator.defFile)
+	if(locator.defFile)
 	{
 		auto defFile = getAnimationFile(*locator.defFile);
 		int preScaledFactor = locator.preScaledFactor;
@@ -258,7 +258,14 @@ std::shared_ptr<const ISharedImage> RenderHandler::loadImageFromFileUncached(con
 			preScaledFactor = 1;
 			defFile = getAnimationFile(AnimationPath::builtin(tmpPath));
 		}
-		return std::make_shared<SDLImageShared>(defFile.get(), locator.defFrame, locator.defGroup, preScaledFactor);
+		if(defFile->hasFrame(locator.defFrame, locator.defGroup))
+			return std::make_shared<SDLImageShared>(defFile.get(), locator.defFrame, locator.defGroup, preScaledFactor);
+		else
+		{
+			logGlobal->error("Frame %d in group %d not found in file: %s", 
+				locator.defFrame, locator.defGroup, locator.defFile->getName().c_str());
+			return std::make_shared<SDLImageShared>(ImagePath::builtin("DEFAULT"), locator.preScaledFactor);
+		}
 	}
 
 	throw std::runtime_error("Invalid image locator received!");

+ 14 - 7
client/renderSDL/SDL_Extensions.cpp

@@ -671,10 +671,6 @@ SDL_Surface * CSDL_Ext::scaleSurfaceIntegerFactor(SDL_Surface * surf, int factor
 	const uint32_t * srcPixels = static_cast<const uint32_t*>(intermediate->pixels);
 	uint32_t * dstPixels = static_cast<uint32_t*>(ret->pixels);
 
-	// avoid excessive granulation - xBRZ prefers at least 8-16 lines per task
-	// TODO: compare performance and size of images, recheck values for potentially better parameters
-	const int granulation = std::clamp(surf->h / 64 * 8, 8, 64);
-
 	switch (algorithm)
 	{
 		case EScalingAlgorithm::NEAREST:
@@ -687,11 +683,22 @@ SDL_Surface * CSDL_Ext::scaleSurfaceIntegerFactor(SDL_Surface * surf, int factor
 		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)
+
+			if(intermediate->h < 32)
 			{
+				// for tiny images tbb incurs too high overhead
+				xbrz::scale(factor, srcPixels, dstPixels, intermediate->w, intermediate->h, format, {});
+			}
+			else
+			{
+				// xbrz recommends granulation of 16, but according to tests, for smaller images granulation of 4 is actually the best option
+				const int granulation = intermediate->h > 400 ? 16 : 4;
+				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, format, {}, r.begin(), r.end());
+				});
+			}
 
-				xbrz::scale(factor, srcPixels, dstPixels, intermediate->w, intermediate->h, format, {}, r.begin(), r.end());
-			});
 			break;
 		}
 		default:

+ 13 - 0
client/renderSDL/ScreenHandler.cpp

@@ -356,6 +356,10 @@ EUpscalingFilter ScreenHandler::loadUpscalingFilter() const
 		return filter;
 
 	// else - autoselect
+#ifdef VCMI_MOBILE
+	// to help with performance - only if player explicitly enabled xbrz
+	return EUpscalingFilter::NONE;
+#else
 	Point outputResolution = getRenderResolution();
 	Point logicalResolution = getPreferredLogicalResolution();
 
@@ -363,6 +367,14 @@ EUpscalingFilter ScreenHandler::loadUpscalingFilter() const
 	float scaleY = static_cast<float>(outputResolution.x) / logicalResolution.x;
 	float scaling = std::min(scaleX, scaleY);
 
+	if (scaling <= 1.001f)
+		return EUpscalingFilter::NONE; // running at original resolution or even lower than that - no need for xbrz
+	else
+		return EUpscalingFilter::XBRZ_2;
+#endif
+
+#if 0
+// Old version, most optimal, but rather performance-heavy
 	if (scaling <= 1.001f)
 		return EUpscalingFilter::NONE; // running at original resolution or even lower than that - no need for xbrz
 	if (scaling <= 2.001f)
@@ -371,6 +383,7 @@ EUpscalingFilter ScreenHandler::loadUpscalingFilter() const
 		return EUpscalingFilter::XBRZ_3; // resolutions below 2400p (including 1440p and 2160p / 4K)
 
 	return EUpscalingFilter::XBRZ_4; // Only for massive displays, e.g. 8K
+#endif
 }
 
 void ScreenHandler::selectUpscalingFilter()

+ 3 - 2
client/widgets/CGarrisonInt.cpp

@@ -33,6 +33,7 @@
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/networkPacks/ArtifactLocation.h"
 #include "../../lib/gameState/CGameState.h"
+#include "../../lib/gameState/UpgradeInfo.h"
 
 void CGarrisonSlot::setHighlight(bool on)
 {
@@ -162,10 +163,10 @@ std::function<void()> CGarrisonSlot::getDismiss() const
 /// @return Whether the view should be refreshed
 bool CGarrisonSlot::viewInfo()
 {
-	UpgradeInfo pom;
+	UpgradeInfo pom(ID.getNum());
 	LOCPLINT->cb->fillUpgradeInfo(getObj(), ID, pom);
 
-	bool canUpgrade = getObj()->tempOwner == LOCPLINT->playerID && pom.oldID != CreatureID::NONE; //upgrade is possible
+	bool canUpgrade = getObj()->tempOwner == LOCPLINT->playerID && pom.canUpgrade(); //upgrade is possible
 	std::function<void(CreatureID)> upgr = nullptr;
 	auto dism = getDismiss();
 	if(canUpgrade) upgr = [=] (CreatureID newID) { LOCPLINT->cb->upgradeCreature(getObj(), ID, newID); };

+ 5 - 5
client/windows/CCastleInterface.cpp

@@ -157,7 +157,7 @@ void CBuildingRect::showPopupWindow(const Point & cursorPosition)
 
 	BuildingID bid = getBuilding()->bid;
 	const CBuilding *bld = town->getTown()->buildings.at(bid);
-	if (bid < BuildingID::DWELL_FIRST)
+	if (!bid.IsDwelling())
 	{
 		CRClickPopup::createAndPush(CInfoWindow::genText(bld->getNameTranslated(), bld->getDescriptionTranslated()),
 									std::make_shared<CComponent>(ComponentType::BUILDING, BuildingTypeUniqueID(bld->town->faction->getId(), bld->bid)));
@@ -751,7 +751,7 @@ bool CCastleBuildings::buildingTryActivateCustomUI(BuildingID buildingToTest, Bu
 		return true;
 	}
 
-	if (buildingToTest >= BuildingID::DWELL_FIRST)
+	if (buildingToTest.IsDwelling())
 	{
 		enterDwelling((BuildingID::getLevelFromDwelling(buildingToTest)));
 		return true;
@@ -815,7 +815,7 @@ bool CCastleBuildings::buildingTryActivateCustomUI(BuildingID buildingToTest, Bu
 				case BuildingSubID::CASTLE_GATE:
 						if (LOCPLINT->makingTurn)
 						{
-							enterCastleGate();
+							enterCastleGate(buildingToTest);
 							return true;
 						}
 						return false;
@@ -902,7 +902,7 @@ void CCastleBuildings::enterBuilding(BuildingID building)
 	LOCPLINT->showInfoDialog( town->getTown()->buildings.find(building)->second->getDescriptionTranslated(), comps);
 }
 
-void CCastleBuildings::enterCastleGate()
+void CCastleBuildings::enterCastleGate(BuildingID building)
 {
 	if (!town->visitingHero)
 	{
@@ -929,7 +929,7 @@ void CCastleBuildings::enterCastleGate()
 		}
 	}
 
-	auto gateIcon = std::make_shared<CAnimImage>(town->getTown()->clientInfo.buildingsIcons, BuildingID::CASTLE_GATE);//will be deleted by selection window
+	auto gateIcon = std::make_shared<CAnimImage>(town->getTown()->clientInfo.buildingsIcons, building);//will be deleted by selection window
 	auto wnd = std::make_shared<CObjectListWindow>(availableTowns, gateIcon, CGI->generaltexth->jktexts[40],
 		CGI->generaltexth->jktexts[41], std::bind (&CCastleInterface::castleTeleport, LOCPLINT->castleInt, _1), 0, images);
 	wnd->onPopup = [availableTowns](int index) { CRClickPopup::createAndPush(LOCPLINT->cb->getObjInstance(ObjectInstanceID(availableTowns[index])), GH.getCursorPosition()); };

+ 1 - 1
client/windows/CCastleInterface.h

@@ -152,7 +152,7 @@ class CCastleBuildings : public CIntObject
 
 	void enterBlacksmith(BuildingID building, ArtifactID artifactID);//support for blacksmith + ballista yard
 	void enterBuilding(BuildingID building);//for buildings with simple description + pic left-click messages
-	void enterCastleGate();
+	void enterCastleGate(BuildingID building);
 	void enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades);//Rampart's fountains
 	
 	void openMagesGuild();

+ 14 - 6
client/windows/CCreatureWindow.cpp

@@ -35,9 +35,11 @@
 #include "../../lib/IGameSettings.h"
 #include "../../lib/entities/hero/CHeroHandler.h"
 #include "../../lib/gameState/CGameState.h"
+#include "../../lib/gameState/UpgradeInfo.h"
 #include "../../lib/networkPacks/ArtifactLocation.h"
 #include "../../lib/texts/CGeneralTextHandler.h"
 #include "../../lib/texts/TextOperations.h"
+#include "../../lib/bonuses/Propagators.h"
 
 class CCreatureArtifactInstance;
 class CSelectableSkill;
@@ -57,6 +59,10 @@ public:
 	};
 	struct StackUpgradeInfo
 	{
+		StackUpgradeInfo() = delete;
+		StackUpgradeInfo(const UpgradeInfo & upgradeInfo)
+			: info(upgradeInfo)
+		{ }
 		UpgradeInfo info;
 		std::function<void(CreatureID)> callback;
 	};
@@ -355,15 +361,15 @@ CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset)
 		// besides - should commander really be upgradeable?
 
 		auto & upgradeInfo = parent->info->upgradeInfo.value();
-		const size_t buttonsToCreate = std::min<size_t>(upgradeInfo.info.newID.size(), upgrade.size());
+		const size_t buttonsToCreate = std::min<size_t>(upgradeInfo.info.size(), upgrade.size());
 
 		for(size_t buttonIndex = 0; buttonIndex < buttonsToCreate; buttonIndex++)
 		{
-			TResources totalCost = upgradeInfo.info.cost[buttonIndex] * parent->info->creatureCount;
+			TResources totalCost = upgradeInfo.info.getAvailableUpgradeCosts().at(buttonIndex) * parent->info->creatureCount;
 
 			auto onUpgrade = [=]()
 			{
-				upgradeInfo.callback(upgradeInfo.info.newID[buttonIndex]);
+				upgradeInfo.callback(upgradeInfo.info.getAvailableUpgrades().at(buttonIndex));
 				parent->close();
 			};
 			auto onClick = [=]()
@@ -385,7 +391,7 @@ CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset)
 			};
 			auto upgradeBtn = std::make_shared<CButton>(Point(221 + (int)buttonIndex * 40, 5), AnimationPath::builtin("stackWindow/upgradeButton"), CGI->generaltexth->zelp[446], onClick);
 
-			upgradeBtn->setOverlay(std::make_shared<CAnimImage>(AnimationPath::builtin("CPRSMALL"), VLC->creh->objects[upgradeInfo.info.newID[buttonIndex]]->getIconIndex()));
+			upgradeBtn->setOverlay(std::make_shared<CAnimImage>(AnimationPath::builtin("CPRSMALL"), VLC->creh->objects[upgradeInfo.info.getAvailableUpgrades()[buttonIndex]]->getIconIndex()));
 
 			if(buttonsToCreate == 1) // single upgrade available
 				upgradeBtn->assignedKey = EShortcut::RECRUITMENT_UPGRADE;
@@ -763,9 +769,8 @@ CStackWindow::CStackWindow(const CStackInstance * stack, std::function<void()> d
 	info->creature = stack->getCreature();
 	info->creatureCount = stack->count;
 
-	info->upgradeInfo = std::make_optional(UnitView::StackUpgradeInfo());
+	info->upgradeInfo = std::make_optional(UnitView::StackUpgradeInfo(upgradeInfo));
 	info->dismissInfo = std::make_optional(UnitView::StackDismissInfo());
-	info->upgradeInfo->info = upgradeInfo;
 	info->upgradeInfo->callback = callback;
 	info->dismissInfo->callback = dismiss;
 	info->owner = dynamic_cast<const CGHeroInstance *> (stack->armyObj);
@@ -852,6 +857,9 @@ void CStackWindow::initBonusesList()
 		bonusInfo.imagePath = info->stackNode->bonusToGraphics(b);
 		bonusInfo.bonusSource = b->source;
 
+		if(b->sid.getNum() != info->stackNode->getId() && b->propagator && b->propagator->getPropagatorType() == CBonusSystemNode::HERO) // Shows bonus with "propagator":"HERO" only at creature with bonus
+			continue;
+
 		//if it's possible to give any description or image for this kind of bonus
 		//TODO: figure out why half of bonuses don't have proper description
 		if(!bonusInfo.name.empty() || !bonusInfo.imagePath.empty())

+ 1 - 1
client/windows/CCreatureWindow.h

@@ -19,7 +19,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 class CCommanderInstance;
 class CStackInstance;
 class CStack;
-struct UpgradeInfo;
+class UpgradeInfo;
 
 VCMI_LIB_NAMESPACE_END
 

+ 1 - 1
client/windows/CHeroWindow.cpp

@@ -284,7 +284,7 @@ void CHeroWindow::update()
 
 	dismissButton->block(noDismiss);
 
-	if(curHero->valOfBonuses(Selector::type()(BonusType::BEFORE_BATTLE_REPOSITION)) == 0)
+	if(curHero->valOfBonuses(BonusType::BEFORE_BATTLE_REPOSITION) == 0)
 	{
 		tacticsButton->block(true);
 	}

+ 28 - 22
client/windows/GUIClasses.cpp

@@ -53,6 +53,7 @@
 #include "../lib/gameState/CGameState.h"
 #include "../lib/gameState/SThievesGuildInfo.h"
 #include "../lib/gameState/TavernHeroesPool.h"
+#include "../lib/gameState/UpgradeInfo.h"
 #include "../lib/texts/CGeneralTextHandler.h"
 #include "../lib/IGameSettings.h"
 #include "ConditionalWait.h"
@@ -1153,12 +1154,15 @@ void CHillFortWindow::updateGarrisons()
 		State newState = getState(SlotID(i));
 		if(newState != State::EMPTY)
 		{
-			UpgradeInfo info;
-			LOCPLINT->cb->fillUpgradeInfo(hero, SlotID(i), info);
-			if(info.newID.size())//we have upgrades here - update costs
+			if(const CStackInstance * s = hero->getStackPtr(SlotID(i)))
 			{
-				costs[i] = info.cost.back() * hero->getStackCount(SlotID(i));
-				totalSum += costs[i];
+				UpgradeInfo info(s->getCreature()->getId());
+				LOCPLINT->cb->fillUpgradeInfo(hero, SlotID(i), info);
+				if(info.canUpgrade())	//we have upgrades here - update costs
+				{
+					costs[i] = info.getUpgradeCosts() * hero->getStackCount(SlotID(i));
+					totalSum += costs[i];
+				}
 			}
 		}
 
@@ -1264,9 +1268,12 @@ void CHillFortWindow::makeDeal(SlotID slot)
 			{
 				if(slot.getNum() == i || ( slot.getNum() == slotsCount && currState[i] == State::MAKE_UPGRADE ))//this is activated slot or "upgrade all"
 				{
-					UpgradeInfo info;
-					LOCPLINT->cb->fillUpgradeInfo(hero, SlotID(i), info);
-					LOCPLINT->cb->upgradeCreature(hero, SlotID(i), info.newID.back());
+					if(const CStackInstance * s = hero->getStackPtr(SlotID(i)))
+					{
+						UpgradeInfo info(s->getCreatureID());
+						LOCPLINT->cb->fillUpgradeInfo(hero, SlotID(i), info);
+						LOCPLINT->cb->upgradeCreature(hero, SlotID(i), info.getUpgrade());
+					}
 				}
 			}
 			break;
@@ -1295,18 +1302,15 @@ CHillFortWindow::State CHillFortWindow::getState(SlotID slot)
 	if(hero->slotEmpty(slot))
 		return State::EMPTY;
 
-	UpgradeInfo info;
+	UpgradeInfo info(hero->getStackPtr(slot)->getCreatureID());
 	LOCPLINT->cb->fillUpgradeInfo(hero, slot, info);
-	if (info.newID.empty())
-	{
-		// Hill Fort may limit level of upgradeable creatures, e.g. mini Hill Fort from HOTA
-		if (hero->getCreature(slot)->hasUpgrades())
-			return State::UNAVAILABLE;
+	if(info.hasUpgrades() && !info.canUpgrade())
+		return State::UNAVAILABLE;  // Hill Fort may limit level of upgradeable creatures, e.g. mini Hill Fort from HOTA
 
+	if(!info.hasUpgrades())
 		return State::ALREADY_UPGRADED;
-	}
 
-	if(!(info.cost.back() * hero->getStackCount(slot)).canBeAfforded(myRes))
+	if(!(info.getUpgradeCosts() * hero->getStackCount(slot)).canBeAfforded(myRes))
 		return State::UNAFFORDABLE;
 
 	return State::MAKE_UPGRADE;
@@ -1394,15 +1398,17 @@ CThievesGuildWindow::CThievesGuildWindow(const CGObjectInstance * _owner):
 			bestHeroes.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("PortraitsSmall"), iter.second.getIconIndex(), 0, 260 + 66 * counter, 360));
 			//TODO: r-click info:
 			// - r-click on hero
-			// - r-click on primary skill label
 			if(iter.second.details)
 			{
-				primSkillHeaders.push_back(std::make_shared<CTextBox>(CGI->generaltexth->allTexts[184], Rect(260 + 66*counter, 396, 52, 64),
-					0, FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE));
-
-				for(int i=0; i<iter.second.details->primskills.size(); ++i)
+				std::vector<std::string> lines;
+				boost::split(lines, CGI->generaltexth->allTexts[184], boost::is_any_of("\n"));
+				for(int i=0; i<GameConstants::PRIMARY_SKILLS; ++i)
 				{
-					primSkillValues.push_back(std::make_shared<CLabel>(310 + 66 * counter, 407 + 11*i, FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE,
+					primSkillHeaders.push_back(std::make_shared<CLabel>(260 + 66 * counter, 407 + 11 * i, FONT_TINY, ETextAlignment::BOTTOMLEFT, Colors::WHITE, lines[i]));
+					primSkillHeadersArea.push_back(std::make_shared<LRClickableArea>(Rect(primSkillHeaders.back()->pos.x - pos.x, primSkillHeaders.back()->pos.y - pos.y - 11, 50, 11), nullptr, [i]{
+						CRClickPopup::createAndPush(CGI->generaltexth->arraytxt[2 + i]);
+					}));
+					primSkillValues.push_back(std::make_shared<CLabel>(310 + 66 * counter, 407 + 11 * i, FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE,
 							   std::to_string(iter.second.details->primskills[i])));
 				}
 			}

+ 2 - 1
client/windows/GUIClasses.h

@@ -502,7 +502,8 @@ class CThievesGuildWindow : public CStatusbarWindow
 
 	std::vector<std::shared_ptr<CPicture>> banners;
 	std::vector<std::shared_ptr<CAnimImage>> bestHeroes;
-	std::vector<std::shared_ptr<CTextBox>> primSkillHeaders;
+	std::vector<std::shared_ptr<CLabel>> primSkillHeaders;
+	std::vector<std::shared_ptr<LRClickableArea>> primSkillHeadersArea;
 	std::vector<std::shared_ptr<CLabel>> primSkillValues;
 	std::vector<std::shared_ptr<CAnimImage>> bestCreatures;
 	std::vector<std::shared_ptr<CLabel>> personalities;

+ 85 - 0
client/windows/InfoWindows.cpp

@@ -15,12 +15,14 @@
 #include "../PlayerLocalState.h"
 
 #include "../adventureMap/AdventureMapInterface.h"
+#include "../adventureMap/CMinimap.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/CursorHandler.h"
 #include "../gui/Shortcut.h"
 #include "../gui/WindowHandler.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/CComponent.h"
+#include "../widgets/GraphicalPrimitiveCanvas.h"
 #include "../widgets/Images.h"
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/TextControls.h"
@@ -331,6 +333,83 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGCreature * creature)
 	fitToScreen(10);
 }
 
+TeleporterPopup::TeleporterPopup(const Point & position, const CGTeleport * teleporter)
+	: CWindowObject(BORDERED | RCLICK_POPUP)
+{
+	OBJECT_CONSTRUCTION;
+	pos.w = 322;
+	pos.h = 200;
+
+	Rect areaSurface(11, 41, 144, 144);
+	Rect areaUnderground(167, 41, 144, 144);
+
+	Rect borderSurface(10, 40, 147, 147);
+	Rect borderUnderground(166, 40, 147, 147);
+
+	bool singleLevelMap = LOCPLINT->cb->getMapSize().y == 0;
+
+	if (singleLevelMap)
+	{
+		areaSurface.x += 144;
+		borderSurface.x += 144;
+	}
+
+	filledBackground = std::make_shared<FilledTexturePlayerColored>(Rect(0, 0, pos.w, pos.h));
+
+	backgroundSurface = std::make_shared<TransparentFilledRectangle>(borderSurface, Colors::TRANSPARENCY, Colors::YELLOW);
+	surface = std::make_shared<CMinimapInstance>(areaSurface.topLeft(), areaSurface.dimensions(), 0);
+
+	if (!singleLevelMap)
+	{
+		backgroundUnderground = std::make_shared<TransparentFilledRectangle>(borderUnderground, Colors::TRANSPARENCY, Colors::YELLOW);
+		undergroud = std::make_shared<CMinimapInstance>(areaUnderground.topLeft(), areaUnderground.dimensions(), 1);
+	}
+
+	labelTitle = std::make_shared<CLabel>(pos.w / 2, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, teleporter->getPopupText(LOCPLINT->playerID));
+
+	const auto & entrances = teleporter->getAllEntrances();
+	const auto & exits = teleporter->getAllExits();
+
+	std::set<ObjectInstanceID> allTeleporters;
+	allTeleporters.insert(entrances.begin(), entrances.end());
+	allTeleporters.insert(exits.begin(), exits.end());
+
+	for (const auto exit : allTeleporters)
+	{
+		const auto * exitObject = LOCPLINT->cb->getObj(exit, false);
+
+		if (!exitObject)
+			continue;
+
+		int3 position = exitObject->visitablePos();
+
+		int positionX = 144 * position.x / LOCPLINT->cb->getMapSize().x;
+		int positionY = 144 * position.y / LOCPLINT->cb->getMapSize().y;
+
+		Point iconPosition(positionX, positionY);
+
+		iconPosition -= Point(8,8); // compensate for 16x16 icon half-size
+
+		if (position.z == 0)
+			iconPosition += areaSurface.topLeft();
+		else
+			iconPosition += areaUnderground.topLeft();
+
+		ImagePath image;
+
+		if (!vstd::contains(entrances, exit))
+			image = ImagePath::builtin("portalExit");
+		else if (!vstd::contains(exits, exit))
+			image = ImagePath::builtin("portalEntrance");
+		else
+			image = ImagePath::builtin("portalBidirectional");
+
+		iconsOverlay.push_back(std::make_shared<CPicture>(image, iconPosition));
+	}
+
+	center(position);
+}
+
 std::shared_ptr<WindowBase>
 CRClickPopup::createCustomInfoWindow(Point position, const CGObjectInstance * specific) //specific=0 => draws info about selected town/hero
 {
@@ -354,6 +433,12 @@ CRClickPopup::createCustomInfoWindow(Point position, const CGObjectInstance * sp
 		case Obj::GARRISON:
 		case Obj::GARRISON2:
 			return std::make_shared<CInfoBoxPopup>(position, dynamic_cast<const CGGarrison *>(specific));
+		case Obj::MONOLITH_ONE_WAY_ENTRANCE:
+		case Obj::MONOLITH_ONE_WAY_EXIT:
+		case Obj::MONOLITH_TWO_WAY:
+		case Obj::SUBTERRANEAN_GATE:
+		case Obj::WHIRLPOOL:
+			return std::make_shared<TeleporterPopup>(position, dynamic_cast<const CGTeleport *>(specific));
 		default:
 			return std::shared_ptr<WindowBase>();
 	}

+ 23 - 0
client/windows/InfoWindows.h

@@ -20,6 +20,7 @@ class CGTownInstance;
 class CGHeroInstance;
 class CGGarrison;
 class CGCreature;
+class CGTeleport;
 
 VCMI_LIB_NAMESPACE_END
 
@@ -29,6 +30,10 @@ class CSelectableComponent;
 class CTextBox;
 class CButton;
 class CFilledTexture;
+class FilledTexturePlayerColored;
+class TransparentFilledRectangle;
+class CMinimapInstance;
+class CLabel;
 
 /// text + comp. + ok button
 class CInfoWindow : public WindowBase
@@ -110,3 +115,21 @@ public:
 	void madeChoiceAndClose();
 	CSelWindow(const std::string & text, PlayerColor player, int charperline, const std::vector<std::shared_ptr<CSelectableComponent>> & comps, const std::vector<std::pair<AnimationPath,CFunctionList<void()> > > &Buttons, QueryID askID);
 };
+
+class TeleporterPopup : public CWindowObject
+{
+	std::shared_ptr<FilledTexturePlayerColored> filledBackground;
+
+	std::shared_ptr<TransparentFilledRectangle> backgroundSurface;
+	std::shared_ptr<TransparentFilledRectangle> backgroundUnderground;
+
+	std::shared_ptr<CMinimapInstance> surface;
+	std::shared_ptr<CMinimapInstance> undergroud;
+
+	std::shared_ptr<CLabel> labelTitle;
+
+	std::vector<std::shared_ptr<CPicture>> iconsOverlay;
+public:
+	TeleporterPopup(const Point & position, const CGTeleport * teleporter);
+};
+

+ 1 - 1
cmake_modules/VersionDefinition.cmake

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

+ 29 - 24
config/ai/nkai/nkai-settings.json

@@ -33,17 +33,18 @@
 	
 	
 	"pawn" : {
-		"maxRoamingHeroes" : 4, //H3 value: 3,
-		"maxpass" : 30,
+		"maxRoamingHeroes" : 3, //H3 value: 3,
+		"maxPass" : 30,
+		"maxPriorityPass" : 10,
 		"mainHeroTurnDistanceLimit" : 10,
 		"scoutHeroTurnDistanceLimit" : 5,
 		"maxGoldPressure" : 0.3,
 		"updateHitmapOnTileReveal" : false,
 		"useTroopsFromGarrisons" : true,
 		"openMap": true,
-		"allowObjectGraph": false,
-		"pathfinderBucketsCount" : 1, // old value: 3,
-		"pathfinderBucketSize" : 32, // old value: 7,
+		"allowObjectGraph": true,
+		"pathfinderBucketsCount" : 4, // old value: 3,
+		"pathfinderBucketSize" : 8, // old value: 7,
 		"retreatThresholdRelative" : 0,
 		"retreatThresholdAbsolute" : 0,
 		"safeAttackRatio" : 1.1,
@@ -52,17 +53,18 @@
 	},
 	
 	"knight" : {
-		"maxRoamingHeroes" : 6, //H3 value: 3,
-		"maxpass" : 30,
+		"maxRoamingHeroes" : 3, //H3 value: 3,
+		"maxPass" : 30,
+		"maxPriorityPass" : 10,
 		"mainHeroTurnDistanceLimit" : 10,
 		"scoutHeroTurnDistanceLimit" : 5,
 		"maxGoldPressure" : 0.3,
 		"updateHitmapOnTileReveal" : false,
 		"useTroopsFromGarrisons" : true,
 		"openMap": true,
-		"allowObjectGraph": false,
-		"pathfinderBucketsCount" : 1, // old value: 3,
-		"pathfinderBucketSize" : 32, // old value: 7,
+		"allowObjectGraph": true,
+		"pathfinderBucketsCount" : 4, // old value: 3,
+		"pathfinderBucketSize" : 8, // old value: 7,
 		"retreatThresholdRelative" : 0.1,
 		"retreatThresholdAbsolute" : 5000,
 		"safeAttackRatio" : 1.1,
@@ -71,17 +73,18 @@
 	},
 	
 	"rook" : {
-		"maxRoamingHeroes" : 8, //H3 value: 4
-		"maxpass" : 30,
+		"maxRoamingHeroes" : 4, //H3 value: 4
+		"maxPass" : 30,
+		"maxPriorityPass" : 10,
 		"mainHeroTurnDistanceLimit" : 10,
 		"scoutHeroTurnDistanceLimit" : 5,
 		"maxGoldPressure" : 0.3,
 		"updateHitmapOnTileReveal" : false,
 		"useTroopsFromGarrisons" : true,
 		"openMap": true,
-		"allowObjectGraph": false,
-		"pathfinderBucketsCount" : 1, // old value: 3,
-		"pathfinderBucketSize" : 32, // old value: 7,
+		"allowObjectGraph": true,
+		"pathfinderBucketsCount" : 4, // old value: 3,
+		"pathfinderBucketSize" : 8, // old value: 7,
 		"retreatThresholdRelative" : 0.3,
 		"retreatThresholdAbsolute" : 10000,
 		"safeAttackRatio" : 1.1,
@@ -90,17 +93,18 @@
 	},
 	
 	"queen" : {
-		"maxRoamingHeroes" : 8, //H3 value: 5
-		"maxpass" : 30,
+		"maxRoamingHeroes" : 6, //H3 value: 5
+		"maxPass" : 30,
+		"maxPriorityPass" : 10,
 		"mainHeroTurnDistanceLimit" : 10,
 		"scoutHeroTurnDistanceLimit" : 5,
 		"maxGoldPressure" : 0.3,
 		"updateHitmapOnTileReveal" : false,
 		"useTroopsFromGarrisons" : true,
 		"openMap": true,
-		"allowObjectGraph": false,
-		"pathfinderBucketsCount" : 1, // old value: 3,
-		"pathfinderBucketSize" : 32, // old value: 7,
+		"allowObjectGraph": true,
+		"pathfinderBucketsCount" : 4, // old value: 3,
+		"pathfinderBucketSize" : 8, // old value: 7,
 		"retreatThresholdRelative" : 0.3,
 		"retreatThresholdAbsolute" : 10000,
 		"safeAttackRatio" : 1.1,
@@ -110,16 +114,17 @@
 	
 	"king" : {
 		"maxRoamingHeroes" : 8, //H3 value: 6
-		"maxpass" : 30,
+		"maxPass" : 30,
+		"maxPriorityPass" : 10,
 		"mainHeroTurnDistanceLimit" : 10,
 		"scoutHeroTurnDistanceLimit" : 5,
 		"maxGoldPressure" : 0.3,
 		"updateHitmapOnTileReveal" : false,
 		"useTroopsFromGarrisons" : true,
 		"openMap": true,
-		"allowObjectGraph": false,
-		"pathfinderBucketsCount" : 1, // old value: 3,
-		"pathfinderBucketSize" : 32, // old value: 7,
+		"allowObjectGraph": true,
+		"pathfinderBucketsCount" : 4, // old value: 3,
+		"pathfinderBucketSize" : 8, // old value: 7,
 		"retreatThresholdRelative" : 0.3,
 		"retreatThresholdAbsolute" : 10000,
 		"safeAttackRatio" : 1.1,

+ 12 - 0
config/bonuses.json

@@ -407,6 +407,14 @@
 		}
 	},
 
+	"OPENING_BATTLE_SPELL":
+	{
+		"graphics":
+		{
+			"icon":  "zvs/Lib1.res/E_SPDFIRE"
+		}
+	},
+
 	"RANDOM_SPELLCASTER":
 	{
 		"graphics":
@@ -532,6 +540,10 @@
 		}
 	},
 
+	"SPELL_SCHOOL_IMMUNITY":
+	{
+	},
+
 	"SPELL_RESISTANCE_AURA":
 	{
 		"graphics":

+ 6 - 0
debian/changelog

@@ -1,3 +1,9 @@
+vcmi (1.6.1) jammy; urgency=medium
+
+  * New upstream release
+
+ -- Ivan Savenko <[email protected]>  Fri, 27 Dec 2024 12:00:00 +0200
+
 vcmi (1.6.0) jammy; urgency=medium
 
   * New upstream release

+ 1 - 0
docs/Readme.md

@@ -2,6 +2,7 @@
 
 [![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.6.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.6.0)
+[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.6.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.6.1)
 [![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.

+ 2 - 1
include/vcmi/Creature.h

@@ -23,7 +23,8 @@ class DLL_LINKAGE ACreature: public AFactionMember
 {
 public:
 	bool isLiving() const; //non-undead, non-non living or alive
-	ui32 getMovementRange(int turn = 0) const; //get speed (in moving tiles) of creature with all modificators
+	ui32 getMovementRange(int turn) const; //get speed (in moving tiles) of creature with all modificators
+	ui32 getMovementRange() const; //get speed (in moving tiles) of creature with all modificators
 	virtual ui32 getMaxHealth() const; //get max HP of stack with all modifiers
 };
 

+ 13 - 9
include/vstd/CLoggerBase.h

@@ -58,6 +58,7 @@ public:
 
 	virtual void log(ELogLevel::ELogLevel level, const std::string & message) const = 0;
 	virtual void log(ELogLevel::ELogLevel level, const boost::format & fmt) const = 0;
+	virtual ELogLevel::ELogLevel getEffectiveLevel() const = 0;
 
 	/// Returns true if a debug/trace log message will be logged, false if not.
 	/// Useful if performance is important and concatenating the log message is a expensive task.
@@ -67,16 +68,19 @@ public:
 	template<typename T, typename ... Args>
 	void log(ELogLevel::ELogLevel level, const std::string & format, T t, Args ... args) const
 	{
-		try
+		if (getEffectiveLevel() <= level)
 		{
-			boost::format fmt(format);
-			makeFormat(fmt, t, args...);
-			log(level, fmt);
-		}
-		catch(...)
-		{
-			log(ELogLevel::ERROR, "Log formatting failed, format was:");
-			log(ELogLevel::ERROR, format);
+			try
+			{
+				boost::format fmt(format);
+				makeFormat(fmt, t, args...);
+				log(level, fmt);
+			}
+			catch(...)
+			{
+				log(ELogLevel::ERROR, "Log formatting failed, format was:");
+				log(ELogLevel::ERROR, format);
+			}
 		}
 	}
 

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

@@ -90,6 +90,7 @@
 	</screenshots>
 	<launchable type="desktop-id">vcmilauncher.desktop</launchable>
 	<releases>
+		<release version="1.6.1" date="2024-12-27" type="stable"/>
 		<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.6" date="2024-08-04" type="stable"/>

+ 8 - 6
launcher/firstLaunch/firstlaunch_moc.cpp

@@ -19,6 +19,7 @@
 #include "../../lib/texts/Languages.h"
 #include "../../lib/VCMIDirs.h"
 #include "../../lib/filesystem/Filesystem.h"
+#include "../../vcmiqt/MessageBox.h"
 #include "../helper.h"
 #include "../languages.h"
 #include "../innoextract.h"
@@ -122,7 +123,7 @@ void FirstLaunchView::on_pushButtonDataCopy_clicked()
 #else
 	// iOS can't display modal dialogs when called directly on button press
 	// https://bugreports.qt.io/browse/QTBUG-98651
-	QTimer::singleShot(0, this, [this]{ copyHeroesData(); });
+	MessageBoxCustom::showDialog(this, [this]{ copyHeroesData(); });
 #endif
 }
 
@@ -130,7 +131,7 @@ void FirstLaunchView::on_pushButtonGogInstall_clicked()
 {
 	// iOS can't display modal dialogs when called directly on button press
 	// https://bugreports.qt.io/browse/QTBUG-98651
-	QTimer::singleShot(0, this, &FirstLaunchView::extractGogData);
+	MessageBoxCustom::showDialog(this, [this]{extractGogData();});
 }
 
 void FirstLaunchView::enterSetup()
@@ -188,10 +189,10 @@ void FirstLaunchView::activateTabModPreset()
 	modPresetUpdate();
 }
 
-void FirstLaunchView::exitSetup()
+void FirstLaunchView::exitSetup(bool goToMods)
 {
 	if(auto * mainWindow = dynamic_cast<MainWindow *>(QApplication::activeWindow()))
-		mainWindow->exitSetup();
+		mainWindow->exitSetup(goToMods);
 }
 
 // Tab Language
@@ -547,7 +548,7 @@ void FirstLaunchView::modPresetUpdate()
 
 	// we can't install anything - either repository checkout is off or all recommended mods are already installed
 	if (!checkCanInstallTranslation() && !checkCanInstallExtras() && !checkCanInstallHota() && !checkCanInstallWog())
-		exitSetup();
+		exitSetup(false);
 }
 
 QString FirstLaunchView::findTranslationModName()
@@ -624,7 +625,8 @@ void FirstLaunchView::on_pushButtonPresetNext_clicked()
 	if (ui->buttonPresetHota->isChecked() && checkCanInstallHota())
 		modsToInstall.push_back("hota");
 
-	exitSetup();
+	bool goToMods = !modsToInstall.empty();
+	exitSetup(goToMods);
 
 	for (auto const & modName : modsToInstall)
 		getModView()->doInstallMod(modName);

+ 1 - 1
launcher/firstLaunch/firstlaunch_moc.h

@@ -29,7 +29,7 @@ class FirstLaunchView : public QWidget
 	void activateTabLanguage();
 	void activateTabHeroesData();
 	void activateTabModPreset();
-	void exitSetup();
+	void exitSetup(bool goToMods);
 	
 	// Tab Language
 	void languageSelected(const QString & languageCode);

+ 15 - 4
launcher/mainwindow_moc.cpp

@@ -18,6 +18,7 @@
 #include "../lib/filesystem/Filesystem.h"
 #include "../lib/logging/CBasicLogConfigurator.h"
 #include "../lib/texts/Languages.h"
+#include "../lib/ExceptionsCommon.h"
 
 #include "updatedialog_moc.h"
 #include "main.h"
@@ -35,8 +36,15 @@ void MainWindow::load()
 	CBasicLogConfigurator logConfig(VCMIDirs::get().userLogsPath() / "VCMI_Launcher_log.txt", console);
 	logConfig.configureDefault();
 
-	CResourceHandler::initialize();
-	CResourceHandler::load("config/filesystem.json");
+	try
+	{
+		CResourceHandler::initialize();
+		CResourceHandler::load("config/filesystem.json");
+	}
+	catch (const DataLoadingException & e)
+	{
+		QMessageBox::critical(this, tr("Error starting executable"), QString::fromStdString(e.what()));
+	}
 
 	Helper::loadSettings();
 }
@@ -149,7 +157,7 @@ void MainWindow::enterSetup()
 	ui->tabListWidget->setCurrentIndex(TabRows::SETUP);
 }
 
-void MainWindow::exitSetup()
+void MainWindow::exitSetup(bool goToMods)
 {
 	Settings writer = settings.write["launcher"]["setupCompleted"];
 	writer->Bool() = true;
@@ -158,7 +166,10 @@ void MainWindow::exitSetup()
 	ui->settingsButton->setEnabled(true);
 	ui->aboutButton->setEnabled(true);
 	ui->modslistButton->setEnabled(true);
-	ui->tabListWidget->setCurrentIndex(TabRows::MODS);
+	if (goToMods)
+		ui->tabListWidget->setCurrentIndex(TabRows::MODS);
+	else
+		ui->tabListWidget->setCurrentIndex(TabRows::START);
 }
 
 void MainWindow::switchToStartTab()

+ 1 - 1
launcher/mainwindow_moc.h

@@ -62,7 +62,7 @@ public:
 	
 	void detectPreferredLanguage();
 	void enterSetup();
-	void exitSetup();
+	void exitSetup(bool goToMods);
 	void switchToModsTab();
 	void switchToStartTab();
 

+ 2 - 2
launcher/modManager/cdownloadmanager_moc.cpp

@@ -122,7 +122,7 @@ void CDownloadManager::downloadFinished(QNetworkReply * reply)
 	}
 
 	if(downloadComplete)
-		emit finished(successful, failed, encounteredErrors);
+		finished(successful, failed, encounteredErrors);
 
 	file.reply->deleteLater();
 	file.reply = nullptr;
@@ -149,7 +149,7 @@ void CDownloadManager::downloadProgressChanged(qint64 bytesReceived, qint64 byte
 	if(received > total)
 		total = received;
 
-	emit downloadProgress(received, total);
+	downloadProgress(received, total);
 }
 
 bool CDownloadManager::downloadInProgress(const QUrl & url) const

+ 34 - 5
launcher/modManager/cmodlistview_moc.cpp

@@ -561,7 +561,7 @@ QStringList CModListView::getModsToInstall(QString mod)
 				potentialToInstall = modStateModel->getTopParent(potentialToInstall);
 		}
 
-		if (modStateModel->isModExists(potentialToInstall) && !modStateModel->isModInstalled(potentialToInstall))
+		if (!modStateModel->isModInstalled(potentialToInstall))
 			result.push_back(potentialToInstall);
 
 		if (modStateModel->isModExists(potentialToInstall))
@@ -818,13 +818,14 @@ void CModListView::installFiles(QStringList files)
 			ChroniclesExtractor ce(this, [&prog](float progress) { prog = progress; });
 			ce.installChronicles(exe);
 			reload();
-			enableModByName("chronicles");
+			if (modStateModel->isModExists("chronicles"))
+				enableModByName("chronicles");
 			return true;
 		});
 		
 		while(futureExtract.wait_for(std::chrono::milliseconds(10)) != std::future_status::ready)
 		{
-			emit extractionProgress(static_cast<int>(prog * 1000.f), 1000);
+			extractionProgress(static_cast<int>(prog * 1000.f), 1000);
 			qApp->processEvents();
 		}
 		
@@ -857,6 +858,12 @@ void CModListView::installMods(QStringList archives)
 		modNames.push_back(modName);
 	}
 
+	if (!activatingPreset.isEmpty())
+	{
+		modStateModel->activatePreset(activatingPreset);
+		activatingPreset.clear();
+	}
+
 	// uninstall old version of mod, if installed
 	for(QString mod : modNames)
 	{
@@ -925,7 +932,7 @@ void CModListView::on_pushButton_clicked()
 
 void CModListView::modelReset()
 {
-	selectMod(filterModel->rowCount() > 0 ? filterModel->index(0, 0) : QModelIndex());
+	ui->allModsView->setCurrentIndex(filterModel->rowCount() > 0 ? filterModel->index(0, 0) : QModelIndex());
 }
 
 void CModListView::checkManagerErrors()
@@ -1006,7 +1013,7 @@ void CModListView::doInstallMod(const QString & modName)
 
 bool CModListView::isModAvailable(const QString & modName)
 {
-	return !modStateModel->isModInstalled(modName);
+	return modStateModel->isModExists(modName) && !modStateModel->isModInstalled(modName);
 }
 
 bool CModListView::isModEnabled(const QString & modName)
@@ -1147,3 +1154,25 @@ QString CModListView::getActivePreset() const
 {
 	return modStateModel->getActivePreset();
 }
+
+JsonNode CModListView::exportCurrentPreset() const
+{
+	return modStateModel->exportCurrentPreset();
+}
+
+void CModListView::importPreset(const JsonNode & data)
+{
+	const auto & [presetName, modList] = modStateModel->importPreset(data);
+
+	if (modList.empty())
+	{
+		modStateModel->activatePreset(presetName);
+		modStateModel->reloadLocalState();
+	}
+	else
+	{
+		activatingPreset = presetName;
+		for (const auto & modID : modList)
+			doInstallMod(modID);
+	}
+}

+ 4 - 4
launcher/modManager/cmodlistview_moc.h

@@ -37,6 +37,7 @@ class CModListView : public QWidget
 	CModFilterModel * filterModel;
 	CDownloadManager * dlManager;
 	JsonNode accumulatedRepositoryData;
+	QString activatingPreset;
 
 	QStringList enqueuedModDownloads;
 
@@ -97,17 +98,16 @@ public:
 	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;
 
+	JsonNode exportCurrentPreset() const;
+	void importPreset(const JsonNode & data);
+
 	/// returns true if mod is currently enabled
 	bool isModEnabled(const QString & modName);
 

+ 1 - 1
launcher/modManager/modstatecontroller.cpp

@@ -214,7 +214,7 @@ bool ModStateController::doInstallMod(QString modname, QString archivePath)
 	
 	while(futureExtract.wait_for(std::chrono::milliseconds(10)) != std::future_status::ready)
 	{
-		emit extractionProgress(filesCounter, filesToExtract.size());
+		extractionProgress(filesCounter, filesToExtract.size());
 		qApp->processEvents();
 	}
 	

+ 1 - 1
launcher/modManager/modstateitemmodel_moc.cpp

@@ -206,7 +206,7 @@ void ModStateItemModel::modChanged(QString modID)
 	int index = modNameToID.indexOf(modID);
 	QModelIndex parent = this->parent(createIndex(0, 0, index));
 	int row = modIndex[modIndexToName(parent)].indexOf(modID);
-	emit dataChanged(createIndex(row, 0, index), createIndex(row, 4, index));
+	dataChanged(createIndex(row, 0, index), createIndex(row, 4, index));
 }
 
 void ModStateItemModel::endResetModel()

+ 14 - 1
launcher/modManager/modstatemodel.cpp

@@ -65,7 +65,7 @@ bool ModStateModel::isModExists(QString modName) const
 
 bool ModStateModel::isModInstalled(QString modName) const
 {
-	return getMod(modName).isInstalled();
+	return isModExists(modName) && getMod(modName).isInstalled();
 }
 
 bool ModStateModel::isModSettingEnabled(QString rootModName, QString modSettingName) const
@@ -157,3 +157,16 @@ QString ModStateModel::getActivePreset() const
 {
 	return QString::fromStdString(modManager->getActivePreset());
 }
+
+JsonNode ModStateModel::exportCurrentPreset() const
+{
+	return modManager->exportCurrentPreset();
+}
+
+std::tuple<QString, QStringList> ModStateModel::importPreset(const JsonNode & data)
+{
+	std::tuple<QString, QStringList> result;
+	const auto & [presetName, modList] = modManager->importPreset(data);
+
+	return {QString::fromStdString(presetName), stringListStdToQt(modList)};
+}

+ 3 - 0
launcher/modManager/modstatemodel.h

@@ -57,4 +57,7 @@ public:
 
 	QStringList getAllPresets() const;
 	QString getActivePreset() const;
+
+	JsonNode exportCurrentPreset() const;
+	std::tuple<QString, QStringList> importPreset(const JsonNode & data);
 };

+ 92 - 45
launcher/startGame/StartGameTab.cpp

@@ -19,6 +19,7 @@
 
 #include "../../lib/filesystem/Filesystem.h"
 #include "../../lib/VCMIDirs.h"
+#include "../../vcmiqt/MessageBox.h"
 
 void StartGameTab::changeEvent(QEvent *event)
 {
@@ -44,12 +45,41 @@ StartGameTab::StartGameTab(QWidget * parent)
 	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
+
+	auto clipboard = QGuiApplication::clipboard();
+
+	connect(clipboard, SIGNAL(dataChanged()), this, SLOT(clipboardDataChanged()));
+}
+
+void StartGameTab::clipboardDataChanged()
+{
+	ui->buttonPresetExport->setIcon(QIcon{});// reset icon, if any
+
+	auto clipboard = QGuiApplication::clipboard();
+	QString clipboardText = clipboard->text().trimmed();
+
+	if (clipboardText.isEmpty())
+	{
+		ui->buttonPresetImport->setEnabled(false);
+	}
+	else
+	{
+		// this *may* be json, try parsing it
+		if (clipboardText.startsWith('{'))
+		{
+			QByteArray presetBytes(clipboardText.toUtf8());
+			const JsonNode presetJson(reinterpret_cast<const std::byte*>(presetBytes.data()), presetBytes.size(), "preset in clipboard");
+			bool presetValid = !presetJson["name"].String().empty() && !presetJson["mods"].Vector().empty();
+
+			ui->buttonPresetImport->setEnabled(presetValid);
+		}
+		else
+			ui->buttonPresetImport->setEnabled(false);
+	}
 }
 
 StartGameTab::~StartGameTab()
@@ -72,6 +102,8 @@ void StartGameTab::refreshState()
 	refreshTranslation(getMainWindow()->getTranslationStatus());
 	refreshPresets();
 	refreshMods();
+
+	clipboardDataChanged();
 }
 
 void StartGameTab::refreshPresets()
@@ -227,7 +259,7 @@ void StartGameTab::on_buttonImportFiles_clicked()
 
 	// iOS can't display modal dialogs when called directly on button press
 	// https://bugreports.qt.io/browse/QTBUG-98651
-	QTimer::singleShot(0, this, importFunctor);
+	MessageBoxCustom::showDialog(this, importFunctor);
 }
 
 void StartGameTab::on_buttonInstallTranslation_clicked()
@@ -269,7 +301,7 @@ void StartGameTab::on_buttonHelpImportFiles_clicked()
 		" - VCMI configuration files (.json)\n"
 	);
 
-	QMessageBox::information(this, ui->buttonImportFiles->text(), message);
+	MessageBoxCustom::information(this, ui->buttonImportFiles->text(), message);
 }
 
 void StartGameTab::on_buttonInstallTranslationHelp_clicked()
@@ -279,7 +311,7 @@ void StartGameTab::on_buttonInstallTranslationHelp_clicked()
 		"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);
+	MessageBoxCustom::information(this, ui->buttonInstallTranslation->text(), message);
 }
 
 void StartGameTab::on_buttonActivateTranslationHelp_clicked()
@@ -289,7 +321,7 @@ void StartGameTab::on_buttonActivateTranslationHelp_clicked()
 		"Use this option to enable it."
 	);
 
-	QMessageBox::information(this, ui->buttonActivateTranslation->text(), message);
+	MessageBoxCustom::information(this, ui->buttonActivateTranslation->text(), message);
 }
 
 void StartGameTab::on_buttonUpdateModsHelp_clicked()
@@ -298,10 +330,10 @@ void StartGameTab::on_buttonUpdateModsHelp_clicked()
 		"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."
+		"You may want to postpone mod update until you finish any of your ongoing games."
 		);
 
-	QMessageBox::information(this, ui->buttonUpdateMods->text(), message);
+	MessageBoxCustom::information(this, ui->buttonUpdateMods->text(), message);
 }
 
 void StartGameTab::on_buttonChroniclesHelp_clicked()
@@ -314,7 +346,7 @@ void StartGameTab::on_buttonChroniclesHelp_clicked()
 		"This will generate and install mod for VCMI that contains imported chronicles"
 	);
 
-	QMessageBox::information(this, ui->labelChronicles->text(), message);
+	MessageBoxCustom::information(this, ui->labelChronicles->text(), message);
 }
 
 void StartGameTab::on_buttonMissingSoundtrackHelp_clicked()
@@ -325,7 +357,7 @@ void StartGameTab::on_buttonMissingSoundtrackHelp_clicked()
 		"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);
+	MessageBoxCustom::information(this, ui->labelMissingSoundtrack->text(), message);
 }
 
 void StartGameTab::on_buttonMissingVideoHelp_clicked()
@@ -336,7 +368,7 @@ void StartGameTab::on_buttonMissingVideoHelp_clicked()
 		"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);
+	MessageBoxCustom::information(this, ui->labelMissingVideo->text(), message);
 }
 
 void StartGameTab::on_buttonMissingFilesHelp_clicked()
@@ -347,7 +379,7 @@ void StartGameTab::on_buttonMissingFilesHelp_clicked()
 		"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);
+	MessageBoxCustom::information(this, ui->labelMissingFiles->text(), message);
 }
 
 void StartGameTab::on_buttonMissingCampaignsHelp_clicked()
@@ -358,36 +390,49 @@ void StartGameTab::on_buttonMissingCampaignsHelp_clicked()
 		"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);
+	MessageBoxCustom::information(this, ui->labelMissingCampaigns->text(), message);
 }
 
 void StartGameTab::on_buttonPresetExport_clicked()
 {
-	// TODO
+	JsonNode presetJson = getMainWindow()->getModView()->exportCurrentPreset();
+	QString presetString = QString::fromStdString(presetJson.toCompactString());
+	QGuiApplication::clipboard()->setText(presetString);
+
+	ui->buttonPresetExport->setIcon(QIcon{":/icons/mod-enabled.png"});
 }
 
 void StartGameTab::on_buttonPresetImport_clicked()
 {
-	// TODO
+	QString presetString = QGuiApplication::clipboard()->text();
+	QByteArray presetBytes(presetString.toUtf8());
+	JsonNode presetJson(reinterpret_cast<const std::byte*>(presetBytes.data()), presetBytes.size(), "imported preset");
+
+	getMainWindow()->getModView()->importPreset(presetJson);
+	getMainWindow()->switchToModsTab();
+	refreshPresets();
 }
 
 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();
-	}
+	const auto & functor = [this](){
+		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();
+		}
+	};
+	MessageBoxCustom::showDialog(this, functor);
 }
 
 void StartGameTab::on_buttonPresetDelete_clicked()
@@ -411,21 +456,23 @@ void StartGameTab::on_comboBoxModPresets_currentTextChanged(const QString &prese
 
 void StartGameTab::on_buttonPresetRename_clicked()
 {
-	QString currentName = getMainWindow()->getModView()->getActivePreset();
+	const auto & functor = [this](){
+		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);
+		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();
-	}
+		if (ok && !newName.isEmpty())
+		{
+			getMainWindow()->getModView()->renamePreset(currentName, newName);
+			refreshPresets();
+		}
+	};
+	MessageBoxCustom::showDialog(this, functor);
 }
-

+ 1 - 6
launcher/startGame/StartGameTab.h

@@ -65,19 +65,14 @@ private slots:
 	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();
 
+	void clipboardDataChanged();
 private:
 	Ui::StartGameTab * ui;
 };

File diff suppressed because it is too large
+ 104 - 436
launcher/translation/chinese.ts


File diff suppressed because it is too large
+ 167 - 189
launcher/translation/czech.ts


+ 176 - 193
launcher/translation/english.ts

@@ -95,222 +95,222 @@
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.ui" line="66"/>
+        <location filename="../modManager/cmodlistview_moc.ui" line="69"/>
         <source>All mods</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.ui" line="71"/>
+        <location filename="../modManager/cmodlistview_moc.ui" line="74"/>
         <source>Downloadable</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.ui" line="76"/>
+        <location filename="../modManager/cmodlistview_moc.ui" line="79"/>
         <source>Installed</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.ui" line="81"/>
+        <location filename="../modManager/cmodlistview_moc.ui" line="84"/>
         <source>Updatable</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.ui" line="86"/>
+        <location filename="../modManager/cmodlistview_moc.ui" line="89"/>
         <source>Active</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.ui" line="91"/>
+        <location filename="../modManager/cmodlistview_moc.ui" line="94"/>
         <source>Inactive</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.ui" line="163"/>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="379"/>
+        <location filename="../modManager/cmodlistview_moc.ui" line="166"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="380"/>
         <source>Description</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.ui" line="211"/>
+        <location filename="../modManager/cmodlistview_moc.ui" line="214"/>
         <source>Changelog</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.ui" line="233"/>
+        <location filename="../modManager/cmodlistview_moc.ui" line="236"/>
         <source>Screenshots</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.ui" line="391"/>
+        <location filename="../modManager/cmodlistview_moc.ui" line="397"/>
         <source>Uninstall</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.ui" line="422"/>
+        <location filename="../modManager/cmodlistview_moc.ui" line="428"/>
         <source>Enable</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.ui" line="453"/>
+        <location filename="../modManager/cmodlistview_moc.ui" line="459"/>
         <source>Disable</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.ui" line="484"/>
+        <location filename="../modManager/cmodlistview_moc.ui" line="490"/>
         <source>Update</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.ui" line="515"/>
+        <location filename="../modManager/cmodlistview_moc.ui" line="521"/>
         <source>Install</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.ui" line="329"/>
+        <location filename="../modManager/cmodlistview_moc.ui" line="335"/>
         <source> %p% (%v KB out of %m KB)</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.ui" line="105"/>
+        <location filename="../modManager/cmodlistview_moc.ui" line="108"/>
         <source>Reload repositories</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.ui" line="342"/>
+        <location filename="../modManager/cmodlistview_moc.ui" line="348"/>
         <source>Abort</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="301"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="302"/>
         <source>Mod name</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="304"/>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="310"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="305"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="311"/>
         <source>Installed version</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="305"/>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="312"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="306"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="313"/>
         <source>Latest version</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="316"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="317"/>
         <source>Size</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="319"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="320"/>
         <source>Download size</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="321"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="322"/>
         <source>Authors</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="324"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="325"/>
         <source>License</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="327"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="328"/>
         <source>Contact</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="336"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="337"/>
         <source>Compatibility</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="338"/>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="346"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="339"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="347"/>
         <source>Required VCMI version</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="344"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="345"/>
         <source>Supported VCMI version</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="344"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="345"/>
         <source>please upgrade mod</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="173"/>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="753"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="174"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="761"/>
         <source>mods repository index</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="346"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="347"/>
         <source>or newer</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="349"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="350"/>
         <source>Supported VCMI versions</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="365"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="366"/>
         <source>Languages</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="377"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="378"/>
         <source>Required mods</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="378"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="379"/>
         <source>Conflicting mods</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="383"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="384"/>
         <source>This mod cannot be enabled because it translates into a different language.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="384"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="385"/>
         <source>This mod can not be enabled because the following dependencies are not present</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="385"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="386"/>
         <source>This mod can not be installed because the following dependencies are not present</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="386"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="387"/>
         <source>This is a submod and it cannot be installed or uninstalled separately from its parent mod</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="405"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="406"/>
         <source>Notes</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="649"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="655"/>
         <source>Downloading %1. %p% (%v MB out of %m MB) finished</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="674"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="680"/>
         <source>Download failed</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="675"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="681"/>
         <source>Unable to download all files.
 
 Encountered errors:
@@ -319,45 +319,45 @@ Encountered errors:
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="676"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="682"/>
         <source>
 
 Install successfully downloaded?</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="791"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="810"/>
         <source>Installing Heroes Chronicles</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="853"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="890"/>
         <source>Installing mod %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="898"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="943"/>
         <source>Operation failed</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="899"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="944"/>
         <source>Encountered errors:
 </source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="934"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="979"/>
         <source>screenshots</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="940"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="985"/>
         <source>Screenshot %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/cmodlistview_moc.cpp" line="296"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="297"/>
         <source>Mod is incompatible</source>
         <translation type="unfinished"></translation>
     </message>
@@ -782,49 +782,42 @@ Fullscreen Exclusive Mode - the game will cover the entirety of your screen and
 <context>
     <name>ChroniclesExtractor</name>
     <message>
-        <location filename="../modManager/chroniclesextractor.cpp" line="56"/>
-        <location filename="../modManager/chroniclesextractor.cpp" line="71"/>
+        <location filename="../modManager/chroniclesextractor.cpp" line="61"/>
         <source>Invalid file selected</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/chroniclesextractor.cpp" line="48"/>
-        <source>The file cannot be opened</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location filename="../modManager/chroniclesextractor.cpp" line="56"/>
-        <source>You have to select a gog installer file!</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location filename="../modManager/chroniclesextractor.cpp" line="71"/>
+        <location filename="../modManager/chroniclesextractor.cpp" line="61"/>
         <source>You have to select a Heroes Chronicles installer file!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/chroniclesextractor.cpp" line="88"/>
+        <location filename="../modManager/chroniclesextractor.cpp" line="76"/>
         <source>Extracting error!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/chroniclesextractor.cpp" line="90"/>
+        <location filename="../modManager/chroniclesextractor.cpp" line="78"/>
         <source>Hash error!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/chroniclesextractor.cpp" line="107"/>
-        <location filename="../modManager/chroniclesextractor.cpp" line="108"/>
-        <location filename="../modManager/chroniclesextractor.cpp" line="159"/>
+        <location filename="../modManager/chroniclesextractor.cpp" line="95"/>
+        <location filename="../modManager/chroniclesextractor.cpp" line="96"/>
         <source>Heroes Chronicles</source>
         <translation type="unfinished"></translation>
     </message>
+    <message>
+        <location filename="../modManager/chroniclesextractor.cpp" line="144"/>
+        <source>Heroes Chronicles %1 - %2</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>File size</name>
     <message>
         <location filename="../modManager/modstate.cpp" line="140"/>
-        <location filename="../modManager/modstatemodel.cpp" line="95"/>
+        <location filename="../modManager/modstatemodel.cpp" line="93"/>
         <source>%1 MiB</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1011,105 +1004,105 @@ Offline installer consists of two files: &quot;.exe&quot; and &quot;.bin&quot; -
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="174"/>
+        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="175"/>
         <source>Heroes III installation found!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="174"/>
+        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="175"/>
         <source>Copy data to VCMI folder?</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="321"/>
+        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="322"/>
         <source>Select %1 file...</source>
         <comment>param is file extension</comment>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="322"/>
+        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="323"/>
         <source>You have to select %1 file!</source>
         <comment>param is file extension</comment>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="324"/>
+        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="325"/>
         <source>GOG file (*.*)</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="325"/>
+        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="326"/>
         <source>File selection</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="334"/>
+        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="335"/>
         <source>File cannot be opened</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="340"/>
+        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="341"/>
         <source>Invalid file selected</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="350"/>
+        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="351"/>
         <source>GOG installer</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="392"/>
+        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="396"/>
         <source>You&apos;ve provided a GOG Galaxy installer! This file doesn&apos;t contain the game. Please download the offline backup game installer!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="486"/>
+        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="490"/>
         <source>Heroes III: HD Edition files are not supported by VCMI.
 Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="491"/>
+        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="495"/>
         <source>Unknown or unsupported Heroes III version found.
 Please select the directory with Heroes III: Complete Edition or Heroes III: Shadow of Death.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="347"/>
+        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="348"/>
         <source>GOG data</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="413"/>
+        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="417"/>
         <source>Extracting error!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="415"/>
+        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="419"/>
         <source>Hash error!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="418"/>
+        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="422"/>
         <source>No Heroes III data!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="418"/>
+        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="422"/>
         <source>Selected files do not contain Heroes III data!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="462"/>
+        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="466"/>
         <source>Failed to detect valid Heroes III data in chosen directory.
 Please select the directory with installed Heroes III data.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="465"/>
-        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="479"/>
-        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="486"/>
-        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="491"/>
+        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="469"/>
+        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="483"/>
+        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="490"/>
+        <location filename="../firstLaunch/firstlaunch_moc.cpp" line="495"/>
         <source>Heroes III data not found!</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1141,38 +1134,38 @@ error reason: </source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../innoextract.cpp" line="132"/>
+        <location filename="../innoextract.cpp" line="134"/>
         <source>SHA1 hash of provided files:
 Exe (%1 bytes):
 %2</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../innoextract.cpp" line="134"/>
+        <location filename="../innoextract.cpp" line="136"/>
         <source>
 Bin (%1 bytes):
 %2</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../innoextract.cpp" line="137"/>
+        <location filename="../innoextract.cpp" line="139"/>
         <source>Internal copy process failed. Enough space on device?
 
 %1</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../innoextract.cpp" line="146"/>
+        <location filename="../innoextract.cpp" line="148"/>
         <source>Exe</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../innoextract.cpp" line="146"/>
+        <location filename="../innoextract.cpp" line="148"/>
         <source>Bin</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../innoextract.cpp" line="155"/>
+        <location filename="../innoextract.cpp" line="157"/>
         <source>Language mismatch!
 %1
 
@@ -1180,7 +1173,7 @@ Bin (%1 bytes):
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../innoextract.cpp" line="157"/>
+        <location filename="../innoextract.cpp" line="159"/>
         <source>Only one file known! Maybe files are corrupted? Please download again.
 %1
 
@@ -1188,7 +1181,7 @@ Bin (%1 bytes):
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../innoextract.cpp" line="163"/>
+        <location filename="../innoextract.cpp" line="165"/>
         <source>Unknown files! Maybe files are corrupted? Please download again.
 
 %1</source>
@@ -1296,32 +1289,37 @@ Bin (%1 bytes):
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../mainwindow_moc.ui" line="99"/>
+        <location filename="../mainwindow_moc.ui" line="150"/>
         <source>Settings</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../mainwindow_moc.ui" line="145"/>
+        <location filename="../mainwindow_moc.ui" line="196"/>
         <source>Help</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../mainwindow_moc.ui" line="209"/>
+        <location filename="../mainwindow_moc.ui" line="58"/>
         <source>Game</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../mainwindow_moc.ui" line="53"/>
+        <location filename="../mainwindow_moc.ui" line="104"/>
         <source>Mods</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../mainwindow_moc.cpp" line="264"/>
+        <location filename="../mainwindow_moc.cpp" line="46"/>
+        <source>Error starting executable</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../mainwindow_moc.cpp" line="284"/>
         <source>Replace config file?</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../mainwindow_moc.cpp" line="264"/>
+        <location filename="../mainwindow_moc.cpp" line="284"/>
         <source>Do you want to replace %1?</source>
         <translation type="unfinished"></translation>
     </message>
@@ -1342,64 +1340,59 @@ Bin (%1 bytes):
 <context>
     <name>ModStateController</name>
     <message>
-        <location filename="../modManager/modstatecontroller.cpp" line="126"/>
+        <location filename="../modManager/modstatecontroller.cpp" line="129"/>
         <source>Can not install submod</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/modstatecontroller.cpp" line="129"/>
+        <location filename="../modManager/modstatecontroller.cpp" line="132"/>
         <source>Mod is already installed</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/modstatecontroller.cpp" line="138"/>
+        <location filename="../modManager/modstatecontroller.cpp" line="141"/>
         <source>Can not uninstall submod</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/modstatecontroller.cpp" line="141"/>
+        <location filename="../modManager/modstatecontroller.cpp" line="144"/>
         <source>Mod is not installed</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/modstatecontroller.cpp" line="151"/>
+        <location filename="../modManager/modstatecontroller.cpp" line="154"/>
         <source>Mod is already enabled</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/modstatecontroller.cpp" line="154"/>
-        <location filename="../modManager/modstatecontroller.cpp" line="180"/>
+        <location filename="../modManager/modstatecontroller.cpp" line="157"/>
+        <location filename="../modManager/modstatecontroller.cpp" line="183"/>
         <source>Mod must be installed first</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/modstatecontroller.cpp" line="158"/>
+        <location filename="../modManager/modstatecontroller.cpp" line="161"/>
         <source>Mod is not compatible, please update VCMI and check the latest mod revisions</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/modstatecontroller.cpp" line="161"/>
+        <location filename="../modManager/modstatecontroller.cpp" line="164"/>
         <source>Can not enable translation mod for a different language!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/modstatecontroller.cpp" line="166"/>
+        <location filename="../modManager/modstatecontroller.cpp" line="169"/>
         <source>Required mod %1 is missing</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/modstatecontroller.cpp" line="177"/>
+        <location filename="../modManager/modstatecontroller.cpp" line="180"/>
         <source>Mod is already disabled</source>
         <translation type="unfinished"></translation>
     </message>
-    <message>
-        <location filename="../modManager/modstatecontroller.cpp" line="190"/>
-        <source>Mod archive is missing</source>
-        <translation type="unfinished"></translation>
-    </message>
     <message>
         <location filename="../modManager/modstatecontroller.cpp" line="193"/>
-        <source>Mod with such name is already installed</source>
+        <source>Mod archive is missing</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
@@ -1413,12 +1406,12 @@ Bin (%1 bytes):
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/modstatecontroller.cpp" line="250"/>
+        <location filename="../modManager/modstatecontroller.cpp" line="248"/>
         <source>Data with this mod was not found</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../modManager/modstatecontroller.cpp" line="254"/>
+        <location filename="../modManager/modstatecontroller.cpp" line="252"/>
         <source>Mod is located in a protected directory, please remove it manually:
 </source>
         <translation type="unfinished"></translation>
@@ -1545,135 +1538,125 @@ Reason: %2</source>
 <context>
     <name>StartGameTab</name>
     <message>
-        <location filename="../startGame/StartGameTab.ui" line="14"/>
-        <source>Form</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location filename="../startGame/StartGameTab.ui" line="49"/>
+        <location filename="../startGame/StartGameTab.ui" line="597"/>
         <source>Import from Clipboard</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.ui" line="81"/>
+        <location filename="../startGame/StartGameTab.ui" line="616"/>
         <source>Rename Current Preset</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.ui" line="101"/>
-        <source>Current Preset</source>
-        <translation type="unfinished"></translation>
-    </message>
-    <message>
-        <location filename="../startGame/StartGameTab.ui" line="121"/>
+        <location filename="../startGame/StartGameTab.ui" line="530"/>
         <source>Create New Preset</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.ui" line="140"/>
+        <location filename="../startGame/StartGameTab.ui" line="511"/>
         <source>Export to Clipboard</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.ui" line="159"/>
+        <location filename="../startGame/StartGameTab.ui" line="565"/>
         <source>Delete Current Preset</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.ui" line="200"/>
+        <location filename="../startGame/StartGameTab.ui" line="119"/>
         <source>Unsupported or corrupted game data detected!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.ui" line="257"/>
-        <location filename="../startGame/StartGameTab.ui" line="276"/>
-        <location filename="../startGame/StartGameTab.ui" line="314"/>
-        <location filename="../startGame/StartGameTab.ui" line="333"/>
-        <location filename="../startGame/StartGameTab.ui" line="374"/>
-        <location filename="../startGame/StartGameTab.ui" line="415"/>
-        <location filename="../startGame/StartGameTab.ui" line="434"/>
-        <location filename="../startGame/StartGameTab.ui" line="475"/>
-        <location filename="../startGame/StartGameTab.ui" line="551"/>
+        <location filename="../startGame/StartGameTab.ui" line="141"/>
+        <location filename="../startGame/StartGameTab.ui" line="160"/>
+        <location filename="../startGame/StartGameTab.ui" line="201"/>
+        <location filename="../startGame/StartGameTab.ui" line="220"/>
+        <location filename="../startGame/StartGameTab.ui" line="353"/>
+        <location filename="../startGame/StartGameTab.ui" line="372"/>
+        <location filename="../startGame/StartGameTab.ui" line="413"/>
+        <location filename="../startGame/StartGameTab.ui" line="451"/>
+        <location filename="../startGame/StartGameTab.ui" line="470"/>
         <source>?</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.ui" line="295"/>
+        <location filename="../startGame/StartGameTab.ui" line="239"/>
         <source>Install Translation</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.ui" line="352"/>
+        <location filename="../startGame/StartGameTab.ui" line="391"/>
         <source>No soundtrack detected!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.ui" line="393"/>
+        <location filename="../startGame/StartGameTab.ui" line="258"/>
         <source>Armaggedon&apos;s Blade campaigns are missing!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.ui" line="453"/>
+        <location filename="../startGame/StartGameTab.ui" line="293"/>
         <source>No video files detected!</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.ui" line="494"/>
+        <location filename="../startGame/StartGameTab.ui" line="432"/>
         <source>Activate Translation</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.ui" line="513"/>
+        <location filename="../startGame/StartGameTab.ui" line="315"/>
         <source>Import files</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.ui" line="624"/>
+        <location filename="../startGame/StartGameTab.ui" line="701"/>
         <source>Check For Updates</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.ui" line="643"/>
+        <location filename="../startGame/StartGameTab.ui" line="720"/>
         <source>Go to Downloads Page</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.ui" line="662"/>
+        <location filename="../startGame/StartGameTab.ui" line="739"/>
         <source>Go to Changelog Page</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.ui" line="681"/>
+        <location filename="../startGame/StartGameTab.ui" line="657"/>
         <source>You are using the latest version</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.ui" line="718"/>
+        <location filename="../startGame/StartGameTab.ui" line="37"/>
         <source>Game Data Files</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.ui" line="736"/>
+        <location filename="../startGame/StartGameTab.ui" line="55"/>
         <source>Mod Preset</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.ui" line="781"/>
+        <location filename="../startGame/StartGameTab.ui" line="773"/>
         <source>Resume</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.ui" line="830"/>
+        <location filename="../startGame/StartGameTab.ui" line="834"/>
         <source>Play</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.ui" line="874"/>
+        <location filename="../startGame/StartGameTab.ui" line="801"/>
         <source>Editor</source>
         <translation type="unfinished"></translation>
     </message>
     <message numerus="yes">
-        <location filename="../startGame/StartGameTab.cpp" line="141"/>
+        <location filename="../startGame/StartGameTab.cpp" line="184"/>
         <source>Update %n mods</source>
         <translation type="unfinished">
             <numerusform></numerusform>
@@ -1681,7 +1664,7 @@ Reason: %2</source>
         </translation>
     </message>
     <message numerus="yes">
-        <location filename="../startGame/StartGameTab.cpp" line="145"/>
+        <location filename="../startGame/StartGameTab.cpp" line="188"/>
         <source>Heroes Chronicles:
 %n/%1 installed</source>
         <translation type="unfinished">
@@ -1690,52 +1673,52 @@ Reason: %2</source>
         </translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.cpp" line="162"/>
+        <location filename="../startGame/StartGameTab.cpp" line="205"/>
         <source>Update to %1 available</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.cpp" line="198"/>
+        <location filename="../startGame/StartGameTab.cpp" line="241"/>
         <source>All supported files</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.cpp" line="199"/>
+        <location filename="../startGame/StartGameTab.cpp" line="242"/>
         <source>Maps</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.cpp" line="200"/>
+        <location filename="../startGame/StartGameTab.cpp" line="243"/>
         <source>Campaigns</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.cpp" line="201"/>
+        <location filename="../startGame/StartGameTab.cpp" line="244"/>
         <source>Configs</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.cpp" line="202"/>
+        <location filename="../startGame/StartGameTab.cpp" line="245"/>
         <source>Mods</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.cpp" line="203"/>
+        <location filename="../startGame/StartGameTab.cpp" line="246"/>
         <source>Gog files</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.cpp" line="206"/>
+        <location filename="../startGame/StartGameTab.cpp" line="249"/>
         <source>All files (*.*)</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.cpp" line="208"/>
+        <location filename="../startGame/StartGameTab.cpp" line="251"/>
         <source>Select files (configs, mods, maps, campaigns, gog files) to install...</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.cpp" line="248"/>
+        <location filename="../startGame/StartGameTab.cpp" line="294"/>
         <source>This option allows you to import additional data files into your VCMI installation. At the moment, following options are supported:
 
  - Heroes III Maps (.h3m or .vmap).
@@ -1747,63 +1730,63 @@ Reason: %2</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.cpp" line="263"/>
+        <location filename="../startGame/StartGameTab.cpp" line="309"/>
         <source>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.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.cpp" line="273"/>
+        <location filename="../startGame/StartGameTab.cpp" line="319"/>
         <source>Translation of Heroes III into your language is installed, but has been turned off. Use this option to enable it.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.cpp" line="283"/>
+        <location filename="../startGame/StartGameTab.cpp" line="329"/>
         <source>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.
 
-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.</source>
+WARNING: In some cases, updated versions of mods may not be compatible with your existing saves. You may want to postpone mod update until you finish any of your ongoing games.</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.cpp" line="295"/>
+        <location filename="../startGame/StartGameTab.cpp" line="341"/>
         <source>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.
 To import Heroes Chronicles, download offline backup installer of each chronicle that you wish to install, select &apos;Import files&apos; option and select downloaded file. This will generate and install mod for VCMI that contains imported chronicles</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.cpp" line="308"/>
+        <location filename="../startGame/StartGameTab.cpp" line="354"/>
         <source>VCMI has detected that Heroes III music files are missing from your installation. VCMI will run, but in-game music will not be available.
 
 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</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.cpp" line="319"/>
+        <location filename="../startGame/StartGameTab.cpp" line="365"/>
         <source>VCMI has detected that Heroes III video files are missing from your installation. VCMI will run, but in-game cutscenes will not be available.
 
 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</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.cpp" line="330"/>
+        <location filename="../startGame/StartGameTab.cpp" line="376"/>
         <source>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.
 
 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</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.cpp" line="341"/>
+        <location filename="../startGame/StartGameTab.cpp" line="387"/>
         <source>VCMI has detected that some of Heroes III: Armageddon&apos;s Blade data files are missing from your installation. VCMI will work, but Armageddon&apos;s Blade campaigns will not be available.
 
 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</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.cpp" line="366"/>
+        <location filename="../startGame/StartGameTab.cpp" line="423"/>
         <source>Enter preset name:</source>
         <translation type="unfinished"></translation>
     </message>
     <message>
-        <location filename="../startGame/StartGameTab.cpp" line="406"/>
+        <location filename="../startGame/StartGameTab.cpp" line="466"/>
         <source>Rename preset &apos;%1&apos; to:</source>
         <translation type="unfinished"></translation>
     </message>

File diff suppressed because it is too large
+ 114 - 434
launcher/translation/french.ts


File diff suppressed because it is too large
+ 114 - 454
launcher/translation/german.ts


File diff suppressed because it is too large
+ 105 - 436
launcher/translation/polish.ts


File diff suppressed because it is too large
+ 177 - 295
launcher/translation/portuguese.ts


File diff suppressed because it is too large
+ 178 - 276
launcher/translation/russian.ts


File diff suppressed because it is too large
+ 149 - 322
launcher/translation/spanish.ts


File diff suppressed because it is too large
+ 170 - 451
launcher/translation/swedish.ts


File diff suppressed because it is too large
+ 114 - 430
launcher/translation/ukrainian.ts


File diff suppressed because it is too large
+ 150 - 315
launcher/translation/vietnamese.ts


+ 33 - 36
lib/BasicTypes.cpp

@@ -32,38 +32,27 @@ bool INativeTerrainProvider::isNativeTerrain(TerrainId terrain) const
 
 TerrainId AFactionMember::getNativeTerrain() const
 {
-	const std::string cachingStringNoTerrainPenalty = "type_TERRAIN_NATIVE_NONE";
-	static const auto selectorNoTerrainPenalty = Selector::typeSubtype(BonusType::TERRAIN_NATIVE, BonusSubtypeID());
-
 	//this code is used in the CreatureTerrainLimiter::limit to setup battle bonuses
 	//and in the CGHeroInstance::getNativeTerrain() to setup movement bonuses or/and penalties.
-	return getBonusBearer()->hasBonus(selectorNoTerrainPenalty, cachingStringNoTerrainPenalty)
+	return getBonusBearer()->hasBonusOfType(BonusType::TERRAIN_NATIVE)
 			 ? TerrainId::ANY_TERRAIN : getFactionID().toEntity(VLC)->getNativeTerrain();
 }
 
 int32_t AFactionMember::magicResistance() const
 {
-	si32 val = getBonusBearer()->valOfBonuses(Selector::type()(BonusType::MAGIC_RESISTANCE));
+	si32 val = getBonusBearer()->valOfBonuses(BonusType::MAGIC_RESISTANCE);
 	vstd::amin (val, 100);
 	return val;
 }
 
 int AFactionMember::getAttack(bool ranged) const
 {
-	const std::string cachingStr = "type_PRIMARY_SKILLs_ATTACK";
-
-	static const auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK));
-
-	return getBonusBearer()->valOfBonuses(selector, cachingStr);
+	return getBonusBearer()->valOfBonuses(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK));
 }
 
 int AFactionMember::getDefense(bool ranged) const
 {
-	const std::string cachingStr = "type_PRIMARY_SKILLs_DEFENSE";
-
-	static const auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE));
-
-	return getBonusBearer()->valOfBonuses(selector, cachingStr);
+	return getBonusBearer()->valOfBonuses(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE));
 }
 
 int AFactionMember::getMinDamage(bool ranged) const
@@ -82,11 +71,9 @@ int AFactionMember::getMaxDamage(bool ranged) const
 
 int AFactionMember::getPrimSkillLevel(PrimarySkill id) const
 {
-	static const CSelector selectorAllSkills = Selector::type()(BonusType::PRIMARY_SKILL);
-	static const std::string keyAllSkills = "type_PRIMARY_SKILL";
-	auto allSkills = getBonusBearer()->getBonuses(selectorAllSkills, keyAllSkills);
-	auto ret = allSkills->valOfBonuses(Selector::subtype()(BonusSubtypeID(id)));
-	auto minSkillValue = VLC->engineSettings()->getVector(EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS)[id.getNum()];
+	auto allSkills = getBonusBearer()->getBonusesOfType(BonusType::PRIMARY_SKILL);
+	int ret = allSkills->valOfBonuses(Selector::subtype()(BonusSubtypeID(id)));
+	int minSkillValue = VLC->engineSettings()->getVectorValue(EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS, id.getNum());
 	return std::max(ret, minSkillValue); //otherwise, some artifacts may cause negative skill value effect, sp=0 works in old saves
 }
 
@@ -114,9 +101,7 @@ int AFactionMember::moraleValAndBonusList(TConstBonusListPtr & bonusList) const
 		return 0;
 	}
 
-	static const auto moraleSelector = Selector::type()(BonusType::MORALE);
-	static const std::string cachingStrMor = "type_MORALE";
-	bonusList = getBonusBearer()->getBonuses(moraleSelector, cachingStrMor);
+	bonusList = getBonusBearer()->getBonusesOfType(BonusType::MORALE);
 
 	return std::clamp(bonusList->totalValue(), maxBadMorale, maxGoodMorale);
 }
@@ -140,9 +125,7 @@ int AFactionMember::luckValAndBonusList(TConstBonusListPtr & bonusList) const
 		return 0;
 	}
 
-	static const auto luckSelector = Selector::type()(BonusType::LUCK);
-	static const std::string cachingStrLuck = "type_LUCK";
-	bonusList = getBonusBearer()->getBonuses(luckSelector, cachingStrLuck);
+	bonusList = getBonusBearer()->getBonusesOfType(BonusType::LUCK);
 
 	return std::clamp(bonusList->totalValue(), maxBadLuck, maxGoodLuck);
 }
@@ -161,25 +144,39 @@ int AFactionMember::luckVal() const
 
 ui32 ACreature::getMaxHealth() const
 {
-	const std::string cachingStr = "type_STACK_HEALTH";
-	static const auto selector = Selector::type()(BonusType::STACK_HEALTH);
-	auto value = getBonusBearer()->valOfBonuses(selector, cachingStr);
+	auto value = getBonusBearer()->valOfBonuses(BonusType::STACK_HEALTH);
 	return std::max(1, value); //never 0
 }
 
+ui32 ACreature::getMovementRange() const
+{
+	//war machines cannot move
+	if (getBonusBearer()->hasBonusOfType(BonusType::SIEGE_WEAPON))
+		return 0;
+
+	if (getBonusBearer()->hasBonusOfType(BonusType::BIND_EFFECT))
+		return 0;
+
+	return getBonusBearer()->valOfBonuses(BonusType::STACKS_SPEED);
+}
+
 ui32 ACreature::getMovementRange(int turn) const
 {
+	if (turn == 0)
+		return getMovementRange();
+
+	const std::string cachingStrSW = "type_SIEGE_WEAPON_turns_" + std::to_string(turn);
+	const std::string cachingStrBE = "type_BIND_EFFECT_turns_" + std::to_string(turn);
+	const std::string cachingStrSS = "type_STACKS_SPEED_turns_" + std::to_string(turn);
+
 	//war machines cannot move
-	if(getBonusBearer()->hasBonus(Selector::type()(BonusType::SIEGE_WEAPON).And(Selector::turns(turn))))
-	{
+	if(getBonusBearer()->hasBonus(Selector::type()(BonusType::SIEGE_WEAPON).And(Selector::turns(turn)), cachingStrSW))
 		return 0;
-	}
-	if(getBonusBearer()->hasBonus(Selector::type()(BonusType::BIND_EFFECT).And(Selector::turns(turn))))
-	{
+
+	if(getBonusBearer()->hasBonus(Selector::type()(BonusType::BIND_EFFECT).And(Selector::turns(turn)), cachingStrBE))
 		return 0;
-	}
 
-	return getBonusBearer()->valOfBonuses(Selector::type()(BonusType::STACKS_SPEED).And(Selector::turns(turn)));
+	return getBonusBearer()->valOfBonuses(Selector::type()(BonusType::STACKS_SPEED).And(Selector::turns(turn)), cachingStrSS);
 }
 
 bool ACreature::isLiving() const //TODO: theoreticaly there exists "LIVING" bonus in stack experience documentation

+ 10 - 1
lib/CBonusTypeHandler.cpp

@@ -75,8 +75,17 @@ std::string CBonusTypeHandler::bonusToString(const std::shared_ptr<Bonus> & bonu
 	std::string textID = description ? bt.getDescriptionTextID() : bt.getNameTextID();
 	std::string text = VLC->generaltexth->translate(textID);
 
+	auto school = bonus->subtype.as<SpellSchool>();
+	if (school.hasValue() && school != SpellSchool::ANY)
+	{
+		std::string schoolName = school.encode(school.getNum());
+		std::string baseTextID = description ? bt.getDescriptionTextID() : bt.getNameTextID();
+		std::string fullTextID = baseTextID + '.' + schoolName;
+		text = VLC->generaltexth->translate(fullTextID);
+	}
+
 	if (text.find("${val}") != std::string::npos)
-		boost::algorithm::replace_all(text, "${val}", std::to_string(bearer->valOfBonuses(Selector::typeSubtype(bonus->type, bonus->subtype))));
+		boost::algorithm::replace_all(text, "${val}", std::to_string(bearer->valOfBonuses(bonus->type, bonus->subtype)));
 
 	if (text.find("${subtype.creature}") != std::string::npos && bonus->subtype.as<CreatureID>().hasValue())
 		boost::algorithm::replace_all(text, "${subtype.creature}", bonus->subtype.as<CreatureID>().toCreature()->getNamePluralTranslated());

+ 2 - 0
lib/CConsoleHandler.cpp

@@ -299,8 +299,10 @@ CConsoleHandler::CConsoleHandler():
 	GetConsoleScreenBufferInfo(handleErr, &csbi);
 	defErrColor = csbi.wAttributes;
 #ifdef NDEBUG
+#ifndef VCMI_ANDROID
 	SetUnhandledExceptionFilter(onUnhandledException);
 #endif
+#endif
 #else
 	defColor = "\x1b[0m";
 #endif

+ 1 - 1
lib/CGameInfoCallback.cpp

@@ -183,7 +183,7 @@ const IMarket * CGameInfoCallback::getMarket(ObjectInstanceID objid) const
 		return nullptr;
 }
 
-void CGameInfoCallback::fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo &out) const
+void CGameInfoCallback::fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo & out) const
 {
 	//boost::shared_lock<boost::shared_mutex> lock(*gs->mx);
 	ERROR_RET_IF(!canGetFullInfo(obj), "Cannot get info about not owned object!");

+ 2 - 2
lib/CGameInfoCallback.h

@@ -33,7 +33,7 @@ struct CPathsInfo;
 struct InfoAboutHero;
 struct InfoAboutTown;
 
-struct UpgradeInfo;
+class UpgradeInfo;
 struct SThievesGuildInfo;
 class CMapHeader;
 struct TeamState;
@@ -172,7 +172,7 @@ public:
 
 
 	//armed object
-	virtual void fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo &out)const;
+	virtual void fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo &out) const;
 
 	//hero
 	const CGHeroInstance * getHero(ObjectInstanceID objid) const override;

+ 1 - 0
lib/CMakeLists.txt

@@ -109,6 +109,7 @@ set(lib_MAIN_SRCS
 	gameState/RumorState.cpp
 	gameState/TavernHeroesPool.cpp
 	gameState/GameStatistics.cpp
+	gameState/UpgradeInfo.cpp
 
 	mapObjectConstructors/AObjectTypeHandler.cpp
 	mapObjectConstructors/CBankInstanceConstructor.cpp

+ 48 - 8
lib/CPlayerState.cpp

@@ -112,24 +112,24 @@ std::vector<T> PlayerState::getObjectsOfType() const
 	return result;
 }
 
-std::vector<const CGHeroInstance *> PlayerState::getHeroes() const
+const std::vector<const CGHeroInstance *> & PlayerState::getHeroes() const
 {
-	return getObjectsOfType<const CGHeroInstance *>();
+	return constOwnedHeroes;
 }
 
-std::vector<const CGTownInstance *> PlayerState::getTowns() const
+const std::vector<const CGTownInstance *> & PlayerState::getTowns() const
 {
-	return getObjectsOfType<const CGTownInstance *>();
+	return constOwnedTowns;
 }
 
-std::vector<CGHeroInstance *> PlayerState::getHeroes()
+const std::vector<CGHeroInstance *> & PlayerState::getHeroes()
 {
-	return getObjectsOfType<CGHeroInstance *>();
+	return ownedHeroes;
 }
 
-std::vector<CGTownInstance *> PlayerState::getTowns()
+const std::vector<CGTownInstance *> & PlayerState::getTowns()
 {
-	return getObjectsOfType<CGTownInstance *>();
+	return ownedTowns;
 }
 
 std::vector<const CGObjectInstance *> PlayerState::getOwnedObjects() const
@@ -141,11 +141,51 @@ void PlayerState::addOwnedObject(CGObjectInstance * object)
 {
 	assert(object->asOwnable() != nullptr);
 	ownedObjects.push_back(object);
+
+	auto * town = dynamic_cast<CGTownInstance*>(object);
+	auto * hero = dynamic_cast<CGHeroInstance*>(object);
+
+	if (town)
+	{
+		ownedTowns.push_back(town);
+		constOwnedTowns.push_back(town);
+	}
+
+	if (hero)
+	{
+		ownedHeroes.push_back(hero);
+		constOwnedHeroes.push_back(hero);
+	}
+}
+
+void PlayerState::postDeserialize()
+{
+	for (const auto& object : ownedObjects)
+	{
+		auto* town = dynamic_cast<CGTownInstance*>(object);
+		auto* hero = dynamic_cast<CGHeroInstance*>(object);
+
+		if (town)
+		{
+			ownedTowns.push_back(town);
+			constOwnedTowns.push_back(town);
+		}
+
+		if (hero)
+		{
+			ownedHeroes.push_back(hero);
+			constOwnedHeroes.push_back(hero);
+		}
+	}
 }
 
 void PlayerState::removeOwnedObject(CGObjectInstance * object)
 {
 	vstd::erase(ownedObjects, object);
+	vstd::erase(ownedTowns, object);
+	vstd::erase(constOwnedTowns, object);
+	vstd::erase(ownedHeroes, object);
+	vstd::erase(constOwnedHeroes, object);
 }
 
 

+ 13 - 4
lib/CPlayerState.h

@@ -49,6 +49,11 @@ class DLL_LINKAGE PlayerState : public CBonusSystemNode, public Player
 
 	std::vector<CGObjectInstance*> ownedObjects;
 
+	std::vector<const CGTownInstance*> constOwnedTowns; //not serialized
+	std::vector<const CGHeroInstance*> constOwnedHeroes; //not serialized
+	std::vector<CGTownInstance*> ownedTowns; //not serialized
+	std::vector<CGHeroInstance*> ownedHeroes; //not serialized
+
 	template<typename T>
 	std::vector<T> getObjectsOfType() const;
 
@@ -92,15 +97,16 @@ public:
 	std::string getNameTextID() const override;
 	void registerIcons(const IconRegistar & cb) const override;
 
-	std::vector<const CGHeroInstance* > getHeroes() const;
-	std::vector<const CGTownInstance* > getTowns() const;
-	std::vector<CGHeroInstance* > getHeroes();
-	std::vector<CGTownInstance* > getTowns();
+	const std::vector<const CGHeroInstance* > & getHeroes() const;
+	const std::vector<const CGTownInstance* > & getTowns() const;
+	const std::vector<CGHeroInstance* > & getHeroes();
+	const std::vector<CGTownInstance* > & getTowns();
 
 	std::vector<const CGObjectInstance* > getOwnedObjects() const;
 
 	void addOwnedObject(CGObjectInstance * object);
 	void removeOwnedObject(CGObjectInstance * object);
+	void postDeserialize();
 
 	bool checkVanquished() const
 	{
@@ -145,6 +151,9 @@ public:
 		h & enteredWinningCheatCode;
 		h & static_cast<CBonusSystemNode&>(*this);
 		h & destroyedObjects;
+
+		if (!h.saving)
+			postDeserialize();
 	}
 };
 

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