Browse Source

Merge branch 'vcmi/beta' into 'vcmi/develop'

Ivan Savenko 1 year ago
parent
commit
3bea383b59
69 changed files with 735 additions and 328 deletions
  1. 21 5
      .github/workflows/github.yml
  2. 33 17
      AI/BattleAI/BattleEvaluator.cpp
  3. 14 14
      AI/BattleAI/BattleExchangeVariant.cpp
  4. 4 4
      AI/BattleAI/BattleExchangeVariant.h
  5. 23 8
      AI/Nullkiller/AIGateway.cpp
  6. 4 4
      AI/Nullkiller/Analyzers/ObjectClusterizer.h
  7. 5 1
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  8. 1 1
      AI/VCAI/VCAI.cpp
  9. 76 0
      ChangeLog.md
  10. 9 14
      Global.h
  11. 7 0
      Mods/vcmi/config/vcmi/german.json
  12. 93 1
      Mods/vcmi/config/vcmi/ukrainian.json
  13. 2 9
      android/AndroidManifest.xml
  14. 8 1
      client/CMT.cpp
  15. 28 19
      client/CPlayerInterface.cpp
  16. 9 0
      client/Client.cpp
  17. 1 3
      client/HeroMovementController.cpp
  18. 4 4
      client/adventureMap/AdventureMapInterface.cpp
  19. 2 2
      client/gui/CGuiHandler.cpp
  20. 31 5
      client/mainmenu/CHighScoreScreen.cpp
  21. 1 1
      client/mainmenu/CMainMenu.cpp
  22. 4 1
      client/renderSDL/SDLImage.cpp
  23. 19 0
      client/widgets/CArtifactsOfHeroBase.cpp
  24. 13 0
      client/widgets/CArtifactsOfHeroBase.h
  25. 3 0
      client/widgets/MiscWidgets.cpp
  26. 3 0
      client/windows/CCastleInterface.cpp
  27. 10 3
      client/windows/GUIClasses.cpp
  28. 2 2
      config/highscoreCreatures.json
  29. 1 0
      docs/Readme.md
  30. 2 2
      docs/modders/Bonus/Bonus_Types.md
  31. 3 4
      launcher/CMakeLists.txt
  32. 24 5
      launcher/aboutProject/aboutproject_moc.cpp
  33. 1 5
      launcher/aboutProject/aboutproject_moc.h
  34. 17 0
      launcher/ios/revealdirectoryinfiles.h
  35. 22 0
      launcher/ios/revealdirectoryinfiles.mm
  36. 25 19
      launcher/settingsView/csettingsview_moc.cpp
  37. 1 0
      launcher/settingsView/csettingsview_moc.h
  38. 39 37
      launcher/translation/german.ts
  39. 42 40
      launcher/translation/ukrainian.ts
  40. 3 1
      lib/CCreatureHandler.cpp
  41. 4 4
      lib/IGameCallback.cpp
  42. 1 1
      lib/bonuses/Bonus.h
  43. 9 2
      lib/bonuses/BonusSelector.cpp
  44. 11 15
      lib/bonuses/BonusSelector.h
  45. 1 1
      lib/bonuses/Limiters.h
  46. 3 1
      lib/filesystem/CFileInputStream.cpp
  47. 9 1
      lib/filesystem/CFilesystemLoader.cpp
  48. 1 1
      lib/mapObjectConstructors/AObjectTypeHandler.cpp
  49. 7 6
      lib/mapObjects/CGTownBuilding.cpp
  50. 4 2
      lib/mapObjects/CQuest.cpp
  51. 11 8
      lib/mapObjects/MiscObjects.cpp
  52. 3 2
      lib/mapObjects/MiscObjects.h
  53. 7 7
      lib/mapping/CMapHeader.h
  54. 1 1
      lib/mapping/MapFormatH3M.cpp
  55. 1 4
      lib/modding/CModHandler.cpp
  56. 11 5
      lib/rmg/CMapGenOptions.cpp
  57. 2 2
      lib/rmg/CZonePlacer.cpp
  58. 1 6
      lib/rmg/RmgMap.cpp
  59. 9 6
      lib/rmg/modificators/TownPlacer.cpp
  60. 1 0
      lib/rmg/modificators/WaterProxy.cpp
  61. 1 0
      lib/serializer/Connection.cpp
  62. 4 0
      mapeditor/mapcontroller.cpp
  63. 0 1
      mapeditor/maphandler.h
  64. 0 1
      mapeditor/playersettings.h
  65. 13 13
      mapeditor/translation/german.ts
  66. 1 1
      mapeditor/windownewmap.cpp
  67. 3 1
      mapeditor/windownewmap.h
  68. 4 0
      server/CVCMIServer.cpp
  69. 2 4
      server/TurnTimerHandler.cpp

+ 21 - 5
.github/workflows/github.yml

@@ -66,7 +66,7 @@ jobs:
             pack: 1
             pack_type: RelWithDebInfo
             extension: exe
-            preset: windows-msvc-release-ccache
+            preset: windows-msvc-release
           - platform: mingw
             os: ubuntu-22.04
             test: 0
@@ -207,8 +207,15 @@ jobs:
 
     - name: Configure
       run: |
-        if [[ ${{matrix.preset}} == linux-gcc-test ]]; then GCC14=1; fi
-        cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }} ${GCC14:+-DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14}
+        if [[ ${{matrix.preset}} == linux-gcc-test ]]
+        then
+            cmake -DENABLE_CCACHE:BOOL=ON -DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14 --preset ${{ matrix.preset }}
+        elif [[ ${{matrix.platform}} != msvc ]]
+        then
+            cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }}
+        else
+            cmake --preset ${{ matrix.preset }}
+        fi
 
     - name: Build
       run: |
@@ -283,7 +290,7 @@ jobs:
       with:
         name: Android JNI ${{matrix.platform}}
         path: |
-          ${{ github.workspace }}/android/vcmi-app/src/main/jniLibs
+          ${{github.workspace}}/out/build/${{matrix.preset}}/android-build/libs
 
     - name: Upload build
       if: ${{ (matrix.pack == 1 || startsWith(matrix.platform, 'android')) && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/features/')) && matrix.platform != 'msvc' && matrix.platform != 'mingw-32' }}
@@ -306,7 +313,7 @@ jobs:
       matrix:
         include:
           - platform: android-32
-            os: ubuntu-22.04
+            os: macos-14
             preset: android-conan-ninja-release
             conan_profile: android-32
             conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT
@@ -346,6 +353,12 @@ jobs:
       env:
         GENERATE_ONLY_BUILT_CONFIG: 1
 
+    - uses: actions/setup-java@v4
+      if: ${{ startsWith(matrix.platform, 'android') }}
+      with:
+        distribution: 'temurin'
+        java-version: '11'
+
     - name: Build Number
       run: |
         source '${{github.workspace}}/CI/get_package_name.sh'
@@ -365,6 +378,9 @@ jobs:
     - name: Build Preset
       run: |
         cmake --build --preset ${{matrix.preset}}
+      env:
+        ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
+        ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
 
     - name: Download libs x64
       uses: actions/download-artifact@v4

+ 33 - 17
AI/BattleAI/BattleEvaluator.cpp

@@ -259,27 +259,46 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
 		return BattleAction::makeDefend(stack);
 	}
 
-	std::sort(hexes.begin(), hexes.end(), [&](BattleHex h1, BattleHex h2) -> bool
-	{
-		return reachability.distances[h1] < reachability.distances[h2];
-	});
+	std::vector<BattleHex> targetHexes = hexes;
 
-	for(auto hex : hexes)
+	for(int i = 0; i < 5; i++)
 	{
-		if(vstd::contains(avHexes, hex))
+		std::sort(targetHexes.begin(), targetHexes.end(), [&](BattleHex h1, BattleHex h2) -> bool
+			{
+				return reachability.distances[h1] < reachability.distances[h2];
+			});
+
+		for(auto hex : targetHexes)
+		{
+			if(vstd::contains(avHexes, hex))
+			{
+				return BattleAction::makeMove(stack, hex);
+			}
+
+			if(stack->coversPos(hex))
+			{
+				logAi->warn("Warning: already standing on neighbouring tile!");
+				//We shouldn't even be here...
+				return BattleAction::makeDefend(stack);
+			}
+		}
+
+		if(reachability.distances[targetHexes.front()] <= GameConstants::BFIELD_SIZE)
 		{
-			return BattleAction::makeMove(stack, hex);
+			break;
 		}
 
-		if(stack->coversPos(hex))
+		std::vector<BattleHex> copy = targetHexes;
+
+		for(auto hex : copy)
 		{
-			logAi->warn("Warning: already standing on neighbouring tile!");
-			//We shouldn't even be here...
-			return BattleAction::makeDefend(stack);
+			vstd::concatenate(targetHexes, hex.allNeighbouringTiles());
 		}
+
+		vstd::removeDuplicates(targetHexes);
 	}
 
-	BattleHex bestNeighbor = hexes.front();
+	BattleHex bestNeighbor = targetHexes.front();
 
 	if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE)
 	{
@@ -602,10 +621,10 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 					ps.value = scoreEvaluator.evaluateExchange(*cachedAttack, 0, *targets, innerCache, state);
 				}
 
-				for(auto unit : allUnits)
+				for(const auto & unit : allUnits)
 				{
 					auto newHealth = unit->getAvailableHealth();
-					auto oldHealth = healthOfStack[unit->unitId()];
+					auto oldHealth = vstd::find_or(healthOfStack, unit->unitId(), 0); // old health value may not exist for newly summoned units
 
 					if(oldHealth != newHealth)
 					{
@@ -732,6 +751,3 @@ void BattleEvaluator::print(const std::string & text) const
 {
 	logAi->trace("%s Battle AI[%p]: %s", playerID.toString(), this, text);
 }
-
-
-

+ 14 - 14
AI/BattleAI/BattleExchangeVariant.cpp

@@ -390,7 +390,7 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
 	const AttackPossibility & ap,
 	uint8_t turn,
 	PotentialTargets & targets,
-	std::shared_ptr<HypotheticBattle> hb)
+	std::shared_ptr<HypotheticBattle> hb) const
 {
 	ReachabilityData result;
 
@@ -402,7 +402,7 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
 
 	for(auto hex : hexes)
 	{
-		vstd::concatenate(allReachableUnits, turn == 0 ? reachabilityMap[hex] : getOneTurnReachableUnits(turn, hex));
+		vstd::concatenate(allReachableUnits, turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex));
 	}
 
 	vstd::removeDuplicates(allReachableUnits);
@@ -481,7 +481,7 @@ float BattleExchangeEvaluator::evaluateExchange(
 	uint8_t turn,
 	PotentialTargets & targets,
 	DamageCache & damageCache,
-	std::shared_ptr<HypotheticBattle> hb)
+	std::shared_ptr<HypotheticBattle> hb) const
 {
 	BattleScore score = calculateExchange(ap, turn, targets, damageCache, hb);
 
@@ -502,7 +502,7 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
 	uint8_t turn,
 	PotentialTargets & targets,
 	DamageCache & damageCache,
-	std::shared_ptr<HypotheticBattle> hb)
+	std::shared_ptr<HypotheticBattle> hb) const
 {
 #if BATTLE_TRACE_LEVEL>=1
 	logAi->trace("Battle exchange at %d", ap.attack.shooting ? ap.dest.hex : ap.from.hex);
@@ -613,7 +613,7 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
 			}
 			else
 			{
-				auto reachable = exchangeBattle->battleGetUnitsIf([&](const battle::Unit * u) -> bool
+				auto reachable = exchangeBattle->battleGetUnitsIf([this, &exchangeBattle, &attacker](const battle::Unit * u) -> bool
 					{
 						if(u->unitSide() == attacker->unitSide())
 							return false;
@@ -621,7 +621,10 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
 						if(!exchangeBattle->getForUpdate(u->unitId())->alive())
 							return false;
 
-						return vstd::contains_if(reachabilityMap[u->getPosition()], [&](const battle::Unit * other) -> bool
+						if (!u->getPosition().isValid())
+							return false; // e.g. tower shooters
+
+						return vstd::contains_if(reachabilityMap.at(u->getPosition()), [&attacker](const battle::Unit * other) -> bool
 							{
 								return attacker->unitId() == other->unitId();
 							});
@@ -732,7 +735,7 @@ void BattleExchangeEvaluator::updateReachabilityMap(std::shared_ptr<HypotheticBa
 	}
 }
 
-std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUnits(uint8_t turn, BattleHex hex)
+std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUnits(uint8_t turn, BattleHex hex) const
 {
 	std::vector<const battle::Unit *> result;
 
@@ -756,13 +759,10 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUn
 			auto unitSpeed = unit->getMovementRange(turn);
 			auto radius = unitSpeed * (turn + 1);
 
-			ReachabilityInfo unitReachability = vstd::getOrCompute(
-				reachabilityCache,
-				unit->unitId(),
-				[&](ReachabilityInfo & data)
-				{
-					data = turnBattle.getReachability(unit);
-				});
+			auto reachabilityIter = reachabilityCache.find(unit->unitId());
+			assert(reachabilityIter != reachabilityCache.end()); // missing updateReachabilityMap call?
+
+			ReachabilityInfo unitReachability = reachabilityIter != reachabilityCache.end() ? reachabilityIter->second : turnBattle.getReachability(unit);
 
 			bool reachable = unitReachability.distances[hex] <= radius;
 

+ 4 - 4
AI/BattleAI/BattleExchangeVariant.h

@@ -139,7 +139,7 @@ private:
 		uint8_t turn,
 		PotentialTargets & targets,
 		DamageCache & damageCache,
-		std::shared_ptr<HypotheticBattle> hb);
+		std::shared_ptr<HypotheticBattle> hb) const;
 
 	bool canBeHitThisTurn(const AttackPossibility & ap);
 
@@ -162,16 +162,16 @@ public:
 		uint8_t turn,
 		PotentialTargets & targets,
 		DamageCache & damageCache,
-		std::shared_ptr<HypotheticBattle> hb);
+		std::shared_ptr<HypotheticBattle> hb) const;
 
-	std::vector<const battle::Unit *> getOneTurnReachableUnits(uint8_t turn, BattleHex hex);
+	std::vector<const battle::Unit *> getOneTurnReachableUnits(uint8_t turn, BattleHex hex) const;
 	void updateReachabilityMap(std::shared_ptr<HypotheticBattle> hb);
 
 	ReachabilityData getExchangeUnits(
 		const AttackPossibility & ap,
 		uint8_t turn,
 		PotentialTargets & targets,
-		std::shared_ptr<HypotheticBattle> hb);
+		std::shared_ptr<HypotheticBattle> hb) const;
 
 	bool checkPositionBlocksOurStacks(HypotheticBattle & hb, const battle::Unit * unit, BattleHex position);
 

+ 23 - 8
AI/Nullkiller/AIGateway.cpp

@@ -1300,7 +1300,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 		{
 			destinationTeleport = exitId;
 			if(exitPos.valid())
-				destinationTeleportPos = h->convertFromVisitablePos(exitPos);
+				destinationTeleportPos = exitPos;
 			cb->moveHero(*h, h->pos, false);
 			destinationTeleport = ObjectInstanceID();
 			destinationTeleportPos = int3(-1);
@@ -1310,17 +1310,32 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 		auto doChannelProbing = [&]() -> void
 		{
 			auto currentPos = h->visitablePos();
-			auto currentExit = getObj(currentPos, true)->id;
+			auto currentTeleport = getObj(currentPos, true);
 
-			status.setChannelProbing(true);
-			for(auto exit : teleportChannelProbingList)
-				doTeleportMovement(exit, int3(-1));
-			teleportChannelProbingList.clear();
-			status.setChannelProbing(false);
+			if(currentTeleport)
+			{
+				auto currentExit = currentTeleport->id;
+
+				status.setChannelProbing(true);
+				for(auto exit : teleportChannelProbingList)
+					doTeleportMovement(exit, int3(-1));
+				teleportChannelProbingList.clear();
+				status.setChannelProbing(false);
+
+				doTeleportMovement(currentExit, currentPos);
+			}
+			else
+			{
+				logAi->debug("Unexpected channel probbing at " + currentPos.toString());
 
-			doTeleportMovement(currentExit, currentPos);
+				teleportChannelProbingList.clear();
+				status.setChannelProbing(false);
+			}
 		};
 
+		teleportChannelProbingList.clear();
+		status.setChannelProbing(false);
+
 		for(; i > 0; i--)
 		{
 			int3 currentCoord = path.nodes[i].coord;

+ 4 - 4
AI/Nullkiller/Analyzers/ObjectClusterizer.h

@@ -17,10 +17,10 @@ namespace NKAI
 
 struct ClusterObjectInfo
 {
-	float priority;
-	float movementCost;
-	uint64_t danger;
-	uint8_t turn;
+	float priority = 0.f;
+	float movementCost = 0.f;
+	uint64_t danger = 0;
+	uint8_t turn = 0;
 };
 
 struct ObjectInstanceIDHash

+ 5 - 1
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -1203,7 +1203,10 @@ void AINodeStorage::calculateTownPortalTeleportations(std::vector<CGPathNode *>
 	std::vector<const ChainActor *> actorsVector(actorsOfInitial.begin(), actorsOfInitial.end());
 	tbb::concurrent_vector<CGPathNode *> output;
 
-	if(actorsVector.size() * initialNodes.size() > 1000)
+// TODO: re-enable after fixing thread races. See issue for details:
+// https://github.com/vcmi/vcmi/pull/4130
+#if 0
+	if (actorsVector.size() * initialNodes.size() > 1000)
 	{
 		tbb::parallel_for(tbb::blocked_range<size_t>(0, actorsVector.size()), [&](const tbb::blocked_range<size_t> & r)
 			{
@@ -1216,6 +1219,7 @@ void AINodeStorage::calculateTownPortalTeleportations(std::vector<CGPathNode *>
 		std::copy(output.begin(), output.end(), std::back_inserter(initialNodes));
 	}
 	else
+#endif
 	{
 		for(auto actor : actorsVector)
 		{

+ 1 - 1
AI/VCAI/VCAI.cpp

@@ -1874,7 +1874,7 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
 		{
 			destinationTeleport = exitId;
 			if(exitPos.valid())
-				destinationTeleportPos = h->convertFromVisitablePos(exitPos);
+				destinationTeleportPos = exitPos;
 			cb->moveHero(*h, h->pos, false);
 			destinationTeleport = ObjectInstanceID();
 			destinationTeleportPos = int3(-1);

+ 76 - 0
ChangeLog.md

@@ -1,3 +1,79 @@
+# 1.5.2 -> 1.5.3
+
+### Stability
+* Fixed possible crash when hero class has no valid commander.
+* Fixed crash when pressing spacebar or enter during combat when hero has no tactics skill.
+* Fixed crash when receiving a commander level-up after winning a battle in a garrison owned by an enemy player.
+* Fixed possible crash when exiting a multiplayer game.
+* Game will now display an error message and exit after loading instead of crashing silently if a creature's combat animation is missing.
+* Game should now generate crash dump on uncaught c++ exception throw
+* Fixed crash when player finishes game with negative score
+* Fixed crash when opening tavern window in some localisations
+* Fixed crash on loading previously generated random map when mods that add object with same name are used
+* Game will now display an error message instead of silent crash if game data directory is not accessible
+
+### Mechanics
+* Transport Artefact victory condition will no longer trigger if another player has completed it.
+* Fixed wandering monster combat not triggering when landing in its zone of control when flying from above the monster using the Fly spell. 
+* Fixed potentially infinite movement loop when the hero has Admiral's Hat whirlpool immunity and the hero tries to enter and exit the same whirlpool.
+* If game picks gold for a random resource pile that has predetermined by map amount, its amount will be correctly multiplied by 100
+* Fixed hero not being able to learn spells from a mod in some cases, even if they are available from the town's mage guild.
+* The game will now actually take resources from seers' huts with the Gather Resources mission instead of awarding them.
+* Heroes with double spell points will no longer trigger the Mana Vortex.
+* If turn timer runs out during pve battle game will end player turn after a battle instead of forcing retreat
+
+### Interface
+* Fixed reversed button functions in Exchange Window
+* Fixed allied towns being missing from the list when using the advanced or expert Town Portal spell.
+* Fixed corrupted UI that could appear for a frame under certain conditions
+* The '*' symbol and non-printable characters can no longer be used in savegames due to Windows file system restrictions.
+* Pressing Ctrl while hovering over the adventure map will now display tile coordinates in the status bar.
+* Selection of another hero while hero is selected now requires Shift press instead of Ctrl
+* Fixed hero troops in the info box view flashing briefly during hero movement.
+* Reduced excessive memory usage on adventure map by several hundreds of megabytes (most noticeable on systems with large screen resolution)
+* Haptic feedback is now enabled by default on Android and on iOS
+* It is now possible to scroll through artifacts backpack using mouse wheel or swipe
+
+### Launcher
+* Android now uses the same Qt-based launcher as other systems
+* Fixed attempt to install a submod when installing new mod that depends on a submod of another mod
+* Fixed wrong order of activating mods in chain when installing multiple mods at once
+* Mod list no longer shows mod version column. Version is now only shown in the mod description.
+* Launcher will now skip the Heroes 3 data import step if data has been found automatically
+* Fixed inport of existing data files on iOS. This option now requires iOS 13 or later
+* Fixed import using offline installer on iOS.
+* Buttons to open data directories in the Help tab are now hidden on mobile systems if they can't be opened with file browser
+* Added the configuration files directory to the Help tab as it is located separately on Linux systems
+* Removed H3 data language selection during setup in favor of auto-detection
+* Replaced checkboxes with toggle buttons for easier of access on touchscreens.
+* Added interface for configuring several previously existing but inaccessible options in Launcher:
+    * Selection of input tolerance precision for all input types
+    * Relative cursor mode for mobile systems (was only available on Android)
+    * Haptic feedback toggle for mobile systems (was only available on Android)
+    * Sound and music volume (was only available in game)
+    * Selection of long touch interval (was only available in game)
+    * Selection of upscaling filter used by SDL 
+    * Controller input sensitivity and acceleration.
+
+### AI
+* Fixed crash when Nullkiller AI tries to explore after losing the hero in combat.
+* Fixed rare crash when Nullkiller AI tries to use portals
+* Fixed potential crash when Nullkiller AI has access to Town Portal spell
+* Fixed potential crash when Battle AI selects a spell to cast from a hero with summon spells.
+* Several fixes to Nullkiller AI exploration logic
+* Fixed bug leading to Battle AI doing nothing if targeted unit is unreachable
+
+### Random Maps Generator
+* Fixed crash when player selects a random number of players and selects a different colour to play, resulting in a non-continuous list of players.
+* Fixed rare crash when generating maps with water
+
+### Map Editor
+* Fixed crash on closing map editor
+
+### Modding
+* Added new building type 'thievesGuild' which implements HotA building in Cove.
+* Creature terrain limiter now actually accepts terrain as parameter
+
 # 1.5.1 -> 1.5.2
 
 ### Stability

+ 9 - 14
Global.h

@@ -348,6 +348,15 @@ namespace vstd
 		return std::find(c.begin(),c.end(),i);
 	}
 
+	// returns existing value from map, or default value if key does not exists
+	template <typename Map>
+	const typename Map::mapped_type & find_or(const Map& m, const typename Map::key_type& key, const typename Map::mapped_type& defaultValue) {
+		auto it = m.find(key);
+		if (it == m.end())
+			return defaultValue;
+		return it->second;
+	}
+
 	//returns first key that maps to given value if present, returns success via found if provided
 	template <typename Key, typename T>
 	Key findKey(const std::map<Key, T> & map, const T & value, bool * found = nullptr)
@@ -684,20 +693,6 @@ namespace vstd
 		return false;
 	}
 
-	template<class M, class Key, class F>
-	typename M::mapped_type & getOrCompute(M & m, const Key & k, F f)
-	{
-		typedef typename M::mapped_type V;
-
-		std::pair<typename M::iterator, bool> r = m.insert(typename M::value_type(k, V()));
-		V & v = r.first->second;
-
-		if(r.second)
-			f(v);
-
-		return v;
-	}
-
 	//c++20 feature
 	template<typename Arithmetic, typename Floating>
 	Arithmetic lerp(const Arithmetic & a, const Arithmetic & b, const Floating & f)

+ 7 - 0
Mods/vcmi/config/vcmi/german.json

@@ -252,6 +252,13 @@
 	"vcmi.battleWindow.damageEstimation.damage.1" : "%d Schaden",
 	"vcmi.battleWindow.damageEstimation.kills" : "%d werden verenden",
 	"vcmi.battleWindow.damageEstimation.kills.1" : "%d werden verenden",
+	
+	"vcmi.battleWindow.damageRetaliation.will" : "Wird Vergeltung üben ",
+	"vcmi.battleWindow.damageRetaliation.may" : "Kann Vergeltung üben ",
+	"vcmi.battleWindow.damageRetaliation.never" : "Wird keine Vergeltung üben.",
+	"vcmi.battleWindow.damageRetaliation.damage" : "(%DAMAGE).",
+	"vcmi.battleWindow.damageRetaliation.damageKills" : "(%DAMAGE, %KILLS).",
+	
 	"vcmi.battleWindow.killed" : "Getötet",
 	"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s wurden durch gezielte Schüsse getötet!",
 	"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s wurde mit einem gezielten Schuss getötet!",

+ 93 - 1
Mods/vcmi/config/vcmi/ukrainian.json

@@ -125,6 +125,13 @@
 	"vcmi.lobby.mod.state.version" : "Розбіжність версій",
 	"vcmi.lobby.mod.state.excessive" : "Має бути вимкнена",
 	"vcmi.lobby.mod.state.missing" : "Не встановлена",
+	"vcmi.lobby.pvp.coin.hover" : "Монетка.",
+	"vcmi.lobby.pvp.coin.help" : "Підкинути монетку",
+	"vcmi.lobby.pvp.randomTown.hover" : "Випадкове місто",
+	"vcmi.lobby.pvp.randomTown.help" : "Написати в чаті випадкове місто",
+	"vcmi.lobby.pvp.randomTownVs.hover" : "Випадкові міста",
+	"vcmi.lobby.pvp.randomTownVs.help" : "Написати в чаті два випадкових міста",
+	"vcmi.lobby.pvp.versus" : "проти",
 
 	"vcmi.client.errors.invalidMap" : "{Пошкоджена карта або кампанія}\n\nНе вдалося запустити гру! Вибрана карта або кампанія може бути невірною або пошкодженою. Причина:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{Не вистачає файлів даних}\n\nФайли даних кампаній не знайдено! Можливо, ви використовуєте неповні або пошкоджені файли даних Heroes 3. Будь ласка, перевстановіть дані гри.",
@@ -253,7 +260,6 @@
 	"vcmi.battleWindow.damageRetaliation.damageKills" : "(%DAMAGE, %KILLS).",
 	
 	"vcmi.battleWindow.killed" : "Загинуло",
-	
 	"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s було вбито влучними пострілами!",
 	"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s було вбито влучним пострілом!",
 	"vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s було вбито влучними пострілами!",
@@ -381,6 +387,14 @@
 	"vcmi.optionsTab.simturns.months.1" : " %d місяць",
 	"vcmi.optionsTab.simturns.months.2" : " %d місяці",
 
+	"vcmi.optionsTab.extraOptions.hover" : "Розширені опції",
+	"vcmi.optionsTab.extraOptions.help" : "Додаткові налаштування для гри",
+
+	"vcmi.optionsTab.cheatAllowed.hover" : "Дозволити чит-коди",
+	"vcmi.optionsTab.unlimitedReplay.hover" : "Необмежена кількість перегравань бою",
+	"vcmi.optionsTab.cheatAllowed.help" : "{Дозволити чіт-коди}\nДозволяє вводити чит-коди під час гри.",
+	"vcmi.optionsTab.unlimitedReplay.help" : "{Необмежена кількість перегравань бою}\nКількість перегравань боїв не обмежена.",
+
 	// Custom victory conditions for H3 campaigns and HotA maps
 	"vcmi.map.victoryCondition.daysPassed.toOthers" : "Ворогу вдалося вижити до сьогоднішнього дня. Він переміг!",
 	"vcmi.map.victoryCondition.daysPassed.toSelf" : "Вітаємо! Вам вдалося залишитися в живих. Перемога за вами!",
@@ -389,6 +403,84 @@
 	"vcmi.map.victoryCondition.collectArtifacts.message" : "Здобути три артефакти",
 	"vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Вітаємо! Усі ваші вороги переможені, і ви маєте Альянс Ангелів! Перемога ваша!",
 	"vcmi.map.victoryCondition.angelicAlliance.message" : "Перемогти всіх ворогів і створити Альянс Ангелів",
+	"vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf" : "На жаль, ви втратили частину Альянсу Ангелів. Все втрачено.",
+
+	// few strings from WoG used by vcmi
+//	"vcmi.stackExperience.description" : "» S t a c k   E x p e r i e n c e   D e t a i l s «\n\nCreature Type ................... : %s\nExperience Rank ................. : %s (%i)\nExperience Points ............... : %i\nExperience Points to Next Rank .. : %i\nMaximum Experience per Battle ... : %i%% (%i)\nNumber of Creatures in stack .... : %i\nMaximum New Recruits\n without losing current Rank .... : %i\nExperience Multiplier ........... : %.2f\nUpgrade Multiplier .............. : %.2f\nExperience after Rank 10 ........ : %i\nMaximum New Recruits to remain at\n Rank 10 if at Maximum Experience : %i",
+	"vcmi.stackExperience.rank.0" : "Базовий",
+	"vcmi.stackExperience.rank.1" : "Початківець",
+	"vcmi.stackExperience.rank.2" : "Підготовлений",
+	"vcmi.stackExperience.rank.3" : "Кваліфікований",
+	"vcmi.stackExperience.rank.4" : "Перевірений",
+	"vcmi.stackExperience.rank.5" : "Ветеран",
+	"vcmi.stackExperience.rank.6" : "Адепт",
+	"vcmi.stackExperience.rank.7" : "Експерт",
+	"vcmi.stackExperience.rank.8" : "Еліта",
+	"vcmi.stackExperience.rank.9" : "Майстер",
+	"vcmi.stackExperience.rank.10" : "Ас",
+	
+	// Strings for HotA Seer Hut / Quest Guards
+	"core.seerhut.quest.heroClass.complete.0" : "А, ти %s. Ось тобі подарунок.  Приймаєш?",
+	"core.seerhut.quest.heroClass.complete.1" : "А, ти %s. Ось тобі подарунок.  Приймаєш?",
+	"core.seerhut.quest.heroClass.complete.2" : "А, ти %s. Ось тобі подарунок.  Приймаєш?",
+	"core.seerhut.quest.heroClass.complete.3" : "Вартові помічають, що ви - %s, і пропонують вас пропустити.  Чи погоджуєтесь ви?",
+	"core.seerhut.quest.heroClass.complete.4" : "Вартові помічають, що ви - %s, і пропонують вас пропустити.  Чи погоджуєтесь ви?",
+	"core.seerhut.quest.heroClass.complete.5" : "Вартові помічають, що ви - %s, і пропонують вас пропустити.  Чи погоджуєтесь ви?",
+	"core.seerhut.quest.heroClass.description.0" : "Відправити %s до %s",
+	"core.seerhut.quest.heroClass.description.1" : "Відправити %s до %s",
+	"core.seerhut.quest.heroClass.description.2" : "Відправити %s до %s",
+	"core.seerhut.quest.heroClass.description.3" : "Відправити %s до щоб відкрити ворота",
+	"core.seerhut.quest.heroClass.description.4" : "Відправити %s до щоб відкрити ворота",
+	"core.seerhut.quest.heroClass.description.5" : "Відправити %s до щоб відкрити ворота",
+	"core.seerhut.quest.heroClass.hover.0" : "(шукає героя класу %s)",
+	"core.seerhut.quest.heroClass.hover.1" : "(шукає героя класу %s)",
+	"core.seerhut.quest.heroClass.hover.2" : "(шукає героя класу %s)",
+	"core.seerhut.quest.heroClass.hover.3" : "(шукає героя класу %s)",
+	"core.seerhut.quest.heroClass.hover.4" : "(шукає героя класу %s)",
+	"core.seerhut.quest.heroClass.hover.5" : "(шукає героя класу %s)",
+	"core.seerhut.quest.heroClass.receive.0" : "У мене є подарунок для %s.",
+	"core.seerhut.quest.heroClass.receive.1" : "У мене є подарунок для %s.",
+	"core.seerhut.quest.heroClass.receive.2" : "У мене є подарунок для %s.",
+	"core.seerhut.quest.heroClass.receive.3" : "Вартові кажуть, що пропускають лише %s.",
+	"core.seerhut.quest.heroClass.receive.4" : "Вартові кажуть, що пропускають лише %s.",
+	"core.seerhut.quest.heroClass.receive.5" : "Вартові кажуть, що пропускають лише %s.",
+	"core.seerhut.quest.heroClass.visit.0" : "Ти не %s.  У мене для тебе нічого немає. Йди геть!",
+	"core.seerhut.quest.heroClass.visit.1" : "Ти не %s.  У мене для тебе нічого немає. Йди геть!",
+	"core.seerhut.quest.heroClass.visit.2" : "Ти не %s.  У мене для тебе нічого немає. Йди геть!",
+	"core.seerhut.quest.heroClass.visit.3" : "Вартові пропускають лише %s.",
+	"core.seerhut.quest.heroClass.visit.4" : "Вартові пропускають лише %s.",
+	"core.seerhut.quest.heroClass.visit.5" : "Вартові пропускають лише %s.",
+	
+	"core.seerhut.quest.reachDate.complete.0" : "Тепер я вільний. Ось що у мене є для тебе. Ти приймаєш?",
+	"core.seerhut.quest.reachDate.complete.1" : "Тепер я вільний. Ось що у мене є для тебе. Ти приймаєш?",
+	"core.seerhut.quest.reachDate.complete.2" : "Тепер я вільний. Ось що у мене є для тебе. Ти приймаєш?",
+	"core.seerhut.quest.reachDate.complete.3" : "Тепер ви можете пройти. Бажаєте пройти?",
+	"core.seerhut.quest.reachDate.complete.4" : "Тепер ви можете пройти. Бажаєте пройти?",
+	"core.seerhut.quest.reachDate.complete.5" : "Тепер ви можете пройти. Бажаєте пройти?",
+	"core.seerhut.quest.reachDate.description.0" : "Зачекайте до %s для %s",
+	"core.seerhut.quest.reachDate.description.1" : "Зачекайте до %s для %s",
+	"core.seerhut.quest.reachDate.description.2" : "Зачекайте до %s для %s",
+	"core.seerhut.quest.reachDate.description.3" : "Зачекайте до %s, щоб відкрити ворота",
+	"core.seerhut.quest.reachDate.description.4" : "Зачекайте до %s, щоб відкрити ворота",
+	"core.seerhut.quest.reachDate.description.5" : "Зачекайте до %s, щоб відкрити ворота",
+	"core.seerhut.quest.reachDate.hover.0" : "(Повертайтеся не раніше, ніж через %s)",
+	"core.seerhut.quest.reachDate.hover.1" : "(Повертайтеся не раніше, ніж через %s)",
+	"core.seerhut.quest.reachDate.hover.2" : "(Повертайтеся не раніше, ніж через %s)",
+	"core.seerhut.quest.reachDate.hover.3" : "(Повертайтеся не раніше, ніж через %s)",
+	"core.seerhut.quest.reachDate.hover.4" : "(Повертайтеся не раніше, ніж через %s)",
+	"core.seerhut.quest.reachDate.hover.5" : "(Повертайтеся не раніше, ніж через %s)",
+	"core.seerhut.quest.reachDate.receive.0" : "Я зайнят. Повертайтеся не раніше, ніж через %s",
+	"core.seerhut.quest.reachDate.receive.1" : "Я зайнят. Повертайтеся не раніше, ніж через %s",
+	"core.seerhut.quest.reachDate.receive.2" : "Я зайнят. Повертайтеся не раніше, ніж через %s",
+	"core.seerhut.quest.reachDate.receive.3" : "Закрито до %s.",
+	"core.seerhut.quest.reachDate.receive.4" : "Закрито до %s.",
+	"core.seerhut.quest.reachDate.receive.5" : "Закрито до %s.",
+	"core.seerhut.quest.reachDate.visit.0" : "Я зайнятий.  Приходь не раніше, ніж %s",
+	"core.seerhut.quest.reachDate.visit.1" : "Я зайнятий.  Приходь не раніше, ніж %s",
+	"core.seerhut.quest.reachDate.visit.2" : "Я зайнятий.  Приходь не раніше, ніж %s",
+	"core.seerhut.quest.reachDate.visit.3" : "Закрито до %s.",
+	"core.seerhut.quest.reachDate.visit.4" : "Закрито до %s.",
+	"core.seerhut.quest.reachDate.visit.5" : "Закрито до %s.",
 
 	"core.bonus.ADDITIONAL_ATTACK.name" : "Подвійний удар",
 	"core.bonus.ADDITIONAL_ATTACK.description" : "Атакує двічі",

+ 2 - 9
android/AndroidManifest.xml

@@ -2,16 +2,9 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="eu.vcmi.vcmi">
 
-    <!-- %%INSERT_PERMISSIONS -->
+    <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.VIBRATE" />
-
-    <!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application.
-         Remove the comment if you do not require these default permissions. -->
-    <!-- %%INSERT_PERMISSIONS_DISABLED -->
-
-    <!-- The following comment will be replaced upon deployment with default features based on the dependencies of the application.
-         Remove the comment if you do not require these default features. -->
-    <!-- %%INSERT_FEATURES -->
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
 
 	<supports-screens
         android:largeScreens="true"

+ 8 - 1
client/CMT.cpp

@@ -210,7 +210,14 @@ int main(int argc, char * argv[])
 	logGlobal->info("The log file will be saved to %s", logPath);
 
 	// Init filesystem and settings
-	preinitDLL(::console, false);
+	try
+	{
+		preinitDLL(::console, false);
+	}
+	catch (const DataLoadingException & e)
+	{
+		handleFatalError(e.what(), true);
+	}
 
 	Settings session = settings.write["session"];
 	auto setSettingBool = [&](std::string key, std::string arg) {

+ 28 - 19
client/CPlayerInterface.cpp

@@ -245,8 +245,15 @@ void CPlayerInterface::performAutosave()
 				int txtlen = TextOperations::getUnicodeCharactersCount(name);
 
 				TextOperations::trimRightUnicode(name, std::max(0, txtlen - 15));
-				std::string forbiddenChars("\\/:*?\"<>| ");
-				std::replace_if(name.begin(), name.end(), [&](char c) { return std::string::npos != forbiddenChars.find(c); }, '_' );
+				auto const & isSymbolIllegal = [&](char c) {
+					static const std::string forbiddenChars("\\/:*?\"<>| ");
+
+					bool charForbidden = forbiddenChars.find(c) != std::string::npos;
+					bool charNonprintable = static_cast<unsigned char>(c) < static_cast<unsigned char>(' ');
+
+					return charForbidden || charNonprintable;
+				};
+				std::replace_if(name.begin(), name.end(), isSymbolIllegal, '_' );
 
 				prefix = name + "_" + cb->getStartInfo()->startTimeIso8601 + "/";
 			}
@@ -1087,18 +1094,20 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 
-	std::vector<ObjectInstanceID> tmpObjects;
-	if(objects.size() && dynamic_cast<const CGTownInstance *>(cb->getObj(objects[0])))
-	{
-		// sorting towns (like in client)
-		std::vector <const CGTownInstance*> Towns = LOCPLINT->localState->getOwnedTowns();
-		for(auto town : Towns)
-			for(auto item : objects)
-				if(town == cb->getObj(item))
-					tmpObjects.push_back(item);
-	}
-	else // other object list than town
-		tmpObjects = objects;
+	std::vector<ObjectInstanceID> objectGuiOrdered = objects;
+
+	std::map<ObjectInstanceID, int> townOrder;
+	auto ownedTowns = localState->getOwnedTowns();
+
+	for (int i = 0; i < ownedTowns.size(); ++i)
+		townOrder[ownedTowns[i]->id] = i;
+
+	auto townComparator = [&townOrder](const ObjectInstanceID & left, const ObjectInstanceID & right){
+		uint32_t leftIndex= townOrder.count(left) ? townOrder.at(left) : std::numeric_limits<uint32_t>::max();
+		uint32_t rightIndex = townOrder.count(right) ? townOrder.at(right) : std::numeric_limits<uint32_t>::max();
+		return leftIndex < rightIndex;
+	};
+	std::stable_sort(objectGuiOrdered.begin(), objectGuiOrdered.end(), townComparator);
 
 	auto selectCallback = [=](int selection)
 	{
@@ -1114,9 +1123,9 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component
 	const std::string localDescription = description.toString();
 
 	std::vector<int> tempList;
-	tempList.reserve(tmpObjects.size());
+	tempList.reserve(objectGuiOrdered.size());
 
-	for(auto item : tmpObjects)
+	for(auto item : objectGuiOrdered)
 		tempList.push_back(item.getNum());
 
 	CComponent localIconC(icon);
@@ -1125,7 +1134,7 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component
 	localIconC.removeChild(localIcon.get(), false);
 
 	std::vector<std::shared_ptr<IImage>> images;
-	for(auto & obj : tmpObjects)
+	for(auto & obj : objectGuiOrdered)
 	{
 		if(!settings["general"]["enableUiEnhancements"].Bool())
 			break;
@@ -1140,8 +1149,8 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component
 
 	auto wnd = std::make_shared<CObjectListWindow>(tempList, localIcon, localTitle, localDescription, selectCallback, 0, images);
 	wnd->onExit = cancelCallback;
-	wnd->onPopup = [this, tmpObjects](int index) { CRClickPopup::createAndPush(cb->getObj(tmpObjects[index]), GH.getCursorPosition()); };
-	wnd->onClicked = [this, tmpObjects](int index) { adventureInt->centerOnObject(cb->getObj(tmpObjects[index])); GH.windows().totalRedraw(); };
+	wnd->onPopup = [this, objectGuiOrdered](int index) { CRClickPopup::createAndPush(cb->getObj(objectGuiOrdered[index]), GH.getCursorPosition()); };
+	wnd->onClicked = [this, objectGuiOrdered](int index) { adventureInt->centerOnObject(cb->getObj(objectGuiOrdered[index])); GH.windows().totalRedraw(); };
 	GH.windows().pushWindow(wnd);
 }
 

+ 9 - 0
client/Client.cpp

@@ -607,9 +607,18 @@ void CClient::removeGUI() const
 #ifdef VCMI_ANDROID
 extern "C" JNIEXPORT jboolean JNICALL Java_eu_vcmi_vcmi_NativeMethods_tryToSaveTheGame(JNIEnv * env, jclass cls)
 {
+	boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
+
 	logGlobal->info("Received emergency save game request");
 	if(!LOCPLINT || !LOCPLINT->cb)
 	{
+		logGlobal->info("... but no active player interface found!");
+		return false;
+	}
+
+	if (!CSH || !CSH->logicConnection)
+	{
+		logGlobal->info("... but no active connection found!");
 		return false;
 	}
 

+ 1 - 3
client/HeroMovementController.cpp

@@ -91,9 +91,7 @@ void HeroMovementController::showTeleportDialog(const CGHeroInstance * hero, Tel
 
 	for(size_t i = 0; i < exits.size(); ++i)
 	{
-		const auto * teleporter = LOCPLINT->cb->getObj(exits[i].first);
-
-		if(teleporter && teleporter->visitableAt(nextNode.coord))
+		if(exits[i].second == nextNode.coord)
 		{
 			// Remove this node from path - it will be covered by teleportation
 			//LOCPLINT->localState->removeLastNode(hero);

+ 4 - 4
client/adventureMap/AdventureMapInterface.cpp

@@ -564,7 +564,7 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &targetPosition)
 			}
 			else
 			{
-				if(GH.isKeyboardCmdDown()) //normal click behaviour (as no hero selected)
+				if(GH.isKeyboardShiftDown()) //normal click behaviour (as no hero selected)
 				{
 					if(canSelect)
 						LOCPLINT->localState->setSelection(static_cast<const CArmedInstance*>(topBlocking));
@@ -643,19 +643,19 @@ void AdventureMapInterface::onTileHovered(const int3 &targetPosition)
 		objRelations = LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, objAtTile->tempOwner);
 		std::string text = LOCPLINT->localState->getCurrentHero() ? objAtTile->getHoverText(LOCPLINT->localState->getCurrentHero()) : objAtTile->getHoverText(LOCPLINT->playerID);
 		boost::replace_all(text,"\n"," ");
-		if (GH.isKeyboardShiftDown())
+		if (GH.isKeyboardCmdDown())
 			text.append(" (" + std::to_string(targetPosition.x) + ", " + std::to_string(targetPosition.y) + ")");
 		GH.statusbar()->write(text);
 	}
 	else if(isTargetPositionVisible)
 	{
 		std::string tileTooltipText = CGI->mh->getTerrainDescr(targetPosition, false);
-		if (GH.isKeyboardShiftDown())
+		if (GH.isKeyboardCmdDown())
 			tileTooltipText.append(" (" + std::to_string(targetPosition.x) + ", " + std::to_string(targetPosition.y) + ")");
 		GH.statusbar()->write(tileTooltipText);
 	}
 
-	if(LOCPLINT->localState->getCurrentArmy()->ID == Obj::TOWN || GH.isKeyboardCtrlDown())
+	if(LOCPLINT->localState->getCurrentArmy()->ID == Obj::TOWN || GH.isKeyboardShiftDown())
 	{
 		if(objAtTile)
 		{

+ 2 - 2
client/gui/CGuiHandler.cpp

@@ -118,9 +118,9 @@ void CGuiHandler::renderFrame()
 
 		if (settings["video"]["showfps"].Bool())
 			drawFPSCounter();
-	}
 
-	SDL_UpdateTexture(screenTexture, nullptr, screen->pixels, screen->pitch);
+		SDL_UpdateTexture(screenTexture, nullptr, screen->pixels, screen->pitch);
+	}
 
 	SDL_RenderClear(mainRenderer);
 	SDL_RenderCopy(mainRenderer, screenTexture, nullptr, nullptr);

+ 31 - 5
client/mainmenu/CHighScoreScreen.cpp

@@ -63,17 +63,43 @@ auto HighScoreCalculation::calculate()
 	return summary;
 }
 
+struct HighScoreCreature
+{
+	CreatureID creature;
+	int min;
+	int max;
+};
+
+static std::vector<HighScoreCreature> getHighscoreCreaturesList()
+{
+	JsonNode configCreatures(JsonPath::builtin("CONFIG/highscoreCreatures.json"));
+
+	std::vector<HighScoreCreature> ret;
+
+	for(auto & json : configCreatures["creatures"].Vector())
+	{
+		HighScoreCreature entry;
+		entry.creature = CreatureID::decode(json["creature"].String());
+		entry.max = json["max"].isNull() ? std::numeric_limits<int>::max() : json["max"].Integer();
+		entry.min = json["min"].isNull() ? std::numeric_limits<int>::min() : json["min"].Integer();
+
+		ret.push_back(entry);
+	}
+
+	return ret;
+}
+
 CreatureID HighScoreCalculation::getCreatureForPoints(int points, bool campaign)
 {
-	static const JsonNode configCreatures(JsonPath::builtin("CONFIG/highscoreCreatures.json"));
-	auto creatures = configCreatures["creatures"].Vector();
+	static const std::vector<HighScoreCreature> creatures = getHighscoreCreaturesList();
+
 	int divide = campaign ? 5 : 1;
 
 	for(auto & creature : creatures)
-		if(points / divide <= creature["max"].Integer() && points / divide >= creature["min"].Integer())
-			return CreatureID::decode(creature["creature"].String());
+		if(points / divide <= creature.max && points / divide >= creature.min)
+			return creature.creature;
 
-	return -1;
+	throw std::runtime_error("Unable to find creature for score " + std::to_string(points));
 }
 
 CHighScoreScreen::CHighScoreScreen(HighScorePage highscorepage, int highlighted)

+ 1 - 1
client/mainmenu/CMainMenu.cpp

@@ -60,7 +60,7 @@
 #include "../../lib/CRandomGenerator.h"
 
 std::shared_ptr<CMainMenu> CMM;
-ISelectionScreenInfo * SEL;
+ISelectionScreenInfo * SEL = nullptr;
 
 static void do_quit()
 {

+ 4 - 1
client/renderSDL/SDLImage.cpp

@@ -235,7 +235,10 @@ void SDLImage::setFlagColor(PlayerColor player)
 
 bool SDLImage::isTransparent(const Point & coords) const
 {
-	return CSDL_Ext::isTransparent(surf, coords.x, coords.y);
+	if (surf)
+		return CSDL_Ext::isTransparent(surf, coords.x, coords.y);
+	else
+		return true;
 }
 
 Point SDLImage::dimensions() const

+ 19 - 0
client/widgets/CArtifactsOfHeroBase.cpp

@@ -83,6 +83,9 @@ void CArtifactsOfHeroBase::init(
 	leftBackpackRoll->block(true);
 	rightBackpackRoll->block(true);
 
+	backpackScroller = std::make_shared<BackpackScroller>(this, Rect(380, 30, 278, 382));
+	backpackScroller->setScrollingEnabled(false);
+
 	setRedrawParent(true);
 }
 
@@ -208,6 +211,8 @@ void CArtifactsOfHeroBase::updateBackpackSlots()
 		leftBackpackRoll->block(!scrollingPossible);
 	if(rightBackpackRoll)
 		rightBackpackRoll->block(!scrollingPossible);
+	if (backpackScroller)
+		backpackScroller->setScrollingEnabled(scrollingPossible);
 }
 
 void CArtifactsOfHeroBase::updateSlot(const ArtifactPosition & slot)
@@ -277,3 +282,17 @@ void CArtifactsOfHeroBase::setSlotData(ArtPlacePtr artPlace, const ArtifactPosit
 		artPlace->setArtifact(nullptr);
 	}
 }
+
+BackpackScroller::BackpackScroller(CArtifactsOfHeroBase * owner, const Rect & dimensions)
+	: Scrollable(0, Point(), Orientation::HORIZONTAL)
+	, owner(owner)
+{
+	pos = dimensions + pos.topLeft();
+	setPanningStep(46);
+}
+
+void BackpackScroller::scrollBy(int distance)
+{
+	if (distance != 0)
+		owner->scrollBackpack(distance < 0);
+}

+ 13 - 0
client/widgets/CArtifactsOfHeroBase.h

@@ -10,10 +10,12 @@
 #pragma once
 
 #include "CArtPlace.h"
+#include "Scrollable.h"
 
 #include "../gui/Shortcut.h"
 
 class CButton;
+class BackpackScroller;
 
 class CArtifactsOfHeroBase : virtual public CIntObject, public CKeyShortcut
 {
@@ -54,6 +56,7 @@ public:
 	std::vector<ArtPlacePtr> backpack;
 	std::shared_ptr<CButton> leftBackpackRoll;
 	std::shared_ptr<CButton> rightBackpackRoll;
+	std::shared_ptr<BackpackScroller> backpackScroller;
 
 	const std::vector<Point> slotPos =
 	{
@@ -71,3 +74,13 @@ protected:
 	// Assigns an artifacts to an artifact place depending on it's new slot ID
 	virtual void setSlotData(ArtPlacePtr artPlace, const ArtifactPosition & slot);
 };
+
+class BackpackScroller : public Scrollable
+{
+	CArtifactsOfHeroBase * owner;
+
+	void scrollBy(int distance) override;
+
+public:
+	BackpackScroller(CArtifactsOfHeroBase * owner, const Rect & dimensions);
+};

+ 3 - 0
client/widgets/MiscWidgets.cpp

@@ -637,6 +637,9 @@ CCreaturePic::CCreaturePic(int x, int y, const CCreature * cre, bool Big, bool A
 
 	assert(CGI->townh->size() > faction);
 
+	if (cre->animDefName.empty())
+		throw std::runtime_error("Creature " + cre->getJsonKey() + " has no valid combat animation!");
+
 	if(Big)
 		bg = std::make_shared<CPicture>((*CGI->townh)[faction]->creatureBg130);
 	else

+ 3 - 0
client/windows/CCastleInterface.cpp

@@ -1548,6 +1548,9 @@ CHallInterface::CHallInterface(const CGTownInstance * Town):
 			const CBuilding * building = nullptr;
 			for(auto & buildingID : boxList[row][col])//we are looking for the first not built structure
 			{
+				if (town->town->buildings.count(buildingID) == 0)
+					throw std::runtime_error("Town " + Town->town->faction->getJsonKey() + " has no building with ID " + std::to_string(buildingID.getNum()));
+
 				const CBuilding * current = town->town->buildings.at(buildingID);
 				if(vstd::contains(town->builtBuildings, buildingID))
 				{

+ 10 - 3
client/windows/GUIClasses.cpp

@@ -491,14 +491,21 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::func
 	}
 	else if(LOCPLINT->cb->howManyHeroes(true) >= CGI->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP))
 	{
+		MetaString message;
+		message.appendTextID("core.tvrninfo.1");
+		message.replaceNumber(LOCPLINT->cb->howManyHeroes(true));
+
 		//Cannot recruit. You already have %d Heroes.
-		recruit->addHoverText(EButtonState::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[1]) % LOCPLINT->cb->howManyHeroes(true)));
+		recruit->addHoverText(EButtonState::NORMAL, message.toString());
 		recruit->block(true);
 	}
 	else if(LOCPLINT->cb->howManyHeroes(false) >= CGI->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
 	{
-		//Cannot recruit. You already have %d Heroes.
-		recruit->addHoverText(EButtonState::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[1]) % LOCPLINT->cb->howManyHeroes(false)));
+		MetaString message;
+		message.appendTextID("core.tvrninfo.1");
+		message.replaceNumber(LOCPLINT->cb->howManyHeroes(false));
+
+		recruit->addHoverText(EButtonState::NORMAL, message.toString());
 		recruit->block(true);
 	}
 	else if(dynamic_cast<const CGTownInstance *>(TavernObj) && dynamic_cast<const CGTownInstance *>(TavernObj)->visitingHero)

+ 2 - 2
config/highscoreCreatures.json

@@ -1,6 +1,6 @@
 {
     "creatures": [
-        { "min" : 1, "max" : 4, "creature": "imp" },
+        {            "max" : 4, "creature": "imp" },
         { "min" : 5, "max" : 8, "creature": "gremlin" },
         { "min" : 9, "max" : 12, "creature": "gnoll" },
         { "min" : 13, "max" : 16, "creature": "troglodyte" },
@@ -117,6 +117,6 @@
         { "min" : 389, "max" : 391, "creature": "titan" },
         { "min" : 392, "max" : 394, "creature": "goldDragon" },
         { "min" : 395, "max" : 397, "creature": "blackDragon" },
-        { "min" : 398, "max" : 500, "creature": "archangel" }
+        { "min" : 398,              "creature": "archangel" }
     ]
 }

+ 1 - 0
docs/Readme.md

@@ -2,6 +2,7 @@
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.0)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.1)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.2/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.2)
+[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.3/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.3)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases)
 
 # VCMI Project

+ 2 - 2
docs/modders/Bonus/Bonus_Types.md

@@ -349,10 +349,10 @@ Negates all natural immunities for affected stacks. (Orb of Vulnerability)
 
 ### OPENING_BATTLE_SPELL
 
-In battle, army affected by this bonus will cast spell at the very start of the battle
+In battle, army affected by this bonus will cast spell at the very start of the battle. Spell is always cast at expert level.
 
 - subtype: spell identifer
-- val: spell mastery level
+- val: duration of the spell, in rounds
 
 ### FREE_SHIP_BOARDING
 

+ 3 - 4
launcher/CMakeLists.txt

@@ -26,6 +26,8 @@ set(launcher_SRCS
 if(APPLE_IOS)
 	list(APPEND launcher_SRCS
 		ios/launchGame.m
+		ios/revealdirectoryinfiles.h
+		ios/revealdirectoryinfiles.mm
 		ios/selectdirectory.h
 		ios/selectdirectory.mm
 	)
@@ -110,12 +112,9 @@ endif()
 
 assign_source_group(${launcher_SRCS} ${launcher_HEADERS} ${launcher_RESOURCES} ${launcher_TS} ${launcher_ICON})
 
-# TODO: enabling AUTORCC breaks msvc build on CI
 set(CMAKE_AUTOMOC ON)
 set(CMAKE_AUTOUIC ON)
-if(NOT (MSVC AND "$ENV{GITHUB_ACTIONS}" STREQUAL true))
-	set(CMAKE_AUTORCC ON)
-endif()
+set(CMAKE_AUTORCC ON)
 
 if(POLICY CMP0071)
 	cmake_policy(SET CMP0071 NEW)

+ 24 - 5
launcher/aboutProject/aboutproject_moc.cpp

@@ -16,6 +16,24 @@
 #include "../../lib/GameConstants.h"
 #include "../../lib/VCMIDirs.h"
 
+#ifdef VCMI_IOS
+#include "ios/revealdirectoryinfiles.h"
+#endif
+
+namespace
+{
+void revealDirectoryInFileBrowser(QLineEdit * dirLineEdit)
+{
+	const auto dirUrl = QUrl::fromLocalFile(QFileInfo{dirLineEdit->text()}.absoluteFilePath());
+#ifdef VCMI_IOS
+	iOS_utils::revealDirectoryInFiles(dirUrl);
+#else
+	QDesktopServices::openUrl(dirUrl);
+#endif
+}
+}
+
+
 void AboutProjectView::hideAndStretchWidget(QGridLayout * layout, QWidget * toHide, QWidget * toStretch)
 {
 	toHide->hide();
@@ -47,10 +65,12 @@ AboutProjectView::AboutProjectView(QWidget * parent)
 	// On mobile platforms these directories are generally not accessible from phone itself, only via USB connection from PC
 	// Remove "Open" buttons and stretch line with text into now-empty space
 	hideAndStretchWidget(ui->gridLayout, ui->openGameDataDir, ui->lineEditGameDir);
+#ifdef VCMI_ANDROID
 	hideAndStretchWidget(ui->gridLayout, ui->openUserDataDir, ui->lineEditUserDataDir);
 	hideAndStretchWidget(ui->gridLayout, ui->openTempDir, ui->lineEditTempDir);
 	hideAndStretchWidget(ui->gridLayout, ui->openConfigDir, ui->lineEditConfigDir);
 #endif
+#endif
 }
 
 void AboutProjectView::changeEvent(QEvent *event)
@@ -68,22 +88,22 @@ void AboutProjectView::on_updatesButton_clicked()
 
 void AboutProjectView::on_openGameDataDir_clicked()
 {
-	QDesktopServices::openUrl(QUrl::fromLocalFile(QFileInfo(ui->lineEditGameDir->text()).absoluteFilePath()));
+	revealDirectoryInFileBrowser(ui->lineEditGameDir);
 }
 
 void AboutProjectView::on_openUserDataDir_clicked()
 {
-	QDesktopServices::openUrl(QUrl::fromLocalFile(QFileInfo(ui->lineEditUserDataDir->text()).absoluteFilePath()));
+	revealDirectoryInFileBrowser(ui->lineEditUserDataDir);
 }
 
 void AboutProjectView::on_openTempDir_clicked()
 {
-	QDesktopServices::openUrl(QUrl::fromLocalFile(QFileInfo(ui->lineEditTempDir->text()).absoluteFilePath()));
+	revealDirectoryInFileBrowser(ui->lineEditTempDir);
 }
 
 void AboutProjectView::on_openConfigDir_clicked()
 {
-	QDesktopServices::openUrl(QUrl::fromLocalFile(QFileInfo(ui->lineEditConfigDir->text()).absoluteFilePath()));
+	revealDirectoryInFileBrowser(ui->lineEditConfigDir);
 }
 
 void AboutProjectView::on_pushButtonDiscord_clicked()
@@ -106,7 +126,6 @@ void AboutProjectView::on_pushButtonHomepage_clicked()
 	QDesktopServices::openUrl(QUrl("https://vcmi.eu/"));
 }
 
-
 void AboutProjectView::on_pushButtonBugreport_clicked()
 {
 	QDesktopServices::openUrl(QUrl("https://github.com/vcmi/vcmi/issues"));

+ 1 - 5
launcher/aboutProject/aboutproject_moc.h

@@ -25,14 +25,11 @@ class AboutProjectView : public QWidget
 
 	/// Hides a widget and expands second widgets to take place of first widget in layout
 	void hideAndStretchWidget(QGridLayout * layout, QWidget * toHide, QWidget * toStretch);
+
 public:
 	explicit AboutProjectView(QWidget * parent = nullptr);
 
-public slots:
-
 private slots:
-
-
 	void on_updatesButton_clicked();
 
 	void on_openGameDataDir_clicked();
@@ -55,5 +52,4 @@ private slots:
 
 private:
 	Ui::AboutProjectView * ui;
-
 };

+ 17 - 0
launcher/ios/revealdirectoryinfiles.h

@@ -0,0 +1,17 @@
+/*
+ * revealdirectoryinfiles.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include <QUrl>
+
+namespace iOS_utils
+{
+void revealDirectoryInFiles(QUrl dirUrl);
+}

+ 22 - 0
launcher/ios/revealdirectoryinfiles.mm

@@ -0,0 +1,22 @@
+/*
+ * revealdirectoryinfiles.mm, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "revealdirectoryinfiles.h"
+
+#import <UIKit/UIKit.h>
+
+namespace iOS_utils
+{
+void revealDirectoryInFiles(QUrl dirUrl)
+{
+	auto urlComponents = [NSURLComponents componentsWithURL:dirUrl.toNSURL() resolvingAgainstBaseURL:NO];
+	urlComponents.scheme = @"shareddocuments";
+	[UIApplication.sharedApplication openURL:urlComponents.URL options:@{} completionHandler:nil];
+}
+}

+ 25 - 19
launcher/settingsView/csettingsview_moc.cpp

@@ -87,8 +87,6 @@ void CSettingsView::updateCheckbuttonText(QToolButton * button)
 
 void CSettingsView::loadSettings()
 {
-	setCheckbuttonState(ui->buttonShowIntro, settings["video"]["showIntro"].Bool());
-
 #ifdef VCMI_MOBILE
 	ui->comboBoxFullScreen->hide();
 	ui->labelFullScreen->hide();
@@ -111,7 +109,6 @@ void CSettingsView::loadSettings()
 	ui->spinBoxInterfaceScaling->setValue(settings["video"]["resolution"]["scaling"].Float());
 	ui->spinBoxFramerateLimit->setValue(settings["video"]["targetfps"].Float());
 	ui->spinBoxFramerateLimit->setDisabled(settings["video"]["vsync"].Bool());
-	setCheckbuttonState(ui->buttonVSync, settings["video"]["vsync"].Bool());
 	ui->sliderReservedArea->setValue(std::round(settings["video"]["reservedWidth"].Float() * 100));
 
 	ui->comboBoxFriendlyAI->setCurrentText(QString::fromStdString(settings["server"]["friendlyAI"].String()));
@@ -123,43 +120,27 @@ void CSettingsView::loadSettings()
 
 	ui->spinBoxNetworkPort->setValue(settings["server"]["localPort"].Integer());
 
-	setCheckbuttonState(ui->buttonAutoCheck, settings["launcher"]["autoCheckRepositories"].Bool());
-
 	ui->lineEditRepositoryDefault->setText(QString::fromStdString(settings["launcher"]["defaultRepositoryURL"].String()));
 	ui->lineEditRepositoryExtra->setText(QString::fromStdString(settings["launcher"]["extraRepositoryURL"].String()));
 
 	ui->lineEditRepositoryDefault->setEnabled(settings["launcher"]["defaultRepositoryEnabled"].Bool());
 	ui->lineEditRepositoryExtra->setEnabled(settings["launcher"]["extraRepositoryEnabled"].Bool());
 
-	setCheckbuttonState(ui->buttonRepositoryDefault, settings["launcher"]["defaultRepositoryEnabled"].Bool());
-	setCheckbuttonState(ui->buttonRepositoryExtra, settings["launcher"]["extraRepositoryEnabled"].Bool());
-
-	setCheckbuttonState(ui->buttonIgnoreSslErrors, settings["launcher"]["ignoreSslErrors"].Bool());
-	setCheckbuttonState(ui->buttonAutoSave, settings["general"]["saveFrequency"].Integer() > 0);
-
 	ui->spinBoxAutoSaveLimit->setValue(settings["general"]["autosaveCountLimit"].Integer());
 
-	setCheckbuttonState(ui->buttonAutoSavePrefix, settings["general"]["useSavePrefix"].Bool());
-
 	ui->lineEditAutoSavePrefix->setText(QString::fromStdString(settings["general"]["savePrefix"].String()));
 	ui->lineEditAutoSavePrefix->setEnabled(settings["general"]["useSavePrefix"].Bool());
 
 	Languages::fillLanguages(ui->comboBoxLanguage, false);
 	fillValidRenderers();
 
-	std::string cursorType = settings["video"]["cursor"].String();
-	int cursorTypeIndex = vstd::find_pos(cursorTypesList, cursorType);
-	setCheckbuttonState(ui->buttonCursorType, cursorTypeIndex);
-
 	std::string upscalingFilter = settings["video"]["scalingMode"].String();
 	int upscalingFilterIndex = vstd::find_pos(upscalingFilterTypes, upscalingFilter);
 	ui->comboBoxUpscalingFilter->setCurrentIndex(upscalingFilterIndex);
 
 	ui->sliderMusicVolume->setValue(settings["general"]["music"].Integer());
 	ui->sliderSoundVolume->setValue(settings["general"]["sound"].Integer());
-	setCheckbuttonState(ui->buttonRelativeCursorMode, settings["general"]["userRelativePointer"].Bool());
 	ui->sliderRelativeCursorSpeed->setValue(settings["general"]["relativePointerSpeedMultiplier"].Integer());
-	setCheckbuttonState(ui->buttonHapticFeedback, settings["launcher"]["hapticFeedback"].Bool());
 	ui->sliderLongTouchDuration->setValue(settings["general"]["longTouchTimeMilliseconds"].Integer());
 	ui->slideToleranceDistanceMouse->setValue(settings["input"]["mouseToleranceDistance"].Integer());
 	ui->sliderToleranceDistanceTouch->setValue(settings["input"]["touchToleranceDistance"].Integer());
@@ -168,6 +149,30 @@ void CSettingsView::loadSettings()
 	ui->sliderControllerSticksAcceleration->setValue(settings["input"]["controllerAxisScale"].Float() * 100);
 	ui->lineEditGameLobbyHost->setText(QString::fromStdString(settings["lobby"]["hostname"].String()));
 	ui->spinBoxNetworkPortLobby->setValue(settings["lobby"]["port"].Integer());
+
+	loadToggleButtonSettings();
+}
+
+void CSettingsView::loadToggleButtonSettings()
+{
+	setCheckbuttonState(ui->buttonShowIntro, settings["video"]["showIntro"].Bool());
+	setCheckbuttonState(ui->buttonVSync, settings["video"]["vsync"].Bool());
+	setCheckbuttonState(ui->buttonAutoCheck, settings["launcher"]["autoCheckRepositories"].Bool());
+
+	setCheckbuttonState(ui->buttonRepositoryDefault, settings["launcher"]["defaultRepositoryEnabled"].Bool());
+	setCheckbuttonState(ui->buttonRepositoryExtra, settings["launcher"]["extraRepositoryEnabled"].Bool());
+
+	setCheckbuttonState(ui->buttonIgnoreSslErrors, settings["launcher"]["ignoreSslErrors"].Bool());
+	setCheckbuttonState(ui->buttonAutoSave, settings["general"]["saveFrequency"].Integer() > 0);
+
+	setCheckbuttonState(ui->buttonAutoSavePrefix, settings["general"]["useSavePrefix"].Bool());
+
+	setCheckbuttonState(ui->buttonRelativeCursorMode, settings["general"]["userRelativePointer"].Bool());
+	setCheckbuttonState(ui->buttonHapticFeedback, settings["general"]["hapticFeedback"].Bool());
+
+	std::string cursorType = settings["video"]["cursor"].String();
+	int cursorTypeIndex = vstd::find_pos(cursorTypesList, cursorType);
+	setCheckbuttonState(ui->buttonCursorType, cursorTypeIndex);
 }
 
 void CSettingsView::fillValidResolutions()
@@ -431,6 +436,7 @@ void CSettingsView::changeEvent(QEvent *event)
 		ui->retranslateUi(this);
 		Languages::fillLanguages(ui->comboBoxLanguage, false);
 		loadTranslation();
+		loadToggleButtonSettings();
 	}
 	QWidget::changeEvent(event);
 }

+ 1 - 0
launcher/settingsView/csettingsview_moc.h

@@ -23,6 +23,7 @@ public:
 	~CSettingsView();
 
 	void loadSettings();
+	void loadToggleButtonSettings();
 	void loadTranslation();
 	void setDisplayList();
 	void changeEvent(QEvent *event) override;

+ 39 - 37
launcher/translation/german.ts

@@ -79,7 +79,7 @@
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="234"/>
         <source>Configuration files directory</source>
-        <translation type="unfinished"></translation>
+        <translation>Verzeichnis der Konfiguarions-Dateien</translation>
     </message>
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="297"/>
@@ -294,7 +294,7 @@
     <message>
         <location filename="../modManager/cmodlistview_moc.ui" line="105"/>
         <source>Reload repositories</source>
-        <translation type="unfinished"></translation>
+        <translation>Verzeichnis aktualisieren</translation>
     </message>
     <message>
         <location filename="../modManager/cmodlistview_moc.ui" line="340"/>
@@ -669,62 +669,62 @@ Installation erfolgreich heruntergeladen?</translation>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="59"/>
         <source>Online Lobby port</source>
-        <translation type="unfinished"></translation>
+        <translation>Online-Lobby-Port</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="66"/>
         <source>Autocombat AI in battles</source>
-        <translation type="unfinished"></translation>
+        <translation>Autokampf-KI in Kämpfen</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="73"/>
         <source>Sticks Sensitivity</source>
-        <translation type="unfinished"></translation>
+        <translation>Sticks Empfindlichkeit</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="87"/>
         <source>Haptic Feedback</source>
-        <translation type="unfinished"></translation>
+        <translation>Haptisches Feedback</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="94"/>
         <source>Software Cursor</source>
-        <translation type="unfinished"></translation>
+        <translation>Software-Cursor</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="108"/>
         <source>Online Lobby address</source>
-        <translation type="unfinished"></translation>
+        <translation>Adresse der Online-Lobby</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="115"/>
         <source>Upscaling Filter</source>
-        <translation type="unfinished"></translation>
+        <translation>Hochskalierungsfilter</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="122"/>
         <source>Use Relative Pointer Mode</source>
-        <translation type="unfinished"></translation>
+        <translation>Relativen Zeigermodus verwenden</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="195"/>
         <source>Nearest</source>
-        <translation type="unfinished"></translation>
+        <translation>Nearest</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="200"/>
         <source>Linear</source>
-        <translation type="unfinished"></translation>
+        <translation>Linear</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="205"/>
         <source>Best (Linear)</source>
-        <translation type="unfinished"></translation>
+        <translation>Bester (Linear)</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="231"/>
         <source>Input - Touchscreen</source>
-        <translation type="unfinished"></translation>
+        <translation>Eingabe - Touchscreen</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="440"/>
@@ -734,62 +734,62 @@ Installation erfolgreich heruntergeladen?</translation>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="474"/>
         <source>Network</source>
-        <translation type="unfinished"></translation>
+        <translation>Netzwerk</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="540"/>
         <source>Audio</source>
-        <translation type="unfinished"></translation>
+        <translation>Audio</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="578"/>
         <source>Relative Pointer Speed</source>
-        <translation type="unfinished"></translation>
+        <translation>Relative Zeigergeschwindigkeit</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="585"/>
         <source>Music Volume</source>
-        <translation type="unfinished"></translation>
+        <translation>Musik Lautstärke</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="606"/>
         <source>Ignore SSL errors</source>
-        <translation type="unfinished"></translation>
+        <translation>SSL-Fehler ignorieren</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="618"/>
         <source>Input - Mouse</source>
-        <translation type="unfinished"></translation>
+        <translation>Eingabe - Maus</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="628"/>
         <source>Long Touch Duration</source>
-        <translation type="unfinished"></translation>
+        <translation>Dauer der Berührung für &quot;lange Berührung&quot;</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="635"/>
         <source>%</source>
-        <translation type="unfinished"></translation>
+        <translation>%</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="658"/>
         <source>Controller Click Tolerance</source>
-        <translation type="unfinished"></translation>
+        <translation>Toleranz bei Controller Klick</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="665"/>
         <source>Touch Tap Tolerance</source>
-        <translation type="unfinished"></translation>
+        <translation>Toleranz bei Berührungen</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="704"/>
         <source>Input - Controller</source>
-        <translation type="unfinished"></translation>
+        <translation>Eingabe - Controller</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="771"/>
         <source>Sound Volume</source>
-        <translation type="unfinished"></translation>
+        <translation>Sound-Lautstärke</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="801"/>
@@ -828,12 +828,12 @@ Installation erfolgreich heruntergeladen?</translation>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="839"/>
         <source>Mouse Click Tolerance</source>
-        <translation type="unfinished"></translation>
+        <translation>Toleranz bei Mausklick</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="853"/>
         <source>Sticks Acceleration</source>
-        <translation type="unfinished"></translation>
+        <translation>Sticks Beschleunigung</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="995"/>
@@ -1064,12 +1064,12 @@ Heroes III: HD Edition wird derzeit nicht unterstützt!</translation>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="304"/>
         <source>Use offline installer from gog.com</source>
-        <translation type="unfinished"></translation>
+        <translation>Offline-Installer von gog.com verwenden</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="317"/>
         <source>You can manually copy directories Maps, Data and Mp3 from the original game directory to VCMI data directory that you can see on top of this page</source>
-        <translation type="unfinished"></translation>
+        <translation>Es können die Verzeichnisse Maps, Data und Mp3 manuell aus dem ursprünglichen Spielverzeichnis in das VCMI-Datenverzeichnis kopiert werden, das oben auf dieser Seite gesehen werden kann</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="336"/>
@@ -1106,23 +1106,24 @@ Heroes III: HD Edition wird derzeit nicht unterstützt!</translation>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="395"/>
         <source>Installing... %p%</source>
-        <translation type="unfinished"></translation>
+        <translation>Installation... %p%</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="424"/>
         <source>If you already have Heroes III files on your device, you can select this directory and VCMI will copy the existing data automatically.</source>
-        <translation type="unfinished"></translation>
+        <translation>Wenn bereits Heroes III-Dateien auf Ihrem Gerät sind, kann dieses Verzeichnis auswählt werden und VCMI wird die vorhandenen Daten automatisch kopieren.</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="466"/>
         <source>Copy existing files</source>
-        <translation type="unfinished"></translation>
+        <translation>Vorhandene Dateien kopieren</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="511"/>
         <source>If you own Heroes III on gog.com you can download backup offline installer from gog.com, and VCMI will import Heroes III data using offline installer. 
 Offline installer consists of two parts, .exe and .bin. Make sure you download both of them.</source>
-        <translation type="unfinished"></translation>
+        <translation>Wenn Sie Heroes III auf gog.com besitzen, können Sie den Backup-Offline-Installer von gog.com herunterladen, und VCMI wird die Daten von Heroes III mit dem Offline-Installer importieren. 
+Der Offline-Installer besteht aus zwei Teilen, .exe und .bin. Stellen Sie sicher, dass Sie beide Teile herunterladen.</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="696"/>
@@ -1173,7 +1174,7 @@ Offline installer consists of two parts, .exe and .bin. Make sure you download b
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="354"/>
         <source>Manual Installation</source>
-        <translation type="unfinished"></translation>
+        <translation>Manuelle Installation</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="367"/>
@@ -1487,13 +1488,14 @@ Bitte wählen Sie ein Verzeichnis mit Heroes III: Complete Edition oder Heroes I
     <message>
         <location filename="../main.cpp" line="121"/>
         <source>Error starting executable</source>
-        <translation type="unfinished"></translation>
+        <translation>Fehler beim Starten der ausführbaren Datei</translation>
     </message>
     <message>
         <location filename="../main.cpp" line="122"/>
         <source>Failed to start %1
 Reason: %2</source>
-        <translation type="unfinished"></translation>
+        <translation>Start von %1 fehlgeschlagen
+Grund: %2</translation>
     </message>
 </context>
 <context>

+ 42 - 40
launcher/translation/ukrainian.ts

@@ -64,7 +64,7 @@
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="114"/>
         <source>Data Directories</source>
-        <translation>Теки даних гри</translation>
+        <translation>Теки гри</translation>
     </message>
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="175"/>
@@ -79,7 +79,7 @@
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="234"/>
         <source>Configuration files directory</source>
-        <translation type="unfinished"></translation>
+        <translation>Тека файлів конфігурації</translation>
     </message>
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="297"/>
@@ -294,7 +294,7 @@
     <message>
         <location filename="../modManager/cmodlistview_moc.ui" line="105"/>
         <source>Reload repositories</source>
-        <translation type="unfinished"></translation>
+        <translation>Обновити репозиторії</translation>
     </message>
     <message>
         <location filename="../modManager/cmodlistview_moc.ui" line="340"/>
@@ -630,7 +630,7 @@ Install successfully downloaded?</source>
     <message>
         <location filename="../settingsView/csettingsview_moc.cpp" line="85"/>
         <source>Off</source>
-        <translation>Вимкнено</translation>
+        <translation>Ні</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="452"/>
@@ -669,62 +669,62 @@ Install successfully downloaded?</source>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="59"/>
         <source>Online Lobby port</source>
-        <translation type="unfinished"></translation>
+        <translation>Порт онлайн лобі</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="66"/>
         <source>Autocombat AI in battles</source>
-        <translation type="unfinished"></translation>
+        <translation>ШІ автобою</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="73"/>
         <source>Sticks Sensitivity</source>
-        <translation type="unfinished"></translation>
+        <translation>Чутливість стиків</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="87"/>
         <source>Haptic Feedback</source>
-        <translation type="unfinished"></translation>
+        <translation></translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="94"/>
         <source>Software Cursor</source>
-        <translation type="unfinished"></translation>
+        <translation>Програмний курсор</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="108"/>
         <source>Online Lobby address</source>
-        <translation type="unfinished"></translation>
+        <translation>Адреса онлайн-лобі</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="115"/>
         <source>Upscaling Filter</source>
-        <translation type="unfinished"></translation>
+        <translation>Фільтр масштабування</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="122"/>
         <source>Use Relative Pointer Mode</source>
-        <translation type="unfinished"></translation>
+        <translation>Режим відносного вказівника</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="195"/>
         <source>Nearest</source>
-        <translation type="unfinished"></translation>
+        <translation>Найближчий</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="200"/>
         <source>Linear</source>
-        <translation type="unfinished"></translation>
+        <translation>Лінійний</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="205"/>
         <source>Best (Linear)</source>
-        <translation type="unfinished"></translation>
+        <translation>Найкращий (лінійний)</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="231"/>
         <source>Input - Touchscreen</source>
-        <translation type="unfinished"></translation>
+        <translation>Введення - Сенсорний екран</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="440"/>
@@ -734,62 +734,62 @@ Install successfully downloaded?</source>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="474"/>
         <source>Network</source>
-        <translation type="unfinished"></translation>
+        <translation>Мережа</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="540"/>
         <source>Audio</source>
-        <translation type="unfinished"></translation>
+        <translation>Аудіо</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="578"/>
         <source>Relative Pointer Speed</source>
-        <translation type="unfinished"></translation>
+        <translation>Швидкість відносного вказівника</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="585"/>
         <source>Music Volume</source>
-        <translation type="unfinished"></translation>
+        <translation>Гучність музики</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="606"/>
         <source>Ignore SSL errors</source>
-        <translation type="unfinished"></translation>
+        <translation>Ігнорувати помилки SSL</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="618"/>
         <source>Input - Mouse</source>
-        <translation type="unfinished"></translation>
+        <translation>Введення - Миша</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="628"/>
         <source>Long Touch Duration</source>
-        <translation type="unfinished"></translation>
+        <translation>Тривалість довгого дотику</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="635"/>
         <source>%</source>
-        <translation type="unfinished"></translation>
+        <translation>%</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="658"/>
         <source>Controller Click Tolerance</source>
-        <translation type="unfinished"></translation>
+        <translation>Допуск на натискання контролера</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="665"/>
         <source>Touch Tap Tolerance</source>
-        <translation type="unfinished"></translation>
+        <translation>Допуск на натискання дотиком</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="704"/>
         <source>Input - Controller</source>
-        <translation type="unfinished"></translation>
+        <translation>Введення - Контролер</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="771"/>
         <source>Sound Volume</source>
-        <translation type="unfinished"></translation>
+        <translation>Гучність звуку</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="801"/>
@@ -828,12 +828,12 @@ Install successfully downloaded?</source>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="839"/>
         <source>Mouse Click Tolerance</source>
-        <translation type="unfinished"></translation>
+        <translation>Допуск кліків миші</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="853"/>
         <source>Sticks Acceleration</source>
-        <translation type="unfinished"></translation>
+        <translation>Прискорення стиків</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="995"/>
@@ -858,7 +858,7 @@ Install successfully downloaded?</source>
     <message>
         <location filename="../settingsView/csettingsview_moc.cpp" line="83"/>
         <source>On</source>
-        <translation>Увімкнено</translation>
+        <translation>Так</translation>
     </message>
     <message>
         <source>Cursor</source>
@@ -1064,12 +1064,12 @@ Heroes® of Might and Magic® III HD наразі не підтримуєтьс
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="304"/>
         <source>Use offline installer from gog.com</source>
-        <translation type="unfinished"></translation>
+        <translation>Використати офлайн-інсталятор з gog.com</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="317"/>
         <source>You can manually copy directories Maps, Data and Mp3 from the original game directory to VCMI data directory that you can see on top of this page</source>
-        <translation type="unfinished"></translation>
+        <translation>Ви можете вручну скопіювати теки Maps, Data та Mp3 з теки оригінальної гри до теки даних VCMI, яку ви можете побачити вгорі цієї сторінки</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="336"/>
@@ -1106,23 +1106,24 @@ Heroes® of Might and Magic® III HD наразі не підтримуєтьс
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="395"/>
         <source>Installing... %p%</source>
-        <translation type="unfinished"></translation>
+        <translation>Встановлюємо... %p%</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="424"/>
         <source>If you already have Heroes III files on your device, you can select this directory and VCMI will copy the existing data automatically.</source>
-        <translation type="unfinished"></translation>
+        <translation>Якщо на вашому пристрої вже є файли Heroes III, ви можете вибрати цю теку і VCMI автоматично скопіює наявні дані.</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="466"/>
         <source>Copy existing files</source>
-        <translation type="unfinished"></translation>
+        <translation>Скопіювати існуючі файли</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="511"/>
         <source>If you own Heroes III on gog.com you can download backup offline installer from gog.com, and VCMI will import Heroes III data using offline installer. 
 Offline installer consists of two parts, .exe and .bin. Make sure you download both of them.</source>
-        <translation type="unfinished"></translation>
+        <translation>Якщо у вас є Heroes III на gog.com, ви можете завантажити резервну копію офлайн-інсталятора з gog.com, і VCMI імпортує дані Heroes III за допомогою офлайн-інсталятора. 
+Офлайн-інсталятор складається з двох частин, .exe та .bin. Переконайтеся, що ви завантажили обидві частини.</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="696"/>
@@ -1173,7 +1174,7 @@ Offline installer consists of two parts, .exe and .bin. Make sure you download b
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="354"/>
         <source>Manual Installation</source>
-        <translation type="unfinished"></translation>
+        <translation>Ручне встановлення</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="367"/>
@@ -1487,13 +1488,14 @@ Please select directory with Heroes III: Complete Edition or Heroes III: Shadow
     <message>
         <location filename="../main.cpp" line="121"/>
         <source>Error starting executable</source>
-        <translation type="unfinished"></translation>
+        <translation>Помилка запуску виконуваного файлу</translation>
     </message>
     <message>
         <location filename="../main.cpp" line="122"/>
         <source>Failed to start %1
 Reason: %2</source>
-        <translation type="unfinished"></translation>
+        <translation>Не вдалося запустити %1
+Причина: %2</translation>
     </message>
 </context>
 <context>

+ 3 - 1
lib/CCreatureHandler.cpp

@@ -648,7 +648,9 @@ std::shared_ptr<CCreature> CCreatureHandler::loadFromJson(const std::string & sc
 
 	if (!cre->special &&
 		!CResourceHandler::get()->existsResource(cre->animDefName) &&
-		!CResourceHandler::get()->existsResource(cre->animDefName.addPrefix("SPRITES/")))
+		!CResourceHandler::get()->existsResource(cre->animDefName.toType<EResType::JSON>()) &&
+		!CResourceHandler::get()->existsResource(cre->animDefName.addPrefix("SPRITES/")) &&
+		!CResourceHandler::get()->existsResource(cre->animDefName.addPrefix("SPRITES/").toType<EResType::JSON>()))
 		throw ModLoadingException(scope, "creature " + cre->getJsonKey() + " has no combat animation but is not marked as special!" );
 
 	JsonNode advMapFile = node["graphics"]["map"];

+ 4 - 4
lib/IGameCallback.cpp

@@ -158,17 +158,17 @@ void CPrivilegedInfoCallback::pickAllowedArtsSet(std::vector<const CArtifact *>
 
 void CPrivilegedInfoCallback::getAllowedSpells(std::vector<SpellID> & out, std::optional<ui16> level)
 {
-	for (ui32 i = 0; i < gs->map->allowedSpells.size(); i++) //spellh size appears to be greater (?)
+	for (auto const & spellID : gs->map->allowedSpells)
 	{
-		const spells::Spell * spell = SpellID(i).toSpell();
+		const auto * spell = spellID.toEntity(VLC);
 
-		if (!isAllowed(spell->getId()))
+		if (!isAllowed(spellID))
 			continue;
 
 		if (level.has_value() && spell->getLevel() != level)
 			continue;
 
-		out.push_back(spell->getId());
+		out.push_back(spellID);
 	}
 }
 

+ 1 - 1
lib/bonuses/Bonus.h

@@ -65,7 +65,7 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this<Bonus>, public Se
 	BonusSubtypeID subtype;
 
 	BonusSource source = BonusSource::OTHER; //source type" uses BonusSource values - what gave that bonus
-	BonusSource targetSourceType;//Bonuses of what origin this amplifies, uses BonusSource values. Needed for PERCENT_TO_TARGET_TYPE.
+	BonusSource targetSourceType = BonusSource::OTHER;//Bonuses of what origin this amplifies, uses BonusSource values. Needed for PERCENT_TO_TARGET_TYPE.
 	si32 val = 0;
 	BonusSourceID sid; //source id: id of object/artifact/spell
 	BonusValueType valType = BonusValueType::ADDITIVE_VALUE;

+ 9 - 2
lib/bonuses/BonusSelector.cpp

@@ -51,8 +51,15 @@ namespace Selector
 		return seffectRange;
 	}
 
-	DLL_LINKAGE CWillLastTurns turns;
-	DLL_LINKAGE CWillLastDays days;
+	DLL_LINKAGE CWillLastTurns turns(int turns)
+	{
+		return CWillLastTurns(turns);
+	}
+
+	DLL_LINKAGE CWillLastDays days(int days)
+	{
+		return CWillLastDays(days);
+	}
 
 	CSelector DLL_LINKAGE typeSubtype(BonusType Type, BonusSubtypeID Subtype)
 	{

+ 11 - 15
lib/bonuses/BonusSelector.h

@@ -81,8 +81,11 @@ public:
 
 class DLL_LINKAGE CWillLastTurns
 {
-public:
 	int turnsRequested;
+public:
+	CWillLastTurns(int turnsRequested):
+		turnsRequested(turnsRequested)
+	{}
 
 	bool operator()(const Bonus *bonus) const
 	{
@@ -90,18 +93,17 @@ public:
 			|| !Bonus::NTurns(bonus) //so do every not expriing after N-turns effect
 			|| bonus->turnsRemain > turnsRequested;
 	}
-	CWillLastTurns& operator()(const int &setVal)
-	{
-		turnsRequested = setVal;
-		return *this;
-	}
 };
 
 class DLL_LINKAGE CWillLastDays
 {
-public:
 	int daysRequested;
 
+public:
+	CWillLastDays(int daysRequested):
+		daysRequested(daysRequested)
+	{}
+
 	bool operator()(const Bonus *bonus) const
 	{
 		if(daysRequested <= 0 || Bonus::Permanent(bonus) || Bonus::OneBattle(bonus))
@@ -112,14 +114,8 @@ public:
 		{
 			return bonus->turnsRemain > daysRequested;
 		}
-
 		return false; // TODO: ONE_WEEK need support for turnsRemain, but for now we'll exclude all unhandled durations
 	}
-	CWillLastDays& operator()(const int &setVal)
-	{
-		daysRequested = setVal;
-		return *this;
-	}
 };
 
 
@@ -131,8 +127,8 @@ namespace Selector
 	extern DLL_LINKAGE const CSelectFieldEqual<BonusSource> & sourceType();
 	extern DLL_LINKAGE const CSelectFieldEqual<BonusSource> & targetSourceType();
 	extern DLL_LINKAGE const CSelectFieldEqual<BonusLimitEffect> & effectRange();
-	extern DLL_LINKAGE CWillLastTurns turns;
-	extern DLL_LINKAGE CWillLastDays days;
+	CWillLastTurns DLL_LINKAGE turns(int turns);
+	CWillLastDays DLL_LINKAGE days(int days);
 
 	CSelector DLL_LINKAGE typeSubtype(BonusType Type, BonusSubtypeID Subtype);
 	CSelector DLL_LINKAGE typeSubtypeInfo(BonusType type, BonusSubtypeID subtype, const CAddInfo & info);

+ 1 - 1
lib/bonuses/Limiters.h

@@ -118,7 +118,7 @@ class DLL_LINKAGE HasAnotherBonusLimiter : public ILimiter //applies only to nod
 public:
 	BonusType type;
 	BonusSubtypeID subtype;
-	BonusSource source;
+	BonusSource source = BonusSource::OTHER;
 	BonusSourceID sid;
 	bool isSubtypeRelevant; //check for subtype only if this is true
 	bool isSourceRelevant; //check for bonus source only if this is true

+ 3 - 1
lib/filesystem/CFileInputStream.cpp

@@ -10,6 +10,8 @@
 #include "StdInc.h"
 #include "CFileInputStream.h"
 
+#include "../ExceptionsCommon.h"
+
 VCMI_LIB_NAMESPACE_BEGIN
 
 CFileInputStream::CFileInputStream(const boost::filesystem::path & file, si64 start, si64 size)
@@ -18,7 +20,7 @@ CFileInputStream::CFileInputStream(const boost::filesystem::path & file, si64 st
 	fileStream{file.c_str(), std::ios::in | std::ios::binary}
 {
 	if (fileStream.fail())
-		throw std::runtime_error("File " + file.string() + " isn't available.");
+		throw DataLoadingException("Failed to open file '" + file.string() + "'. Reason: " + strerror(errno) );
 
 	if (dataSize == 0)
 	{

+ 9 - 1
lib/filesystem/CFilesystemLoader.cpp

@@ -12,14 +12,22 @@
 
 #include "CFileInputStream.h"
 
+#include "../ExceptionsCommon.h"
+
 VCMI_LIB_NAMESPACE_BEGIN
 
 CFilesystemLoader::CFilesystemLoader(std::string _mountPoint, boost::filesystem::path baseDirectory, size_t depth, bool initial):
 	baseDirectory(std::move(baseDirectory)),
 	mountPoint(std::move(_mountPoint)),
-	fileList(listFiles(mountPoint, depth, initial)),
 	recursiveDepth(depth)
 {
+	try {
+		fileList = listFiles(mountPoint, depth, initial);
+	}
+	catch (const boost::filesystem::filesystem_error & e) {
+		throw DataLoadingException("Failed to load content of '" + baseDirectory.string() + "'. Reason: " + e.what());
+	}
+
 	logGlobal->trace("File system loaded, %d files found", fileList.size());
 }
 

+ 1 - 1
lib/mapObjectConstructors/AObjectTypeHandler.cpp

@@ -129,7 +129,7 @@ void AObjectTypeHandler::preInitObject(CGObjectInstance * obj) const
 	obj->ID = Obj(type);
 	obj->subID = subtype;
 	obj->typeName = typeName;
-	obj->subTypeName = subTypeName;
+	obj->subTypeName = getJsonKey();
 	obj->blockVisit = blockVisit;
 	obj->removable = removable;
 }

+ 7 - 6
lib/mapObjects/CGTownBuilding.cpp

@@ -181,13 +181,14 @@ void COPWBonus::onHeroVisit (const CGHeroInstance * h) const
 			if(visitors.empty())
 			{
 				if(h->mana < h->manaLimit() * 2)
+				{
 					cb->setManaPoints (heroID, 2 * h->manaLimit());
-				//TODO: investigate line below
-				//cb->setObjProperty (town->id, ObjProperty::VISITED, true);
-				iw.text.appendRawString(getVisitingBonusGreeting());
-				cb->showInfoDialog(&iw);
-				//extra visit penalty if hero alredy had double mana points (or even more?!)
-				town->addHeroToStructureVisitors(h, indexOnTV);
+					//TODO: investigate line below
+					//cb->setObjProperty (town->id, ObjProperty::VISITED, true);
+					iw.text.appendRawString(getVisitingBonusGreeting());
+					cb->showInfoDialog(&iw);
+					town->addHeroToStructureVisitors(h, indexOnTV);
+				}
 			}
 			break;
 		}

+ 4 - 2
lib/mapObjects/CQuest.cpp

@@ -141,6 +141,8 @@ bool CQuest::checkQuest(const CGHeroInstance * h) const
 
 void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const
 {
+	// FIXME: this should be part of 'reward', and not hacking into limiter state that should only limit access to such reward
+
 	for(auto & elem : mission.artifacts)
 	{
 		if(h->hasArt(elem))
@@ -164,9 +166,9 @@ void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const
 			}
 		}
 	}
-			
+
 	cb->takeCreatures(h->id, mission.creatures);
-	cb->giveResources(h->getOwner(), mission.resources);
+	cb->giveResources(h->getOwner(), -mission.resources);
 }
 
 void CQuest::addTextReplacements(IGameCallback * cb, MetaString & text, std::vector<Component> & components) const

+ 11 - 8
lib/mapObjects/MiscObjects.cpp

@@ -263,6 +263,9 @@ void CGResource::pickRandomObject(CRandomGenerator & rand)
 		ID = Obj::RESOURCE;
 		subID = rand.nextInt(EGameResID::WOOD, EGameResID::GOLD);
 		setType(ID, subID);
+
+		if (subID == EGameResID::GOLD && amount != CGResource::RANDOM_AMOUNT)
+			amount *= CGResource::GOLD_AMOUNT_MULTIPLIER;
 	}
 }
 
@@ -275,7 +278,7 @@ void CGResource::initObj(CRandomGenerator & rand)
 		switch(resourceID().toEnum())
 		{
 		case EGameResID::GOLD:
-			amount = rand.nextInt(5, 10) * 100;
+			amount = rand.nextInt(5, 10) * CGResource::GOLD_AMOUNT_MULTIPLIER;
 			break;
 		case EGameResID::WOOD: case EGameResID::ORE:
 			amount = rand.nextInt(6, 10);
@@ -490,7 +493,7 @@ void CGMonolith::onHeroVisit( const CGHeroInstance * h ) const
 			auto exits = cb->getTeleportChannelExits(channel);
 			for(const auto & exit : exits)
 			{
-				td.exits.push_back(std::make_pair(exit, h->convertFromVisitablePos(cb->getObj(exit)->visitablePos())));
+				td.exits.push_back(std::make_pair(exit, cb->getObj(exit)->visitablePos()));
 			}
 		}
 
@@ -522,9 +525,9 @@ void CGMonolith::teleportDialogAnswered(const CGHeroInstance *hero, ui32 answer,
 	else if(vstd::isValidIndex(exits, answer))
 		dPos = exits[answer].second;
 	else
-		dPos = hero->convertFromVisitablePos(cb->getObj(randomExit)->visitablePos());
+		dPos = cb->getObj(randomExit)->visitablePos();
 
-	cb->moveHero(hero->id, dPos, EMovementMode::MONOLITH);
+	cb->moveHero(hero->id, hero->convertFromVisitablePos(dPos), EMovementMode::MONOLITH);
 }
 
 void CGMonolith::initObj(CRandomGenerator & rand)
@@ -566,7 +569,7 @@ void CGSubterraneanGate::onHeroVisit( const CGHeroInstance * h ) const
 	else
 	{
 		auto exit = getRandomExit(h);
-		td.exits.push_back(std::make_pair(exit, h->convertFromVisitablePos(cb->getObj(exit)->visitablePos())));
+		td.exits.push_back(std::make_pair(exit, cb->getObj(exit)->visitablePos()));
 	}
 
 	cb->showTeleportDialog(&td);
@@ -676,7 +679,7 @@ void CGWhirlpool::onHeroVisit( const CGHeroInstance * h ) const
 		{
 			auto blockedPosList = cb->getObj(exit)->getBlockedPos();
 			for(const auto & bPos : blockedPosList)
-				td.exits.push_back(std::make_pair(exit, h->convertFromVisitablePos(bPos)));
+				td.exits.push_back(std::make_pair(exit, bPos));
 		}
 	}
 
@@ -700,10 +703,10 @@ void CGWhirlpool::teleportDialogAnswered(const CGHeroInstance *hero, ui32 answer
 
 		const auto * obj = cb->getObj(exit);
 		std::set<int3> tiles = obj->getBlockedPos();
-		dPos = hero->convertFromVisitablePos(*RandomGeneratorUtil::nextItem(tiles, CRandomGenerator::getDefault()));
+		dPos = *RandomGeneratorUtil::nextItem(tiles, CRandomGenerator::getDefault());
 	}
 
-	cb->moveHero(hero->id, dPos, EMovementMode::MONOLITH);
+	cb->moveHero(hero->id, hero->convertFromVisitablePos(dPos), EMovementMode::MONOLITH);
 }
 
 bool CGWhirlpool::isProtected(const CGHeroInstance * h)

+ 3 - 2
lib/mapObjects/MiscObjects.h

@@ -122,8 +122,9 @@ class DLL_LINKAGE CGResource : public CArmedInstance
 public:
 	using CArmedInstance::CArmedInstance;
 
-	static constexpr ui32 RANDOM_AMOUNT = 0;
-	ui32 amount = RANDOM_AMOUNT; //0 if random
+	static constexpr uint32_t RANDOM_AMOUNT = 0;
+	static constexpr uint32_t GOLD_AMOUNT_MULTIPLIER = 100;
+	uint32_t amount = RANDOM_AMOUNT; //0 if random
 	
 	MetaString message;
 

+ 7 - 7
lib/mapping/CMapHeader.h

@@ -208,13 +208,13 @@ class DLL_LINKAGE CMapHeader: public Serializeable
 	void setupEvents();
 public:
 
-	static const int MAP_SIZE_SMALL = 36;
-	static const int MAP_SIZE_MIDDLE = 72;
-	static const int MAP_SIZE_LARGE = 108;
-	static const int MAP_SIZE_XLARGE = 144;
-	static const int MAP_SIZE_HUGE = 180;
-	static const int MAP_SIZE_XHUGE = 216;
-	static const int MAP_SIZE_GIANT = 252;
+	static constexpr int MAP_SIZE_SMALL = 36;
+	static constexpr int MAP_SIZE_MIDDLE = 72;
+	static constexpr int MAP_SIZE_LARGE = 108;
+	static constexpr int MAP_SIZE_XLARGE = 144;
+	static constexpr int MAP_SIZE_HUGE = 180;
+	static constexpr int MAP_SIZE_XHUGE = 216;
+	static constexpr int MAP_SIZE_GIANT = 252;
 
 	CMapHeader();
 	virtual ~CMapHeader();

+ 1 - 1
lib/mapping/MapFormatH3M.cpp

@@ -1315,7 +1315,7 @@ CGObjectInstance * CMapLoaderH3M::readResource(const int3 & mapPosition, std::sh
 	if(GameResID(objectTemplate->subid) == GameResID(EGameResID::GOLD))
 	{
 		// Gold is multiplied by 100.
-		object->amount *= 100;
+		object->amount *= CGResource::GOLD_AMOUNT_MULTIPLIER;
 	}
 	reader->skipZero(4);
 	return object;

+ 1 - 4
lib/modding/CModHandler.cpp

@@ -227,10 +227,7 @@ void CModHandler::loadOneMod(std::string modName, const std::string & parent, co
 
 	if(CResourceHandler::get("initial")->existsResource(CModInfo::getModFile(modFullName)))
 	{
-		JsonParsingSettings settings;
-		settings.mode = JsonParsingSettings::JsonFormatMode::JSON; // TODO: remove once Android launcher with its strict parser is gone
-
-		CModInfo mod(modFullName, modSettings[modName], JsonNode(CModInfo::getModFile(modFullName), settings));
+		CModInfo mod(modFullName, modSettings[modName], JsonNode(CModInfo::getModFile(modFullName)));
 		if (!parent.empty()) // this is submod, add parent to dependencies
 			mod.dependencies.insert(parent);
 

+ 11 - 5
lib/rmg/CMapGenOptions.cpp

@@ -284,9 +284,12 @@ void CMapGenOptions::resetPlayersMap()
 
 	while (players.size() > realPlayersCnt)
 	{
-		while (eraseLastPlayer(EPlayerType::AI));
-		while (eraseLastPlayer(EPlayerType::COMP_ONLY));
-		while (eraseLastPlayer(EPlayerType::HUMAN));
+		if (eraseLastPlayer(EPlayerType::AI))
+			continue;
+		if (eraseLastPlayer(EPlayerType::COMP_ONLY))
+			continue;
+		if (eraseLastPlayer(EPlayerType::HUMAN))
+			continue;
 	}
 
 	//First colors from the list are assigned to human players, then to CPU players
@@ -503,8 +506,9 @@ void CMapGenOptions::finalize(CRandomGenerator & rand)
 	if (getHumanOrCpuPlayerCount() == RANDOM_SIZE)
 	{
 		auto possiblePlayers = mapTemplate->getPlayers().getNumbers();
+		int requiredPlayers = countHumanPlayers() + countCompOnlyPlayers();
 		//ignore all non-randomized players, make sure these players will not be missing after roll
-		possiblePlayers.erase(possiblePlayers.begin(), possiblePlayers.lower_bound(countHumanPlayers() + countCompOnlyPlayers()));
+		possiblePlayers.erase(possiblePlayers.begin(), possiblePlayers.lower_bound(requiredPlayers));
 
 		vstd::erase_if(possiblePlayers, [maxPlayers](int i)
 		{
@@ -598,7 +602,9 @@ void CMapGenOptions::updatePlayers()
 	{
 		auto it = itrev;
 		--it;
-		if (players.size() == getHumanOrCpuPlayerCount()) break;
+		if (players.size() == getHumanOrCpuPlayerCount())
+			break;
+
 		if(it->second.getPlayerType() != EPlayerType::HUMAN)
 		{
 			players.erase(it);

+ 2 - 2
lib/rmg/CZonePlacer.cpp

@@ -455,9 +455,9 @@ void CZonePlacer::prepareZones(TZoneMap &zones, TZoneVector &zonesVector, const
 				auto player = PlayerColor(*owner - 1);
 				auto playerSettings = map.getMapGenOptions().getPlayersSettings();
 				FactionID faction = FactionID::RANDOM;
-				if (vstd::contains(playerSettings, player))
+				if (playerSettings.size() > player)
 				{
-					faction = playerSettings[player].getStartingTown();
+					faction = std::next(playerSettings.begin(), player)->second.getStartingTown();
 				}
 				else
 				{

+ 1 - 6
lib/rmg/RmgMap.cpp

@@ -369,12 +369,7 @@ ui32 RmgMap::getTotalZoneCount() const
 bool RmgMap::isAllowedSpell(const SpellID & sid) const
 {
 	assert(sid.getNum() >= 0);
-	if (sid.getNum() < mapInstance->allowedSpells.size())
-	{
-		return mapInstance->allowedSpells.count(sid);
-	}
-	else
-		return false;
+	return mapInstance->allowedSpells.count(sid);
 }
 
 void RmgMap::dump(bool zoneId) const

+ 9 - 6
lib/rmg/modificators/TownPlacer.cpp

@@ -54,12 +54,14 @@ void TownPlacer::placeTowns(ObjectManager & manager)
 		//set zone types to player faction, generate main town
 		logGlobal->info("Preparing playing zone");
 		int player_id = *zone.getOwner() - 1;
-		auto& playerInfo = map.getPlayer(player_id);
-		PlayerColor player(player_id);
-		if(playerInfo.canAnyonePlay())
+		const auto & playerSettings = map.getMapGenOptions().getPlayersSettings();
+		PlayerColor player;
+
+		if (playerSettings.size() > player_id)
 		{
-			player = PlayerColor(player_id);
-			zone.setTownType(map.getMapGenOptions().getPlayersSettings().find(player)->second.getStartingTown());
+			const auto & currentPlayerSettings = std::next(playerSettings.begin(), player_id);
+			player = currentPlayerSettings->first;
+			zone.setTownType(currentPlayerSettings->second.getStartingTown());
 			
 			if(zone.getTownType() == FactionID::RANDOM)
 				zone.setTownType(getRandomTownType(true));
@@ -87,11 +89,12 @@ void TownPlacer::placeTowns(ObjectManager & manager)
 		//register MAIN town of zone only
 		map.registerZone(town->getFaction());
 		
-		if(playerInfo.canAnyonePlay()) //configure info for owning player
+		if(player.isValidPlayer()) //configure info for owning player
 		{
 			logGlobal->trace("Fill player info %d", player_id);
 			
 			// Update player info
+			auto & playerInfo = map.getPlayer(player.getNum());
 			playerInfo.allowedFactions.clear();
 			playerInfo.allowedFactions.insert(zone.getTownType());
 			playerInfo.hasMainTown = true;

+ 1 - 0
lib/rmg/modificators/WaterProxy.cpp

@@ -219,6 +219,7 @@ bool WaterProxy::waterKeepConnection(const rmg::ZoneConnection & connection, boo
 	const auto & zoneA = connection.getZoneA();
 	const auto & zoneB = connection.getZoneB();
 
+	RecursiveLock lock(externalAccessMutex);
 	for(auto & lake : lakes)
 	{
 		if(lake.neighbourZones.count(zoneA) && lake.neighbourZones.count(zoneB))

+ 1 - 0
lib/serializer/Connection.cpp

@@ -77,6 +77,7 @@ void CConnection::sendPack(const CPack * pack)
 	if (!connectionPtr)
 		throw std::runtime_error("Attempt to send packet on a closed connection!");
 
+	packWriter->buffer.clear();
 	*serializer & pack;
 
 	logNetwork->trace("Sending a pack of type %s", typeid(*pack).name());

+ 4 - 0
mapeditor/mapcontroller.cpp

@@ -8,6 +8,7 @@
  *
  */
 
+#include "StdInc.h"
 #include "mapcontroller.h"
 
 #include "../lib/ArtifactUtils.h"
@@ -57,6 +58,7 @@ void MapController::connectScenes()
 
 MapController::~MapController()
 {
+	main = nullptr;
 }
 
 const std::unique_ptr<CMap> & MapController::getMapUniquePtr() const
@@ -228,6 +230,8 @@ void MapController::setMap(std::unique_ptr<CMap> cmap)
 
 	_map->getEditManager()->getUndoManager().setUndoCallback([this](bool allowUndo, bool allowRedo)
 		{
+			if(!main)
+				return;
 			main->enableUndo(allowUndo);
 			main->enableRedo(allowRedo);
 		}

+ 0 - 1
mapeditor/maphandler.h

@@ -11,7 +11,6 @@
 #pragma once
 //code is copied from vcmiclient/mapHandler.h with minimal changes
 
-#include "StdInc.h"
 #include "../lib/int3.h"
 #include "Animation.h"
 

+ 0 - 1
mapeditor/playersettings.h

@@ -10,7 +10,6 @@
 
 #pragma once
 
-#include "StdInc.h"
 #include <QDialog>
 #include "playerparams.h"
 

+ 13 - 13
mapeditor/translation/german.ts

@@ -129,37 +129,37 @@
     <message>
         <location filename="../inspector/herospellwidget.ui" line="29"/>
         <source>Spells</source>
-        <translation type="unfinished">Zaubersprüche</translation>
+        <translation>Zaubersprüche</translation>
     </message>
     <message>
         <location filename="../inspector/herospellwidget.ui" line="47"/>
         <source>Customize spells</source>
-        <translation type="unfinished"></translation>
+        <translation>Zaubersprüche anpassen</translation>
     </message>
     <message>
         <location filename="../inspector/herospellwidget.ui" line="76"/>
         <source>Level 1</source>
-        <translation type="unfinished"></translation>
+        <translation>Level 1</translation>
     </message>
     <message>
         <location filename="../inspector/herospellwidget.ui" line="114"/>
         <source>Level 2</source>
-        <translation type="unfinished"></translation>
+        <translation>Level 2</translation>
     </message>
     <message>
         <location filename="../inspector/herospellwidget.ui" line="152"/>
         <source>Level 3</source>
-        <translation type="unfinished"></translation>
+        <translation>Level 3</translation>
     </message>
     <message>
         <location filename="../inspector/herospellwidget.ui" line="190"/>
         <source>Level 4</source>
-        <translation type="unfinished"></translation>
+        <translation>Level 4</translation>
     </message>
     <message>
         <location filename="../inspector/herospellwidget.ui" line="228"/>
         <source>Level 5</source>
-        <translation type="unfinished"></translation>
+        <translation>Level 5</translation>
     </message>
 </context>
 <context>
@@ -1738,32 +1738,32 @@
     <message>
         <location filename="../windownewmap.ui" line="164"/>
         <source>S  (36x36)</source>
-        <translation type="unfinished"></translation>
+        <translation>S  (36x36)</translation>
     </message>
     <message>
         <location filename="../windownewmap.ui" line="169"/>
         <source>M  (72x72)</source>
-        <translation type="unfinished"></translation>
+        <translation>M  (72x72)</translation>
     </message>
     <message>
         <location filename="../windownewmap.ui" line="174"/>
         <source>L  (108x108)</source>
-        <translation type="unfinished"></translation>
+        <translation>L  (108x108)</translation>
     </message>
     <message>
         <location filename="../windownewmap.ui" line="184"/>
         <source>H  (180x180)</source>
-        <translation type="unfinished"></translation>
+        <translation>H  (180x180)</translation>
     </message>
     <message>
         <location filename="../windownewmap.ui" line="189"/>
         <source>XH (216x216)</source>
-        <translation type="unfinished"></translation>
+        <translation>XH (216x216)</translation>
     </message>
     <message>
         <location filename="../windownewmap.ui" line="194"/>
         <source>G  (252x252)</source>
-        <translation type="unfinished"></translation>
+        <translation>G  (252x252)</translation>
     </message>
     <message>
         <location filename="../windownewmap.ui" line="248"/>

+ 1 - 1
mapeditor/windownewmap.cpp

@@ -108,7 +108,7 @@ bool WindowNewMap::loadUserSettings()
 
 		ui->widthTxt->setText(QString::number(mapGenOptions.getWidth()));
 		ui->heightTxt->setText(QString::number(mapGenOptions.getHeight()));
-		for(auto & sz : mapSizes)
+		for(const auto & sz : mapSizes)
 		{
 			if(sz.second.first == mapGenOptions.getWidth() &&
 			sz.second.second == mapGenOptions.getHeight())

+ 3 - 1
mapeditor/windownewmap.h

@@ -11,6 +11,8 @@
 #pragma once
 
 #include <QDialog>
+
+#include "../lib/mapping/CMapHeader.h"
 #include "../lib/rmg/CMapGenOptions.h"
 
 namespace Ui
@@ -62,7 +64,7 @@ class WindowNewMap : public QDialog
 		{7, 6},
 		{8, 7}
 	};
-	
+
 	const std::map<int, std::pair<int, int>> mapSizes
 	{
 		{0, {CMapHeader::MAP_SIZE_SMALL, 	CMapHeader::MAP_SIZE_SMALL}},

+ 4 - 0
server/CVCMIServer.cpp

@@ -662,6 +662,10 @@ void CVCMIServer::updateAndPropagateLobbyState()
 			{
 				si->mapGenOptions->setPlayerTypeForStandardPlayer(pset.color, EPlayerType::HUMAN);
 			}
+			else
+			{
+				si->mapGenOptions->setPlayerTypeForStandardPlayer(pset.color, EPlayerType::AI);
+			}
 		}
 	}
 

+ 2 - 4
server/TurnTimerHandler.cpp

@@ -313,10 +313,8 @@ void TurnTimerHandler::onBattleLoop(const BattleID & battleID, int waitTime)
 		}
 		else
 		{
-			BattleAction retreat;
-			retreat.side = side;
-			retreat.actionType = EActionType::RETREAT; //harsh punishment
-			gameHandler.battles->makePlayerBattleAction(battleID, player, retreat);
+			// battle vs neutrals - no-op, let battle run till the end
+			// once battle is over player turn will be over due to running out of timer on adventure map
 		}
 	}
 }