瀏覽代碼

Merge branch master into develop

Ivan Savenko 9 月之前
父節點
當前提交
aabd92451c
共有 100 個文件被更改,包括 2613 次插入4326 次删除
  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. 二進制
      Mods/vcmi/Content/Sprites/portalBidirectional.png
  19. 二進制
      Mods/vcmi/Content/Sprites/portalEntrance.png
  20. 二進制
      Mods/vcmi/Content/Sprites/portalExit.png
  21. 二進制
      Mods/vcmi/Content/Sprites2x/portalBidirectional.png
  22. 二進制
      Mods/vcmi/Content/Sprites2x/portalEntrance.png
  23. 二進制
      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. 20 2
      Mods/vcmi/Content/config/ukrainian.json
  29. 2 0
      android/AndroidManifest.xml
  30. 2 0
      android/vcmi-app/build.gradle
  31. 1 0
      android/vcmi-app/src/main/res/values-de/strings.xml
  32. 1 0
      android/vcmi-app/src/main/res/values/strings.xml
  33. 13 0
      android/vcmi-app/src/main/res/xml/shortcuts.xml
  34. 13 0
      android/vcmi-app/src/main/res/xml/shortcutsdaily.xml
  35. 13 0
      android/vcmi-app/src/main/res/xml/shortcutsdebug.xml
  36. 1 1
      client/CPlayerInterface.h
  37. 7 5
      client/adventureMap/CMinimap.cpp
  38. 2 2
      client/adventureMap/CMinimap.h
  39. 1 1
      client/battle/BattleActionsController.cpp
  40. 9 3
      client/mainmenu/CHighScoreScreen.cpp
  41. 1 0
      client/mainmenu/CHighScoreScreen.h
  42. 1 1
      client/mapView/MapRenderer.cpp
  43. 21 5
      client/render/CDefFile.cpp
  44. 1 0
      client/render/CDefFile.h
  45. 10 3
      client/renderSDL/RenderHandler.cpp
  46. 14 7
      client/renderSDL/SDL_Extensions.cpp
  47. 13 0
      client/renderSDL/ScreenHandler.cpp
  48. 3 2
      client/widgets/CGarrisonInt.cpp
  49. 5 5
      client/windows/CCastleInterface.cpp
  50. 1 1
      client/windows/CCastleInterface.h
  51. 14 6
      client/windows/CCreatureWindow.cpp
  52. 1 1
      client/windows/CCreatureWindow.h
  53. 1 1
      client/windows/CHeroWindow.cpp
  54. 28 22
      client/windows/GUIClasses.cpp
  55. 2 1
      client/windows/GUIClasses.h
  56. 85 0
      client/windows/InfoWindows.cpp
  57. 23 0
      client/windows/InfoWindows.h
  58. 1 1
      cmake_modules/VersionDefinition.cmake
  59. 29 24
      config/ai/nkai/nkai-settings.json
  60. 12 0
      config/bonuses.json
  61. 6 0
      debian/changelog
  62. 1 0
      docs/Readme.md
  63. 2 1
      include/vcmi/Creature.h
  64. 13 9
      include/vstd/CLoggerBase.h
  65. 1 0
      launcher/eu.vcmi.VCMI.metainfo.xml
  66. 8 6
      launcher/firstLaunch/firstlaunch_moc.cpp
  67. 1 1
      launcher/firstLaunch/firstlaunch_moc.h
  68. 15 4
      launcher/mainwindow_moc.cpp
  69. 1 1
      launcher/mainwindow_moc.h
  70. 2 2
      launcher/modManager/cdownloadmanager_moc.cpp
  71. 34 5
      launcher/modManager/cmodlistview_moc.cpp
  72. 4 4
      launcher/modManager/cmodlistview_moc.h
  73. 1 1
      launcher/modManager/modstatecontroller.cpp
  74. 1 1
      launcher/modManager/modstateitemmodel_moc.cpp
  75. 14 1
      launcher/modManager/modstatemodel.cpp
  76. 3 0
      launcher/modManager/modstatemodel.h
  77. 92 45
      launcher/startGame/StartGameTab.cpp
  78. 1 6
      launcher/startGame/StartGameTab.h
  79. 104 436
      launcher/translation/chinese.ts
  80. 167 189
      launcher/translation/czech.ts
  81. 176 193
      launcher/translation/english.ts
  82. 114 434
      launcher/translation/french.ts
  83. 114 454
      launcher/translation/german.ts
  84. 105 436
      launcher/translation/polish.ts
  85. 172 189
      launcher/translation/portuguese.ts
  86. 178 276
      launcher/translation/russian.ts
  87. 149 322
      launcher/translation/spanish.ts
  88. 173 351
      launcher/translation/swedish.ts
  89. 114 430
      launcher/translation/ukrainian.ts
  90. 150 315
      launcher/translation/vietnamese.ts
  91. 33 36
      lib/BasicTypes.cpp
  92. 10 1
      lib/CBonusTypeHandler.cpp
  93. 2 0
      lib/CConsoleHandler.cpp
  94. 1 1
      lib/CGameInfoCallback.cpp
  95. 2 2
      lib/CGameInfoCallback.h
  96. 1 0
      lib/CMakeLists.txt
  97. 48 8
      lib/CPlayerState.cpp
  98. 13 4
      lib/CPlayerState.h
  99. 5 0
      lib/GameSettings.cpp
  100. 1 0
      lib/IGameSettings.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>

二進制
Mods/vcmi/Content/Sprites/portalBidirectional.png


二進制
Mods/vcmi/Content/Sprites/portalEntrance.png


二進制
Mods/vcmi/Content/Sprites/portalExit.png


二進制
Mods/vcmi/Content/Sprites2x/portalBidirectional.png


二進制
Mods/vcmi/Content/Sprites2x/portalEntrance.png


二進制
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",

+ 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"/>

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

@@ -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;
@@ -766,9 +772,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);
@@ -855,6 +860,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 7)
-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

@@ -4,6 +4,12 @@ vcmi (1.7.0) jammy; urgency=medium
 
  -- Ivan Savenko <[email protected]>  Fri, 30 May 2025 12:00:00 +0200
 
+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

@@ -91,6 +91,7 @@
 	<launchable type="desktop-id">vcmilauncher.desktop</launchable>
 	<releases>
 		<release version="1.7.0" date="2025-05-30" type="development"/>
+		<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;
 };

文件差異過大導致無法顯示
+ 104 - 436
launcher/translation/chinese.ts


文件差異過大導致無法顯示
+ 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>

文件差異過大導致無法顯示
+ 114 - 434
launcher/translation/french.ts


文件差異過大導致無法顯示
+ 114 - 454
launcher/translation/german.ts


文件差異過大導致無法顯示
+ 105 - 436
launcher/translation/polish.ts


文件差異過大導致無法顯示
+ 172 - 189
launcher/translation/portuguese.ts


文件差異過大導致無法顯示
+ 178 - 276
launcher/translation/russian.ts


文件差異過大導致無法顯示
+ 149 - 322
launcher/translation/spanish.ts


文件差異過大導致無法顯示
+ 173 - 351
launcher/translation/swedish.ts


文件差異過大導致無法顯示
+ 114 - 430
launcher/translation/ukrainian.ts


文件差異過大導致無法顯示
+ 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();
 	}
 };
 

+ 5 - 0
lib/GameSettings.cpp

@@ -33,6 +33,11 @@ std::vector<int> IGameSettings::getVector(EGameSettings option) const
 	return getValue(option).convertTo<std::vector<int>>();
 }
 
+int IGameSettings::getVectorValue(EGameSettings option, size_t index) const
+{
+	return getValue(option)[index].Integer();
+}
+
 GameSettings::GameSettings() = default;
 GameSettings::~GameSettings() = default;
 

+ 1 - 0
lib/IGameSettings.h

@@ -101,6 +101,7 @@ public:
 	int64_t getInteger(EGameSettings option) const;
 	double getDouble(EGameSettings option) const;
 	std::vector<int> getVector(EGameSettings option) const;
+	int getVectorValue(EGameSettings option, size_t index) const;
 };
 
 VCMI_LIB_NAMESPACE_END

部分文件因文件數量過多而無法顯示