Selaa lähdekoodia

Merge branch 'beta' into 'develop'

Ivan Savenko 9 kuukautta sitten
vanhempi
sitoutus
920a66edb1
100 muutettua tiedostoa jossa 2246 lisäystä ja 1313 poistoa
  1. 1 0
      .github/workflows/github.yml
  2. 0 9
      AI/BattleAI/BattleExchangeVariant.cpp
  3. 8 7
      AI/Nullkiller/AIGateway.cpp
  4. 30 30
      AI/Nullkiller/Analyzers/BuildAnalyzer.cpp
  5. 1 7
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp
  6. 1 1
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h
  7. 5 3
      AI/Nullkiller/Behaviors/BuildingBehavior.cpp
  8. 1 1
      AI/Nullkiller/Engine/Nullkiller.cpp
  9. 1 0
      AI/Nullkiller/Engine/Settings.cpp
  10. 2 0
      AI/Nullkiller/Engine/Settings.h
  11. 1 1
      AI/Nullkiller/Pathfinding/AINodeStorage.h
  12. 2 4
      AI/Nullkiller/Pathfinding/Actors.cpp
  13. 0 1
      AI/Nullkiller/Pathfinding/Actors.h
  14. 1 0
      AI/Nullkiller/Pathfinding/GraphPaths.cpp
  15. 31 0
      ChangeLog.md
  16. 9 0
      Global.h
  17. BIN
      Mods/vcmi/Content/Sprites/minimapIcons/bordergate.png
  18. BIN
      Mods/vcmi/Content/Sprites/minimapIcons/borderguard.png
  19. BIN
      Mods/vcmi/Content/Sprites/minimapIcons/keymaster.png
  20. BIN
      Mods/vcmi/Content/Sprites/minimapIcons/obelisk.png
  21. BIN
      Mods/vcmi/Content/Sprites/minimapIcons/obeliskVisited.png
  22. 0 0
      Mods/vcmi/Content/Sprites/minimapIcons/portalBidirectional.png
  23. 0 0
      Mods/vcmi/Content/Sprites/minimapIcons/portalEntrance.png
  24. 0 0
      Mods/vcmi/Content/Sprites/minimapIcons/portalExit.png
  25. BIN
      Mods/vcmi/Content/Sprites2x/minimapIcons/bordergate.png
  26. BIN
      Mods/vcmi/Content/Sprites2x/minimapIcons/borderguard.png
  27. BIN
      Mods/vcmi/Content/Sprites2x/minimapIcons/keymaster.png
  28. BIN
      Mods/vcmi/Content/Sprites2x/minimapIcons/obelisk.png
  29. BIN
      Mods/vcmi/Content/Sprites2x/minimapIcons/obeliskVisited.png
  30. 0 0
      Mods/vcmi/Content/Sprites2x/minimapIcons/portalBidirectional.png
  31. 0 0
      Mods/vcmi/Content/Sprites2x/minimapIcons/portalEntrance.png
  32. 0 0
      Mods/vcmi/Content/Sprites2x/minimapIcons/portalExit.png
  33. 785 360
      Mods/vcmi/Content/config/vietnamese.json
  34. 2 2
      Mods/vcmi/mod.json
  35. 1 1
      client/NetPacksClient.cpp
  36. 7 1
      client/mainmenu/CMainMenu.cpp
  37. 8 11
      client/mapView/MapOverlayLogVisualizer.cpp
  38. 1 1
      client/mapView/MapView.cpp
  39. 32 7
      client/media/CVideoHandler.cpp
  40. 2 2
      client/media/CVideoHandler.h
  41. 0 2
      client/renderSDL/CTrueTypeFont.cpp
  42. 2 2
      client/renderSDL/CTrueTypeFont.h
  43. 1 1
      client/renderSDL/ScreenHandler.cpp
  44. 8 2
      client/windows/CHeroOverview.cpp
  45. 2 2
      client/windows/CHeroOverview.h
  46. 12 3
      client/windows/CMessage.cpp
  47. 121 26
      client/windows/InfoWindows.cpp
  48. 36 4
      client/windows/InfoWindows.h
  49. 25 20
      config/ai/nkai/nkai-settings.json
  50. 1 1
      config/bonuses.json
  51. 5 24
      config/gameConfig.json
  52. 3 1
      config/schemas/gameSettings.json
  53. 6 0
      debian/changelog
  54. 1 1
      docs/Readme.md
  55. 3 2
      include/vcmi/Creature.h
  56. 0 4
      include/vcmi/FactionMember.h
  57. 1 0
      launcher/eu.vcmi.VCMI.metainfo.xml
  58. 13 9
      launcher/translation/german.ts
  59. 3 3
      launcher/translation/polish.ts
  60. 13 8
      lib/BasicTypes.cpp
  61. 2 2
      lib/CMakeLists.txt
  62. 2 0
      lib/GameSettings.cpp
  63. 2 0
      lib/IGameSettings.h
  64. 0 30
      lib/TerrainHandler.cpp
  65. 36 7
      lib/TerrainHandler.h
  66. 1 15
      lib/battle/CBattleInfoCallback.cpp
  67. 1 1
      lib/battle/CBattleInfoEssentials.cpp
  68. 77 53
      lib/battle/CUnitState.cpp
  69. 20 30
      lib/battle/CUnitState.h
  70. 3 2
      lib/battle/Unit.h
  71. 212 0
      lib/bonuses/BonusCache.cpp
  72. 201 0
      lib/bonuses/BonusCache.h
  73. 5 4
      lib/bonuses/BonusList.cpp
  74. 2 2
      lib/bonuses/BonusList.h
  75. 0 221
      lib/bonuses/CBonusProxy.cpp
  76. 0 92
      lib/bonuses/CBonusProxy.h
  77. 1 1
      lib/bonuses/IBonusBearer.cpp
  78. 0 12
      lib/bonuses/Updaters.cpp
  79. 205 52
      lib/campaign/CampaignHandler.cpp
  80. 7 0
      lib/campaign/CampaignHandler.h
  81. 35 0
      lib/campaign/CampaignState.cpp
  82. 2 0
      lib/campaign/CampaignState.h
  83. 8 1
      lib/constants/EntityIdentifiers.h
  84. 1 1
      lib/gameState/CGameState.cpp
  85. 19 1
      lib/json/JsonRandom.cpp
  86. 7 0
      lib/json/JsonRandom.h
  87. 9 1
      lib/mapObjectConstructors/CRewardableConstructor.cpp
  88. 2 2
      lib/mapObjects/CArmedInstance.cpp
  89. 2 3
      lib/mapObjects/CArmedInstance.h
  90. 41 84
      lib/mapObjects/CGHeroInstance.cpp
  91. 13 9
      lib/mapObjects/CGHeroInstance.h
  92. 1 0
      lib/mapObjects/CGTownInstance.cpp
  93. 5 0
      lib/mapObjects/CQuest.cpp
  94. 2 1
      lib/mapObjects/CQuest.h
  95. 5 0
      lib/mapObjects/MiscObjects.cpp
  96. 1 0
      lib/mapObjects/MiscObjects.h
  97. 1 94
      lib/mapping/CMap.cpp
  98. 24 11
      lib/mapping/CMap.h
  99. 100 16
      lib/mapping/CMapDefines.h
  100. 3 1
      lib/modding/CModHandler.cpp

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

@@ -267,6 +267,7 @@ jobs:
       env:
         HEROES_3_DATA_PASSWORD: ${{ secrets.HEROES_3_DATA_PASSWORD }}
       if: ${{ env.HEROES_3_DATA_PASSWORD != '' && matrix.test == 1 }}
+      continue-on-error: true
       run: |
         ctest --preset ${{matrix.preset}}
 

+ 0 - 9
AI/BattleAI/BattleExchangeVariant.cpp

@@ -857,15 +857,6 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
 		exchangeBattle->nextRound();
 	}
 
-	// avoid blocking path for stronger stack by weaker stack
-	// the method checks if all stacks can be placed around enemy
-	std::map<BattleHex, battle::Units> reachabilityMap;
-
-	auto hexes = ap.attack.defender->getSurroundingHexes();
-
-	for(auto hex : hexes)
-		reachabilityMap[hex] = getOneTurnReachableUnits(turn, hex);
-
 	auto score = v.getScore();
 
 	if(simulationTurnsCount < totalTurnsCount)

+ 8 - 7
AI/Nullkiller/AIGateway.cpp

@@ -285,8 +285,8 @@ void AIGateway::tileRevealed(const std::unordered_set<int3> & pos)
 			addVisitableObj(obj);
 	}
 
-	if (nullkiller->settings->isUpdateHitmapOnTileReveal())
-		nullkiller->dangerHitMap->reset();
+	if (nullkiller->settings->isUpdateHitmapOnTileReveal() && !pos.empty())
+		nullkiller->dangerHitMap->resetTileOwners();
 }
 
 void AIGateway::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query)
@@ -389,9 +389,10 @@ void AIGateway::objectRemoved(const CGObjectInstance * obj, const PlayerColor &
 	}
 
 	if(obj->ID == Obj::HERO && cb->getPlayerRelations(obj->tempOwner, playerID) == PlayerRelations::ENEMIES)
-	{
-		nullkiller->dangerHitMap->reset();
-	}
+		nullkiller->dangerHitMap->resetHitmap();
+
+	if(obj->ID == Obj::TOWN)
+		nullkiller->dangerHitMap->resetTileOwners();
 }
 
 void AIGateway::showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor)
@@ -507,7 +508,7 @@ void AIGateway::objectPropertyChanged(const SetObjectProperty * sop)
 			else if(relations == PlayerRelations::SAME_PLAYER && obj->ID == Obj::TOWN)
 			{
 				// reevaluate defence for a new town
-				nullkiller->dangerHitMap->reset();
+				nullkiller->dangerHitMap->resetHitmap();
 			}
 		}
 	}
@@ -1246,7 +1247,7 @@ void AIGateway::addVisitableObj(const CGObjectInstance * obj)
 
 	if(obj->ID == Obj::HERO && cb->getPlayerRelations(obj->tempOwner, playerID) == PlayerRelations::ENEMIES)
 	{
-		nullkiller->dangerHitMap->reset();
+		nullkiller->dangerHitMap->resetHitmap();
 	}
 }
 

+ 30 - 30
AI/Nullkiller/Analyzers/BuildAnalyzer.cpp

@@ -11,49 +11,42 @@
 #include "../Engine/Nullkiller.h"
 #include "../Engine/Nullkiller.h"
 #include "../../../lib/entities/building/CBuilding.h"
+#include "../../../lib/IGameSettings.h"
 
 namespace NKAI
 {
 
 void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
 {
-	std::map<BuildingID, BuildingID> parentMap;
-
-	for(auto &pair : developmentInfo.town->getTown()->buildings)
-	{
-		if(pair.second->upgrade != BuildingID::NONE)
-		{
-			parentMap[pair.second->upgrade] = pair.first;
-		}
-	}
-
 	for(int level = 0; level < developmentInfo.town->getTown()->creatures.size(); level++)
 	{
 		logAi->trace("Checking dwelling level %d", level);
-		BuildingInfo nextToBuild = BuildingInfo();
+		std::vector<BuildingID> dwellingsInTown;
 
-		BuildingID buildID = BuildingID(BuildingID::getDwellingFromLevel(level, 0));
+		for(BuildingID buildID = BuildingID::getDwellingFromLevel(level, 0); buildID.hasValue(); BuildingID::advanceDwelling(buildID))
+			if(developmentInfo.town->getTown()->buildings.count(buildID) != 0)
+				dwellingsInTown.push_back(buildID);
 
-		for(; developmentInfo.town->getBuildings().count(buildID); BuildingID::advanceDwelling(buildID))
+		// find best, already built dwelling
+		for (const auto & buildID : boost::adaptors::reverse(dwellingsInTown))
 		{
-			if(!developmentInfo.town->hasBuilt(buildID))
-				continue; // no such building in town
-
-			auto info = getBuildingOrPrerequisite(developmentInfo.town, buildID);
-
-			if(info.exists)
-			{
-				developmentInfo.addExistingDwelling(info);
+			if (!developmentInfo.town->hasBuilt(buildID))
+				continue;
 
-				break;
-			}
-
-			nextToBuild = info;
+			const auto & info = getBuildingOrPrerequisite(developmentInfo.town, buildID);
+			developmentInfo.addExistingDwelling(info);
+			break;
 		}
 
-		if(nextToBuild.id != BuildingID::NONE)
+		// find all non-built dwellings that can be built and add them for consideration
+		for (const auto & buildID : dwellingsInTown)
 		{
-			developmentInfo.addBuildingToBuild(nextToBuild);
+			if (developmentInfo.town->hasBuilt(buildID))
+				continue;
+
+			const auto & info = getBuildingOrPrerequisite(developmentInfo.town, buildID);
+			if (info.canBuild || info.notEnoughRes)
+				developmentInfo.addBuildingToBuild(info);
 		}
 	}
 }
@@ -148,6 +141,9 @@ void BuildAnalyzer::update()
 
 	for(const CGTownInstance* town : towns)
 	{
+		if(town->built >= cb->getSettings().getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP))
+			continue; // Not much point in trying anything - can't built in this town anymore today
+
 		logAi->trace("Checking town %s", town->getNameTranslated());
 
 		developmentInfos.push_back(TownDevelopmentInfo(town));
@@ -272,11 +268,11 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
 
 			if(vstd::contains_if(missingBuildings, otherDwelling))
 			{
-				logAi->trace("cant build. Need other dwelling");
+				logAi->trace("cant build %d. Need other dwelling %d", toBuild.getNum(), missingBuildings.front().getNum());
 			}
 			else if(missingBuildings[0] != toBuild)
 			{
-				logAi->trace("cant build. Need %d", missingBuildings[0].num);
+				logAi->trace("cant build %d. Need %d", toBuild.getNum(), missingBuildings[0].num);
 
 				BuildingInfo prerequisite = getBuildingOrPrerequisite(town, missingBuildings[0], excludeDwellingDependencies);
 
@@ -307,10 +303,14 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
 				return info;
 			}
 		}
+		else
+		{
+			logAi->trace("Cant build. Reason: %d", static_cast<int>(canBuild));
+		}
 	}
 	else
 	{
-		logAi->trace("exists");
+		logAi->trace("Dwelling %d exists", toBuild.getNum());
 		info.exists = true;
 	}
 

+ 1 - 7
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp

@@ -119,7 +119,7 @@ void DangerHitMapAnalyzer::updateHitMap()
 
 		PathfinderSettings ps;
 
-		ps.scoutTurnDistanceLimit = ps.mainTurnDistanceLimit = ai->settings->getMainHeroTurnDistanceLimit();
+		ps.scoutTurnDistanceLimit = ps.mainTurnDistanceLimit = ai->settings->getThreatTurnDistanceLimit();
 		ps.useHeroChain = false;
 
 		ai->pathfinder->updatePaths(pair.second, ps);
@@ -345,10 +345,4 @@ std::set<const CGObjectInstance *> DangerHitMapAnalyzer::getOneTurnAccessibleObj
 	return result;
 }
 
-void DangerHitMapAnalyzer::reset()
-{
-	hitMapUpToDate = false;
-	tileOwnersUpToDate = false;
-}
-
 }

+ 1 - 1
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h

@@ -87,7 +87,7 @@ public:
 	const HitMapNode & getObjectThreat(const CGObjectInstance * obj) const;
 	const HitMapNode & getTileThreat(const int3 & tile) const;
 	std::set<const CGObjectInstance *> getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const;
-	void reset();
+	void resetHitmap() {hitMapUpToDate = false;}
 	void resetTileOwners() { tileOwnersUpToDate = false; }
 	PlayerColor getTileOwner(const int3 & tile) const;
 	const CGTownInstance * getClosestTown(const int3 & tile) const;

+ 5 - 3
AI/Nullkiller/Behaviors/BuildingBehavior.cpp

@@ -59,17 +59,19 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const
 		{
 			closestThreat = std::min(closestThreat, threat.turn);
 		}
-		for (auto& buildingInfo : developmentInfo.toBuild)
+
+		if (closestThreat <= 1 && developmentInfo.town->fortLevel() < CGTownInstance::EFortLevel::CASTLE)
 		{
-			if (closestThreat <= 1 && developmentInfo.town->fortLevel() < CGTownInstance::EFortLevel::CASTLE && !buildingInfo.notEnoughRes)
+			for (auto& buildingInfo : developmentInfo.toBuild)
 			{
-				if (buildingInfo.id == BuildingID::CITADEL || buildingInfo.id == BuildingID::CASTLE)
+				if ( !buildingInfo.notEnoughRes && (buildingInfo.id == BuildingID::CITADEL || buildingInfo.id == BuildingID::CASTLE))
 				{
 					tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
 					emergencyDefense = true;
 				}
 			}
 		}
+
 		if (!emergencyDefense)
 		{
 			for (auto& buildingInfo : developmentInfo.toBuild)

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

@@ -229,7 +229,7 @@ void Nullkiller::resetAiState()
 	lockedResources = TResources();
 	scanDepth = ScanDepth::MAIN_FULL;
 	lockedHeroes.clear();
-	dangerHitMap->reset();
+	dangerHitMap->resetHitmap();
 	useHeroChain = true;
 	objectClusterizer->reset();
 

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

@@ -28,6 +28,7 @@ namespace NKAI
 		: maxRoamingHeroes(8),
 		mainHeroTurnDistanceLimit(10),
 		scoutHeroTurnDistanceLimit(5),
+		threatTurnDistanceLimit(5),
 		maxGoldPressure(0.3f),
 		retreatThresholdRelative(0.3),
 		retreatThresholdAbsolute(10000),

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

@@ -24,6 +24,7 @@ namespace NKAI
 		int maxRoamingHeroes;
 		int mainHeroTurnDistanceLimit;
 		int scoutHeroTurnDistanceLimit;
+		int threatTurnDistanceLimit;
 		int maxPass;
 		int maxPriorityPass;
 		int pathfinderBucketsCount;
@@ -52,6 +53,7 @@ namespace NKAI
 		int getMaxRoamingHeroes() const { return maxRoamingHeroes; }
 		int getMainHeroTurnDistanceLimit() const { return mainHeroTurnDistanceLimit; }
 		int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; }
+		int getThreatTurnDistanceLimit() const { return threatTurnDistanceLimit; }
 		int getPathfinderBucketsCount() const { return pathfinderBucketsCount; }
 		int getPathfinderBucketSize() const { return pathfinderBucketSize; }
 		bool isObjectGraphAllowed() const { return allowObjectGraph; }

+ 1 - 1
AI/Nullkiller/Pathfinding/AINodeStorage.h

@@ -11,7 +11,7 @@
 #pragma once
 
 #define NKAI_PATHFINDER_TRACE_LEVEL 0
-constexpr int NKAI_GRAPH_TRACE_LEVEL = 0;
+constexpr int NKAI_GRAPH_TRACE_LEVEL = 0; // To actually enable graph visualization, enter `/vslog graph` in game chat
 #define NKAI_TRACE_LEVEL 0
 
 #include "../../../lib/pathfinder/CGPathNode.h"

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

@@ -47,11 +47,10 @@ ChainActor::ChainActor(const CGHeroInstance * hero, HeroRole heroRole, uint64_t
 	initialTurn = 0;
 	armyValue = getHeroArmyStrengthWithCommander(hero, hero);
 	heroFightingStrength = hero->getHeroStrength();
-	tiCache.reset(new TurnInfo(hero));
 }
 
 ChainActor::ChainActor(const ChainActor * carrier, const ChainActor * other, const CCreatureSet * heroArmy)
-	:hero(carrier->hero), tiCache(carrier->tiCache), heroRole(carrier->heroRole), isMovable(true), creatureSet(heroArmy), chainMask(carrier->chainMask | other->chainMask),
+	:hero(carrier->hero), heroRole(carrier->heroRole), isMovable(true), creatureSet(heroArmy), chainMask(carrier->chainMask | other->chainMask),
 	baseActor(this), carrierParent(carrier), otherParent(other), heroFightingStrength(carrier->heroFightingStrength),
 	actorExchangeCount(carrier->actorExchangeCount + other->actorExchangeCount), armyCost(carrier->armyCost + other->armyCost), actorAction()
 {
@@ -75,7 +74,7 @@ int ChainActor::maxMovePoints(CGPathNode::ELayer layer)
 		throw std::logic_error("Asking movement points for static actor");
 #endif
 
-	return hero->movementPointsLimitCached(layer, tiCache.get());
+	return hero->movementPointsLimit(layer);
 }
 
 std::string ChainActor::toString() const
@@ -133,7 +132,6 @@ void ChainActor::setBaseActor(HeroActor * base)
 	heroFightingStrength = base->heroFightingStrength;
 	armyCost = base->armyCost;
 	actorAction = base->actorAction;
-	tiCache = base->tiCache;
 	actorExchangeCount = base->actorExchangeCount;
 }
 

+ 0 - 1
AI/Nullkiller/Pathfinding/Actors.h

@@ -73,7 +73,6 @@ public:
 	float heroFightingStrength;
 	uint8_t actorExchangeCount;
 	TResources armyCost;
-	std::shared_ptr<TurnInfo> tiCache;
 
 	ChainActor() = default;
 	virtual ~ChainActor() = default;

+ 1 - 0
AI/Nullkiller/Pathfinding/GraphPaths.cpp

@@ -367,6 +367,7 @@ void GraphPaths::quickAddChainInfoWithBlocker(std::vector<AIPath> & paths, int3
 			// final node
 			n.coord = tile;
 			n.cost = targetNode.cost;
+			n.turns = static_cast<ui8>(targetNode.cost);
 			n.danger = danger;
 			n.parentIndex = path.nodes.size();
 			path.nodes.push_back(n);

+ 31 - 0
ChangeLog.md

@@ -1,5 +1,36 @@
 # VCMI Project Changelog
 
+## 1.6.2 -> 1.6.3
+
+### Stability
+
+* Fixed possible crash on attempt to play corrupted video file
+* Fixed possible crash on invalid or corrupted game data
+* Fixed possible crash on invalid upscaling filter
+
+### Interface
+
+* Added right-click popup to Keymasters, Border Guards, and Border Gates that show all discovered objects of the same color
+* Added right-click popup to Obelisks that shows all discovered objects and their visitation status
+* Added support for randomly selected main menu backgrounds
+* Fixed display of long text in text-only right-click popups
+* Hero overview screen on map setup will now have scrollbars for hero desription when necessary
+* Fixed teleporter right-click popup appearing out of screen when interacting with teleporter near screen edge
+* Scenario Information window will now correctly replace random hero with actual starting hero
+
+### AI
+
+* Improved performance of Battle AI
+* Improved performance of pathfinding calculations
+* Reduced calculation of threat range, especially on low difficulties to improve performance
+* Fixed Nullkiller AI not considering nearby objects for visiting in some cases, breaking its exploration logic
+* Fixed Nullkiller AI not building town dwellings
+
+### Map Editor
+
+* Added option to convert .h3c files into .vcmp
+* It is now possible to configure town to have same faction as player
+
 ## 1.6.1 -> 1.6.2
 
 ### General

+ 9 - 0
Global.h

@@ -369,6 +369,15 @@ namespace vstd
 		return it->second;
 	}
 
+	// given a map from keys to values, creates a new map from values to keys 
+	template<typename K, typename V>
+	static std::map<V, K> reverseMap(const std::map<K, V>& m) {
+		std::map<V, K> r;
+ 		for (const auto& kv : m)
+			r[kv.second] = kv.first;
+		return r;
+	}
+
 	//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)

BIN
Mods/vcmi/Content/Sprites/minimapIcons/bordergate.png


BIN
Mods/vcmi/Content/Sprites/minimapIcons/borderguard.png


BIN
Mods/vcmi/Content/Sprites/minimapIcons/keymaster.png


BIN
Mods/vcmi/Content/Sprites/minimapIcons/obelisk.png


BIN
Mods/vcmi/Content/Sprites/minimapIcons/obeliskVisited.png


+ 0 - 0
Mods/vcmi/Content/Sprites/portalBidirectional.png → Mods/vcmi/Content/Sprites/minimapIcons/portalBidirectional.png


+ 0 - 0
Mods/vcmi/Content/Sprites/portalEntrance.png → Mods/vcmi/Content/Sprites/minimapIcons/portalEntrance.png


+ 0 - 0
Mods/vcmi/Content/Sprites/portalExit.png → Mods/vcmi/Content/Sprites/minimapIcons/portalExit.png


BIN
Mods/vcmi/Content/Sprites2x/minimapIcons/bordergate.png


BIN
Mods/vcmi/Content/Sprites2x/minimapIcons/borderguard.png


BIN
Mods/vcmi/Content/Sprites2x/minimapIcons/keymaster.png


BIN
Mods/vcmi/Content/Sprites2x/minimapIcons/obelisk.png


BIN
Mods/vcmi/Content/Sprites2x/minimapIcons/obeliskVisited.png


+ 0 - 0
Mods/vcmi/Content/Sprites2x/portalBidirectional.png → Mods/vcmi/Content/Sprites2x/minimapIcons/portalBidirectional.png


+ 0 - 0
Mods/vcmi/Content/Sprites2x/portalEntrance.png → Mods/vcmi/Content/Sprites2x/minimapIcons/portalEntrance.png


+ 0 - 0
Mods/vcmi/Content/Sprites2x/portalExit.png → Mods/vcmi/Content/Sprites2x/minimapIcons/portalExit.png


+ 785 - 360
Mods/vcmi/Content/config/vietnamese.json

@@ -1,362 +1,787 @@
 {
-  "vcmi.adventureMap.monsterThreat.title": "\n\nMức độ: ",
-  "vcmi.adventureMap.monsterThreat.levels.0": "Nhẹ nhàng",
-  "vcmi.adventureMap.monsterThreat.levels.1": "Rất yếu",
-  "vcmi.adventureMap.monsterThreat.levels.2": "Yếu",
-  "vcmi.adventureMap.monsterThreat.levels.3": "Yếu hơn",
-  "vcmi.adventureMap.monsterThreat.levels.4": "Ngang bằng",
-  "vcmi.adventureMap.monsterThreat.levels.5": "Nhỉnh hơn",
-  "vcmi.adventureMap.monsterThreat.levels.6": "Mạnh",
-  "vcmi.adventureMap.monsterThreat.levels.7": "Rất mạnh",
-  "vcmi.adventureMap.monsterThreat.levels.8": "Thách thức",
-  "vcmi.adventureMap.monsterThreat.levels.9": "Áp đảo",
-  "vcmi.adventureMap.monsterThreat.levels.10": "Chết chóc",
-  "vcmi.adventureMap.monsterThreat.levels.11": "Bất khả diệt",
-
-  "vcmi.adventureMap.confirmRestartGame": "Bạn muốn chơi lại?",
-  "vcmi.adventureMap.noTownWithMarket": "Chợ không có sẵn!",
-  "vcmi.adventureMap.noTownWithTavern": "Thành không có sẵn quán rượu!",
-  "vcmi.adventureMap.spellUnknownProblem": "Phép này có lỗi! Không có thông tin nào khác.",
-  "vcmi.adventureMap.playerAttacked": "Người chơi bị tấn công: %s",
-  "vcmi.adventureMap.moveCostDetails": "Điểm di chuyển - Cần: %TURNS lượt + %POINTS điểm, Còn lại: %REMAINING",
-  "vcmi.adventureMap.moveCostDetailsNoTurns": "Điểm di chuyển - Cần: %POINTS điểm, Còn lại: %REMAINING",
-
-  "vcmi.capitalColors.0": "Đỏ",
-  "vcmi.capitalColors.1": "Xanh dương",
-  "vcmi.capitalColors.2": "Nâu",
-  "vcmi.capitalColors.3": "Xanh lá",
-  "vcmi.capitalColors.4": "Cam",
-  "vcmi.capitalColors.5": "Tím",
-  "vcmi.capitalColors.6": "Xanh đậm",
-  "vcmi.capitalColors.7": "Hồng",
-
-  "vcmi.heroOverview.startingArmy": "Lính ban đầu",
-  "vcmi.heroOverview.warMachine": "Chiến cơ",
-  "vcmi.heroOverview.secondarySkills": "Kĩ năng",
-  "vcmi.heroOverview.spells": "Phép",
-
-  "vcmi.radialWheel.mergeSameUnit": "Sáp nhập cùng loài",
-  "vcmi.radialWheel.fillSingleUnit": "Làm đầy với từng loài",
-  "vcmi.radialWheel.splitSingleUnit": "Tách 1 loài",
-  "vcmi.radialWheel.splitUnitEqually": "Chia quái bằng nhau",
-  "vcmi.radialWheel.moveUnit": "Di chuyển quái đến đội khác",
-  "vcmi.radialWheel.splitUnit": "Chia quái đến ô khác",
-
-  "vcmi.mainMenu.highscoresNotImplemented": "Xin lỗi, bảng xếp hạng chưa được làm đầy đủ\n",
-  "vcmi.mainMenu.serverConnecting": "Đang kết nối...",
-  "vcmi.mainMenu.serverAddressEnter": "Nhập địa chỉ:",
-  "vcmi.mainMenu.serverClosing": "Đang hủy kết nối...",
-  "vcmi.mainMenu.hostTCP": "Chủ phòng TCP/IP",
-  "vcmi.mainMenu.joinTCP": "Tham gia TCP/IP",
-  "vcmi.mainMenu.playerName": "Người chơi",
-
-  "vcmi.lobby.filepath": "Tên tập tin",
-  "vcmi.lobby.creationDate": "Ngày tạo",
-
-  "vcmi.server.errors.existingProcess": "1 chương trình VCMI khác đang chạy. Tắt nó trước khi mở cái mới",
-  "vcmi.server.errors.modsIncompatibility": "Các bản sửa đổi cần để tải trò chơi:",
-  "vcmi.server.confirmReconnect": "Bạn có muốn kết nối lại phiên trước?",
-
-  "vcmi.settingsMainWindow.generalTab.hover": "Chung",
-  "vcmi.settingsMainWindow.generalTab.help": "Chuyển sang bảng Chung, chứa các cài đặt liên quan đến phần chung trò chơi",
-  "vcmi.settingsMainWindow.battleTab.hover": "Chiến đấu",
-  "vcmi.settingsMainWindow.battleTab.help": "Chuyển sang bảng Chiến đấu, cho phép thiết lập hành vi trong trận đánh",
-  "vcmi.settingsMainWindow.adventureTab.hover": "Phiêu lưu",
-  "vcmi.settingsMainWindow.adventureTab.help": "Chuyển sang bảng Phiêu lưu (bản đồ phiêu lưu là nơi mà người chơi di chuyển tướng của họ)",
-
-  "vcmi.systemOptions.videoGroup": "Thiết lập phim ảnh",
-  "vcmi.systemOptions.audioGroup": "Thiết lập âm thanh",
-  "vcmi.systemOptions.otherGroup": "Thiết lập khác",
-  "vcmi.systemOptions.townsGroup": "Thành phố",
-
-  "vcmi.systemOptions.fullscreenBorderless.hover": "Toàn màn hình (không viền)",
-  "vcmi.systemOptions.fullscreenBorderless.help": "{Toàn màn hình không viền}\n\nNếu chọn, VCMI sẽ chạy chế độ toàn màn hình không viền. Ở chế độ này, trò chơi sẽ luôn dùng độ phân giải của màn hình, bỏ qua độ phân giải đã chọn.",
-  "vcmi.systemOptions.fullscreenExclusive.hover": "Toàn màn hình (riêng biệt)",
-  "vcmi.systemOptions.fullscreenExclusive.help": "{Toàn màn hình}\n\nNếu chọn, VCMI sẽ chạy chế độ dành riêng cho toàn màn hình. Ở chế độ này, trò chơi sẽ chuyển độ phân giải của màn hình sang độ phân giải được chọn.",
-  "vcmi.systemOptions.resolutionButton.hover": "Độ phân giải: %wx%h",
-  "vcmi.systemOptions.resolutionButton.help": "{Chọn độ phân giải}\n\nĐổi độ phân giải trong trò chơi.",
-  "vcmi.systemOptions.resolutionMenu.hover": "Chọn độ phân giải",
-  "vcmi.systemOptions.resolutionMenu.help": "Đổi độ phân giải trong trò chơi.",
-  "vcmi.systemOptions.scalingButton.hover": "Phóng đại giao diện: %p%",
-  "vcmi.systemOptions.scalingButton.help": "{Phóng đại giao diện}\n\nĐổi độ phóng đại giao diện trong trò chơi.",
-  "vcmi.systemOptions.scalingMenu.hover": "Chọn độ phóng đại giao diện",
-  "vcmi.systemOptions.scalingMenu.help": "Đổi độ phóng đại giao diện trong trò chơi.",
-  "vcmi.systemOptions.longTouchButton.hover": "Khoảng thời gian chạm giữ: %d ms",
-  "vcmi.systemOptions.longTouchButton.help": "{Khoảng thời gian chạm giữ}\n\nKhi dùng màn hình cảm ứng, cửa sổ sẽ bật lên sau khi chạm màn hình trong 1 khoảng thời gian xác định, theo mili giây.",
-  "vcmi.systemOptions.longTouchMenu.hover": "Chọn khoảng thời gian chạm giữ",
-  "vcmi.systemOptions.longTouchMenu.help": "Đổi khoảng thời gian chạm giữ.",
-  "vcmi.systemOptions.longTouchMenu.entry": "%d mili giây",
-  "vcmi.systemOptions.framerateButton.hover": "Hiện FPS",
-  "vcmi.systemOptions.framerateButton.help": "{Hiện FPS}\n\nHiện khung hình mỗi giây ở góc cửa sổ trò chơi",
-  "vcmi.systemOptions.hapticFeedbackButton.hover": "Rung khi chạm",
-  "vcmi.systemOptions.hapticFeedbackButton.help": "{Rung khi chạm}\n\nBật/ tắt chế độ rung khi chạm.",
-
-  "vcmi.adventureOptions.infoBarPick.hover": "Hiện thông báo ở bảng thông tin",
-  "vcmi.adventureOptions.infoBarPick.help": "{Hiện thông báo ở bảng thông tin}\n\nThông báo từ các điểm đến thăm sẽ hiện ở bảng thông tin, thay vì trong cửa sổ bật lên.",
-  "vcmi.adventureOptions.numericQuantities.hover": "Số lượng quái",
-  "vcmi.adventureOptions.numericQuantities.help": "{Số lượng quái}\n\nHiện lượng quái đối phương dạng số A-B.",
-  "vcmi.adventureOptions.forceMovementInfo.hover": "Luôn hiện chi phí di chuyển",
-  "vcmi.adventureOptions.forceMovementInfo.help": "{Luôn hiện chi phí di chuyển}\n\nLuôn hiện điểm di chuyển trong thanh trạng thái. (Thay vì chỉ xem khi nhấn giữ phím ALT)",
-  "vcmi.adventureOptions.showGrid.hover": "Hiện ô kẻ",
-  "vcmi.adventureOptions.showGrid.help": "{Hiện ô kẻ}\n\nHiện đường biên giữa các ô trên bản đồ phiêu lưu.",
-  "vcmi.adventureOptions.borderScroll.hover": "Cuộn ở biên",
-  "vcmi.adventureOptions.borderScroll.help": "{Cuộn ở biên}\n\nCuộn bản đồ phiêu lưu ở biên. Nhấn giữ phím CTRL để tắt chức năng.",
-  "vcmi.adventureOptions.infoBarCreatureManagement.hover": "Quản lí quái ở bảng thông tin",
-  "vcmi.adventureOptions.infoBarCreatureManagement.help": "{Quản lí quái ở bảng thông tin}\n\nCho phép sắp xếp quái ở bảng thông tin thay vì luân chuyển giữa các mục mặc định.",
-  "vcmi.adventureOptions.leftButtonDrag.hover": "Chuột trái kéo bản đồ",
-  "vcmi.adventureOptions.leftButtonDrag.help": "{Chuột trái kéo bản đồ}\n\nGiữ chuột trái khi di chuyển sẽ dịch chuyển bản đồ phiêu lưu.",
-  "vcmi.adventureOptions.mapScrollSpeed1.hover": "",
-  "vcmi.adventureOptions.mapScrollSpeed5.hover": "",
-  "vcmi.adventureOptions.mapScrollSpeed6.hover": "",
-  "vcmi.adventureOptions.mapScrollSpeed1.help": "Đặt tốc độ cuộn bản đồ sang rất chậm",
-  "vcmi.adventureOptions.mapScrollSpeed5.help": "Đặt tốc độ cuộn bản đồ sang rất nhanh",
-  "vcmi.adventureOptions.mapScrollSpeed6.help": "Đặt tốc độ cuộn bản đồ sang tức thời.",
-
-  "vcmi.battleOptions.queueSizeLabel.hover": "Hiện thứ tự lượt",
-  "vcmi.battleOptions.queueSizeNoneButton.hover": "TẮT",
-  "vcmi.battleOptions.queueSizeAutoButton.hover": "TỰ ĐỘNG",
-  "vcmi.battleOptions.queueSizeSmallButton.hover": "NHỎ",
-  "vcmi.battleOptions.queueSizeBigButton.hover": "LỚN",
-  "vcmi.battleOptions.queueSizeNoneButton.help": "Không hiện thứ tự lượt.",
-  "vcmi.battleOptions.queueSizeAutoButton.help": "Tự động điều chỉnh kích thước thứ tự lượt theo độ phân giải (NHỎ được dùng khi chiều cao thấp hơn 700 px, ngược lại dùng LỚN).",
-  "vcmi.battleOptions.queueSizeSmallButton.help": "Đặt kích thước thứ tự lượt sang NHỎ.",
-  "vcmi.battleOptions.queueSizeBigButton.help": "Đặt kích thước thứ tự lượt sang LỚN (không hỗ trợ nếu chiều cao nhỏ hơn 700 px).",
-  "vcmi.battleOptions.animationsSpeed1.hover": "",
-  "vcmi.battleOptions.animationsSpeed5.hover": "",
-  "vcmi.battleOptions.animationsSpeed6.hover": "",
-  "vcmi.battleOptions.animationsSpeed1.help": "Đặt tốc độ hoạt ảnh sang rất chậm",
-  "vcmi.battleOptions.animationsSpeed5.help": "Đặt tốc độ hoạt ảnh sang rất nhanh",
-  "vcmi.battleOptions.animationsSpeed6.help": "Đặt tốc độ hoạt ảnh sang tức thời",
-  "vcmi.battleOptions.movementHighlightOnHover.hover": "Hiện di chuyển khi di chuột",
-  "vcmi.battleOptions.movementHighlightOnHover.help": "{Hiện di chuyển khi di chuột}\n\nHiện giới hạn di chuyển của quái khi di chuột lên chúng.",
-  "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Hiện tầm bắn của cung thủ",
-  "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Hiện tầm bắn của cung thủ khi di chuột}\n\nHiện tầm bắn của cung thủ khi di chuột lên chúng.",
-  "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Hiện thông số tướng",
-  "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Hiện thông số tướng}\n\nBật/ tắt bảng chỉ số cơ bản và năng lượng của tướng.",
-  "vcmi.battleOptions.skipBattleIntroMusic.hover": "Bỏ qua nhạc dạo đầu",
-  "vcmi.battleOptions.skipBattleIntroMusic.help": "{Bỏ qua nhạc dạo đầu}\n\nKhông cần chờ hết nhạc khởi đầu mỗi trận đánh",
-
-  "vcmi.battleWindow.pressKeyToSkipIntro": "Nhấn phím bất kì để bắt đầu trận đánh",
-  "vcmi.battleWindow.damageEstimation.melee": "Tấn công %CREATURE (%DAMAGE).",
-  "vcmi.battleWindow.damageEstimation.meleeKills": "Tấn công %CREATURE (%DAMAGE, %KILLS).",
-  "vcmi.battleWindow.damageEstimation.ranged": "Bắn %CREATURE (%SHOTS, %DAMAGE).",
-  "vcmi.battleWindow.damageEstimation.rangedKills": "Bắn %CREATURE (%SHOTS, %DAMAGE, %KILLS).",
-  "vcmi.battleWindow.damageEstimation.shots": "Còn %d lần",
-  "vcmi.battleWindow.damageEstimation.shots.1": "Còn %d lần",
-  "vcmi.battleWindow.damageEstimation.damage": "%d sát thương",
-  "vcmi.battleWindow.damageEstimation.damage.1": "%d sát thương",
-  "vcmi.battleWindow.damageEstimation.kills": "%d sẽ bị diệt",
-  "vcmi.battleWindow.damageEstimation.kills.1": "%d sẽ bị diệt",
-
-  "vcmi.battleResultsWindow.applyResultsLabel": "Dùng kết quả trận đánh",
-
-  "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover": "Hiện quái được mua",
-  "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help": "{Hiện quái được mua}\n\nHiện quái được mua thay vì sinh trưởng trong sơ lược thành (góc trái dưới màn hình thành phố).",
-  "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover": "Hiện sinh trưởng quái hàng tuần",
-  "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help": "{Hiện sinh trưởng quái hàng tuần}\n\nHiện sinh trưởng quái thay vì lượng có sẵn trong sơ lược thành (góc trái dưới màn hình thành phố).",
-  "vcmi.otherOptions.compactTownCreatureInfo.hover": "Thu gọn thông tin quái",
-  "vcmi.otherOptions.compactTownCreatureInfo.help": "{Thu gọn thông tin quái}\n\nHiện thông tin quái nhỏ hơn trong sơ lược thành (góc trái dưới màn hình thành phố).",
-
-  "vcmi.townHall.missingBase": "Căn cứ %s phải được xây trước",
-  "vcmi.townHall.noCreaturesToRecruit": "Không có quái để chiêu mộ!",
-
-  "vcmi.logicalExpressions.anyOf": "Bất kì cái sau:",
-  "vcmi.logicalExpressions.allOf": "Tất cả cái sau:",
-  "vcmi.logicalExpressions.noneOf": "Không có những cái sau:",
-
-  "vcmi.heroWindow.openCommander.hover": "Mở cửa sổ thông tin chỉ huy",
-  "vcmi.heroWindow.openCommander.help": "Hiện chi tiết về chỉ huy tướng này",
-  "vcmi.heroWindow.openBackpack.hover": "Mở hành lí",
-  "vcmi.heroWindow.openBackpack.help": "Hành lí cho phép quản lí vật phẩm dễ dàng hơn.",
-
-  "vcmi.commanderWindow.artifactMessage": "Bạn muốn trả lại vật phẩm này cho tướng?",
-
-  "vcmi.creatureWindow.showBonuses.hover": "Chuyển sang phần tăng thêm",
-  "vcmi.creatureWindow.showBonuses.help": "Hiện thuộc tính tăng thêm của chỉ huy",
-  "vcmi.creatureWindow.showSkills.hover": "Chuyển sang phần kĩ năng",
-  "vcmi.creatureWindow.showSkills.help": "Hiện kĩ năng đã học của chỉ huy",
-  "vcmi.creatureWindow.returnArtifact.hover": "Trả vật phẩm",
-  "vcmi.creatureWindow.returnArtifact.help": "Nhấn nút này để trả vật phẩm cho tướng",
-
-  "vcmi.questLog.hideComplete.hover": "Ẩn nhiệm vụ đã làm",
-  "vcmi.questLog.hideComplete.help": "Ẩn các nhiệm vụ đã hoàn thành",
-
-  "vcmi.randomMapTab.widgets.defaultTemplate": "(mặc định)",
-  "vcmi.randomMapTab.widgets.templateLabel": "Mẫu",
-  "vcmi.randomMapTab.widgets.teamAlignmentsButton": "Cài đặt...",
-  "vcmi.randomMapTab.widgets.teamAlignmentsLabel": "Sắp đội",
-  "vcmi.randomMapTab.widgets.roadTypesLabel": "Kiểu đường xá",
-
-  "vcmi.optionsTab.chessFieldBase.hover" : "Thời gian thêm",
-  "vcmi.optionsTab.chessFieldTurn.hover" : "Thời gian lượt",
-  "vcmi.optionsTab.chessFieldBattle.hover" : "Thời gian trận đánh",
-  "vcmi.optionsTab.chessFieldUnit.hover" : "Thời gian lính",
-  "vcmi.optionsTab.chessFieldBase.help": "Bắt đầu đếm ngược khi {Thời gian lượt} giảm đến 0. Được đặt 1 lần khi bắt đầu trò chơi. Khi thời gian này giảm đến 0, lượt của người chơi kết thúc.",
-  "vcmi.optionsTab.chessFieldTurn.help": "Bắt đầu đếm ngược khi đến lượt người chơi trên bản đồ phiêu lưu. Nó được đặt lại giá trị ban đầu khi bắt đầu mỗi lượt. Thời gian lượt chưa sử dụng sẽ được thêm vào {Thời gian thêm} nếu có.",
-  "vcmi.optionsTab.chessFieldBattle.help": "Đếm ngược trong suốt trận đánh khi {Thời gian lính} giảm đến 0. Nó được đặt lại giá trị ban đầu khi bắt đầu mỗi trận đánh. Nếu thời gian giảm đến 0, đội lính hiện tại sẽ phòng thủ.",
-  "vcmi.optionsTab.chessFieldUnit.help": "Bắt đầu đếm ngược khi người chơi đang chọn hành động cho đội linh hiện tại trong suốt trận đánh. Nó được đặt lại giá trị ban đầu sau khi hành động của đội lính hoàn tất.",
-
-  "vcmi.map.victoryCondition.daysPassed.toOthers": "Đối thủ đã xoay xở để sinh tồn đến ngày này. Họ giành chiến thắng!",
-  "vcmi.map.victoryCondition.daysPassed.toSelf": "Chúc mừng! Bạn đã vượt khó để sinh tồn. Chiến thắng thuộc về bạn!",
-  "vcmi.map.victoryCondition.eliminateMonsters.toOthers": "Đối thủ đã diệt tất cả quái gây hại vùng này và giành chiến thắng!",
-  "vcmi.map.victoryCondition.eliminateMonsters.toSelf": "Chúc mừng! Bạn đã diệt tất cả quái gây hại vùng này và giành chiến thắng!",
-  "vcmi.map.victoryCondition.collectArtifacts.message": "Đoạt 3 vật phẩm",
-  "vcmi.map.victoryCondition.angelicAlliance.toSelf": "Chúc mừng! Tất cả đối thủ bị đánh bại và bạn có Angelic Alliance! Chiến thắng thuộc về bạn!",
-  "vcmi.map.victoryCondition.angelicAlliance.message": "Đánh bại tất cả đối thủ và tạo Angelic Alliance",
-
-  "vcmi.stackExperience.description": "» K I N H  N G H I Ệ M «\n\nLoại Quái ................... : %s\nCấp Kinh Nghiệm ................. : %s (%i)\nĐiểm Kinh Nghiệm ............... : %i\nĐiểm Kinh Nghiệm Để Lên Cấp .. : %i\nKinh Nghiệm Tối Đa Mỗi Trận Đánh ... : %i%% (%i)\nSố Lượng Quái .... : %i\nTối Đa Mua Mới\n không bị giảm cấp .... : %i\nHệ Số Kinh Nghiệm ........... : %.2f\nHệ Số Nâng Cấp .............. : %.2f\nKinh Nghiệm Sau Cấp 10 ........ : %i\nTối Đa Mua Mới để vẫn ở\n Mức Tối Đa Kinh Nghiệm Cấp 10 : %i",
-  "vcmi.stackExperience.rank.0": "Lính Mới",
-  "vcmi.stackExperience.rank.1": "Tập Sự",
-  "vcmi.stackExperience.rank.2": "Lành Nghề",
-  "vcmi.stackExperience.rank.3": "Khéo Léo",
-  "vcmi.stackExperience.rank.4": "Thông Thạo",
-  "vcmi.stackExperience.rank.5": "Kì Cựu",
-  "vcmi.stackExperience.rank.6": "Lão Luyện",
-  "vcmi.stackExperience.rank.7": "Chuyên Gia",
-  "vcmi.stackExperience.rank.8": "Tinh Hoa",
-  "vcmi.stackExperience.rank.9": "Bậc Thầy",
-  "vcmi.stackExperience.rank.10": "Thiên Tài",
-
-  "core.bonus.ADDITIONAL_ATTACK.name": "Đánh 2 lần",
-  "core.bonus.ADDITIONAL_ATTACK.description": "Tấn công 2 lần",
-  "core.bonus.ADDITIONAL_RETALIATION.name": "Thêm phản công",
-  "core.bonus.ADDITIONAL_RETALIATION.description": "Phản công thêm ${val} lần",
-  "core.bonus.AIR_IMMUNITY.name": "Kháng Khí",
-  "core.bonus.AIR_IMMUNITY.description": "Miễn dịch tất cả phép thuộc tính Khí",
-  "core.bonus.ATTACKS_ALL_ADJACENT.name": "Đánh xung quanh",
-  "core.bonus.ATTACKS_ALL_ADJACENT.description": "Tấn công tất cả đối phương xung quanh",
-  "core.bonus.BLOCKS_RETALIATION.name": "Ngăn phản công",
-  "core.bonus.BLOCKS_RETALIATION.description": "Đối phương không thể phản công",
-  "core.bonus.BLOCKS_RANGED_RETALIATION.name": "Ngăn bắn phản công",
-  "core.bonus.BLOCKS_RANGED_RETALIATION.description": "Đối phương không thể bắn phản công",
-  "core.bonus.CATAPULT.name": "Công thành",
-  "core.bonus.CATAPULT.description": "Tấn công tường thành",
-  "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Giảm yêu cầu năng lượng (${val})",
-  "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Giảm ${val} năng lượng cần làm phép",
-  "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Tăng yêu cầu năng lượng (${val})",
-  "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Tăng ${val} năng lượng đối phương cần làm phép",
-  "core.bonus.CHARGE_IMMUNITY.name": "Kháng đột kích",
-  "core.bonus.CHARGE_IMMUNITY.description": "Kháng đột kích của Cavalier và Champion",
-  "core.bonus.DARKNESS.name": "Màn tối",
-  "core.bonus.DARKNESS.description": "Tạo màn bóng tối bán kính ${val}",
-  "core.bonus.DEATH_STARE.name": "Cái nhìn chết chóc (${val}%)",
-  "core.bonus.DEATH_STARE.description": "${val}% cơ hội diệt thêm quái",
-  "core.bonus.DEFENSIVE_STANCE.name": "Tăng Thủ",
-  "core.bonus.DEFENSIVE_STANCE.description": "+${val} Thủ khi đang thế thủ",
-  "core.bonus.DESTRUCTION.name": "Hủy diệt",
-  "core.bonus.DESTRUCTION.description": "${val}% cơ hội diệt thêm quái",
-  "core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Đòn chí mạng",
-  "core.bonus.DOUBLE_DAMAGE_CHANCE.description": "${val}% cơ hội nhân đôi sát thương khi tấn công",
-  "core.bonus.DRAGON_NATURE.name": "Rồng",
-  "core.bonus.DRAGON_NATURE.description": "Quái có chất Rồng",
-  "core.bonus.EARTH_IMMUNITY.name": "Kháng Đất",
-  "core.bonus.EARTH_IMMUNITY.description": "Miễn dịch tất cả phép thuộc tính Đất",
-  "core.bonus.ENCHANTER.name": "Bùa chú",
-  "core.bonus.ENCHANTER.description": "Ếm ${subtype.spell} mỗi lượt",
-  "core.bonus.ENCHANTED.name": "Chúc phúc",
-  "core.bonus.ENCHANTED.description": "Nhận phép vĩnh cửu ${subtype.spell}",
-  "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Xuyên giáp (${val}%)",
-  "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Khi tấn công, bỏ qua ${val}% Thủ",
-  "core.bonus.FIRE_IMMUNITY.name": "Kháng Lửa",
-  "core.bonus.FIRE_IMMUNITY.description": "Miễn dịch tất cả phép thuộc tính Lửa",
-  "core.bonus.FIRE_SHIELD.name": "Khiên lửa (${val}%)",
-  "core.bonus.FIRE_SHIELD.description": "Phản ${val}% sát thương cận chiến",
-  "core.bonus.FIRST_STRIKE.name": "Đánh trước",
-  "core.bonus.FIRST_STRIKE.description": "Quái phản công trước khi bị tấn công",
-  "core.bonus.FEAR.name": "Hãi hùng",
-  "core.bonus.FEAR.description": "Gây Hoảng Sợ lên quái đối phương",
-  "core.bonus.FEARLESS.name": "Can đảm",
-  "core.bonus.FEARLESS.description": "Không bị hoảng sợ",
-  "core.bonus.FLYING.name": "Bay",
-  "core.bonus.FLYING.description": "Vượt chướng ngại vật",
-  "core.bonus.FREE_SHOOTING.name": "Bắn gần",
-  "core.bonus.FREE_SHOOTING.description": "Bắn kể cả khi cận chiến",
-  "core.bonus.GARGOYLE.name": "Gargoyle",
-  "core.bonus.GARGOYLE.description": "Không thể hồi sinh",
-  "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Giảm sát thương (${val}%)",
-  "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Giảm ${val}% sát thương vật lí",
-  "core.bonus.HATE.name": "Ghét ${subtype.creature}",
-  "core.bonus.HATE.description": "Tăng thêm ${val}% sát thương cho ${subtype.creature}",
-  "core.bonus.HEALER.name": "Hồi máu",
-  "core.bonus.HEALER.description": "Hồi máu đồng đội",
-  "core.bonus.HP_REGENERATION.name": "Tự hồi máu",
-  "core.bonus.HP_REGENERATION.description": "Hồi ${SHval} máu mỗi lượt",
-  "core.bonus.JOUSTING.name": "Đột kích",
-  "core.bonus.JOUSTING.description": "+${val}% sát thương cho mỗi ô đi qua",
-  "core.bonus.KING.name": "Khổng lồ",
-  "core.bonus.KING.description": "Dễ tổn thương bởi Diệt Khổng Lồ cấp ${val} hoặc cao hơn",
-  "core.bonus.LEVEL_SPELL_IMMUNITY.name": "Kháng phép 1-${val}",
-  "core.bonus.LEVEL_SPELL_IMMUNITY.description": "Kháng phép cấp 1 - ${val}",
-  "core.bonus.LIMITED_SHOOTING_RANGE.name": "Tầm bắn",
-  "core.bonus.LIMITED_SHOOTING_RANGE.description": "Không thể nhắm bắn quái xa hơn ${val} ô",
-  "core.bonus.LIFE_DRAIN.name": "Hút máu (${val}%)",
-  "core.bonus.LIFE_DRAIN.description": "Hồi máu ${val}% sát thương gây ra",
-  "core.bonus.MANA_CHANNELING.name": "Chuyển năng lượng ${val}%",
-  "core.bonus.MANA_CHANNELING.description": "Cho tướng ${val}% năng lượng dùng bởi đối phương",
-  "core.bonus.MANA_DRAIN.name": "Hút năng lượng",
-  "core.bonus.MANA_DRAIN.description": "Hút ${val} năng lượng mỗi lượt",
-  "core.bonus.MAGIC_MIRROR.name": "Phản phép (${val}%)",
-  "core.bonus.MAGIC_MIRROR.description": "${val}% cơ hội phản phép tấn công đến quái đối phương",
-  "core.bonus.MAGIC_RESISTANCE.name": "Né phép (${val}%)",
-  "core.bonus.MAGIC_RESISTANCE.description": "${val}% cơ hội tránh phép của đối phương",
-  "core.bonus.MIND_IMMUNITY.name": "Kháng phép tinh thần",
-  "core.bonus.MIND_IMMUNITY.description": "Kháng ma thuật về tinh thần",
-  "core.bonus.NO_DISTANCE_PENALTY.name": "Bắn xa",
-  "core.bonus.NO_DISTANCE_PENALTY.description": "Gây trọn sát thương ở bất kì khoảng cách nào",
-  "core.bonus.NO_MELEE_PENALTY.name": "Đánh gần",
-  "core.bonus.NO_MELEE_PENALTY.description": "Quái không bị giảm sát thương khi cận chiến",
-  "core.bonus.NO_MORALE.name": "Bình tĩnh",
-  "core.bonus.NO_MORALE.description": "Quái không ảnh hưởng bởi sĩ khí",
-  "core.bonus.NO_WALL_PENALTY.name": "Bỏ qua tường",
-  "core.bonus.NO_WALL_PENALTY.description": "Gây trọn sát thương khi công thành",
-  "core.bonus.NON_LIVING.name": "Vô sinh",
-  "core.bonus.NON_LIVING.description": "Kháng nhiều hiệu ứng",
-  "core.bonus.RANDOM_SPELLCASTER.name": "Ếm ngẫu nhiên",
-  "core.bonus.RANDOM_SPELLCASTER.description": "Ếm phép ngẫu nhiên",
-  "core.bonus.RANGED_RETALIATION.name": "Phản công tầm xa",
-  "core.bonus.RANGED_RETALIATION.description": "Phản công khi bị bắn",
-  "core.bonus.RECEPTIVE.name": "Tiếp thu",
-  "core.bonus.RECEPTIVE.description": "Không kháng phép có lợi",
-  "core.bonus.REBIRTH.name": "Tái sinh (${val}%)",
-  "core.bonus.REBIRTH.description": "${val}% số lượng sẽ hồi sinh sau khi chết",
-  "core.bonus.RETURN_AFTER_STRIKE.name": "Du kích",
-  "core.bonus.RETURN_AFTER_STRIKE.description": "Trở về sau khi đánh",
-  "core.bonus.SHOOTER.name": "Cung thủ",
-  "core.bonus.SHOOTER.description": "Quái có thể tấn công tầm xa",
-  "core.bonus.SHOOTS_ALL_ADJACENT.name": "Bắn xung quanh",
-  "core.bonus.SHOOTS_ALL_ADJACENT.description": "Bắn tất cả quái trong phạm vi nhỏ",
-  "core.bonus.SOUL_STEAL.name": "Hút hồn",
-  "core.bonus.SOUL_STEAL.description": "Tăng ${val} quái mới với mỗi quái đối phương bị diệt",
-  "core.bonus.SPELLCASTER.name": "Pháp sư",
-  "core.bonus.SPELLCASTER.description": "Có thể ếm phép ${subtype.spell}",
-  "core.bonus.SPELL_AFTER_ATTACK.name": "Ếm sau khi đánh",
-  "core.bonus.SPELL_AFTER_ATTACK.description": "${val}% cơ hội ếm phép ${subtype.spell} sau khi tấn công",
-  "core.bonus.SPELL_BEFORE_ATTACK.name": "Ếm trước khi đánh",
-  "core.bonus.SPELL_BEFORE_ATTACK.description": "${val}% cơ hội ếm phép ${subtype.spell} trước khi tấn công",
-  "core.bonus.SPELL_DAMAGE_REDUCTION.name": "Kháng phép",
-  "core.bonus.SPELL_DAMAGE_REDUCTION.description": "Sát thương phép giảm ${val}%",
-  "core.bonus.SPELL_IMMUNITY.name": "Miễn dịch",
-  "core.bonus.SPELL_IMMUNITY.description": "Miễn dịch với phép ${subtype.spell}",
-  "core.bonus.SPELL_LIKE_ATTACK.name": "Đánh phép",
-  "core.bonus.SPELL_LIKE_ATTACK.description": "Tấn công bằng phép ${subtype.spell}",
-  "core.bonus.SPELL_RESISTANCE_AURA.name": "Hào quang kháng phép",
-  "core.bonus.SPELL_RESISTANCE_AURA.description": "Quái ở gần nhận ${val}% kháng ma thuật",
-  "core.bonus.SUMMON_GUARDIANS.name": "Gọi bảo vệ",
-  "core.bonus.SUMMON_GUARDIANS.description": "Đầu trận gọi quái ${subtype.creature} (${val}%)",
-  "core.bonus.SYNERGY_TARGET.name": "Hợp lực",
-  "core.bonus.SYNERGY_TARGET.description": "Quái này dễ bị ảnh hưởng hợp lực",
-  "core.bonus.TWO_HEX_ATTACK_BREATH.name": "Hơi thở",
-  "core.bonus.TWO_HEX_ATTACK_BREATH.description": "Tấn công 2 ô",
-  "core.bonus.THREE_HEADED_ATTACK.name": "Ba đầu",
-  "core.bonus.THREE_HEADED_ATTACK.description": "Tấn công cả quái liền kề mục tiêu",
-  "core.bonus.TRANSMUTATION.name": "Biến đổi",
-  "core.bonus.TRANSMUTATION.description": "${val}% cơ hội biến đổi quái mục tiêu thành dạng khác",
-  "core.bonus.UNDEAD.name": "Thây ma",
-  "core.bonus.UNDEAD.description": "Quái là thây ma",
-  "core.bonus.UNLIMITED_RETALIATIONS.name": "Phản công vô hạn",
-  "core.bonus.UNLIMITED_RETALIATIONS.description": "Không giới hạn số lần phản công",
-  "core.bonus.WATER_IMMUNITY.name": "Kháng Nước",
-  "core.bonus.WATER_IMMUNITY.description": "Miễn dịch tất cả phép thuộc tính Nước",
-  "core.bonus.WIDE_BREATH.name": "Hơi thở sâu",
-  "core.bonus.WIDE_BREATH.description": "Tấn công nhiều ô"
+	"vcmi.adventureMap.monsterThreat.title": "\n\nMức độ: ",
+	"vcmi.adventureMap.monsterThreat.levels.0": "Nhẹ nhàng",
+	"vcmi.adventureMap.monsterThreat.levels.1": "Rất yếu",
+	"vcmi.adventureMap.monsterThreat.levels.2": "Yếu",
+	"vcmi.adventureMap.monsterThreat.levels.3": "Yếu hơn",
+	"vcmi.adventureMap.monsterThreat.levels.4": "Ngang bằng",
+	"vcmi.adventureMap.monsterThreat.levels.5": "Nhỉnh hơn",
+	"vcmi.adventureMap.monsterThreat.levels.6": "Mạnh",
+	"vcmi.adventureMap.monsterThreat.levels.7": "Rất mạnh",
+	"vcmi.adventureMap.monsterThreat.levels.8": "Thách thức",
+	"vcmi.adventureMap.monsterThreat.levels.9": "Áp đảo",
+	"vcmi.adventureMap.monsterThreat.levels.10": "Chết chóc",
+	"vcmi.adventureMap.monsterThreat.levels.11": "Bất khả diệt",
+	"vcmi.adventureMap.monsterLevel": "\n\nCấp %LEVEL %TOWN %ATTACK_TYPE đơn vị",
+	"vcmi.adventureMap.monsterMeleeType": "Cận chiến",
+	"vcmi.adventureMap.monsterRangedType": "Bắn xa",
+	"vcmi.adventureMap.search.hover": "Tìm đối tượng trên bản đồ.",
+	"vcmi.adventureMap.search.help": "Chọn đối tượng để tìm kiếm.",
+
+	"vcmi.adventureMap.confirmRestartGame": "Bạn muốn chơi lại?",
+	"vcmi.adventureMap.noTownWithMarket": "Chợ không có sẵn!",
+	"vcmi.adventureMap.noTownWithTavern": "Thành không có sẵn quán rượu!",
+	"vcmi.adventureMap.spellUnknownProblem": "Phép này có lỗi! Không có thông tin nào khác.",
+	"vcmi.adventureMap.playerAttacked": "Người chơi bị tấn công: %s",
+	"vcmi.adventureMap.moveCostDetails": "Điểm di chuyển - Cần: %TURNS lượt + %POINTS điểm, Còn lại: %REMAINING",
+	"vcmi.adventureMap.moveCostDetailsNoTurns": "Điểm di chuyển - Cần: %POINTS điểm, Còn lại: %REMAINING",
+	"vcmi.adventureMap.movementPointsHeroInfo": "(Điểm di chuyển: %REMAINING / %POINTS)",
+	"vcmi.adventureMap.replayOpponentTurnNotImplemented": "Xin lỗi, lượt chơi lại của đối thủ vẫn chưa được triển khai!",
+
+	"vcmi.bonusSource.artifact": "Báu vật",
+	"vcmi.bonusSource.creature": "Kỹ năng",
+	"vcmi.bonusSource.spell": "Phép thuật",
+	"vcmi.bonusSource.hero": "Tướng",
+	"vcmi.bonusSource.commander": "Chỉ huy",
+	"vcmi.bonusSource.other": "Khác",
+
+	"vcmi.capitalColors.0": "Đỏ",
+	"vcmi.capitalColors.1": "Xanh dương",
+	"vcmi.capitalColors.2": "Nâu",
+	"vcmi.capitalColors.3": "Xanh lá",
+	"vcmi.capitalColors.4": "Cam",
+	"vcmi.capitalColors.5": "Tím",
+	"vcmi.capitalColors.6": "Xanh đậm",
+	"vcmi.capitalColors.7": "Hồng",
+	
+	"vcmi.heroOverview.startingArmy": "Quân ban đầu",
+	"vcmi.heroOverview.warMachine": "Cỗ máy chiến đấu",
+	"vcmi.heroOverview.secondarySkills": "Kỹ năng phụ",
+	"vcmi.heroOverview.spells": "Phép thuật",
+	
+	"vcmi.quickExchange.moveUnit" : "Di chuyển đơn vị",
+	"vcmi.quickExchange.moveAllUnits" : "Di chuyển tất cả đơn vị",
+	"vcmi.quickExchange.swapAllUnits" : "Trao đổi quân",
+	"vcmi.quickExchange.moveAllArtifacts" : "Di chuyển tất cả báu vật",
+	"vcmi.quickExchange.swapAllArtifacts" : "Trao đổi báu vật",
+	
+	"vcmi.radialWheel.mergeSameUnit" : "Gộp quân cùng loại",
+	"vcmi.radialWheel.fillSingleUnit": "Làm đầy với từng loài",
+	"vcmi.radialWheel.splitSingleUnit": "Tách một quân cùng loài",
+	"vcmi.radialWheel.splitUnitEqually": "Chia quân bằng nhau",
+	"vcmi.radialWheel.moveUnit": "Di chuyển quân đến đội khác",
+	"vcmi.radialWheel.splitUnit": "Chia quân đến ô khác",
+	
+	"vcmi.radialWheel.heroGetArmy" : "Lấy quân từ tướng khác",
+	"vcmi.radialWheel.heroSwapArmy" : "Trao đổi quân với tướng khác",
+	"vcmi.radialWheel.heroExchange" : "Mở màn hình trao đổi tướng",
+	"vcmi.radialWheel.heroGetArtifacts" : "Lấy báu vật từ tướng khác",
+	"vcmi.radialWheel.heroSwapArtifacts" : "Trao đổi báu vật với tướng khác",
+	"vcmi.radialWheel.heroDismiss" : "Loại bỏ tướng",
+
+	"vcmi.radialWheel.moveTop" : "Di chuyển lên trên cùng",
+	"vcmi.radialWheel.moveUp" : "Di chuyển lên trên",
+	"vcmi.radialWheel.moveDown" : "Di chuyển xuống dưới",
+	"vcmi.radialWheel.moveBottom" : "Di chuyển xuống dưới cùng",
+	
+	"vcmi.randomMap.description" : "Bản đồ được tạo ngẫu nhiên.\nMẫu là %s, kích cỡ %dx%d, cấp %d, người chơi %d, máy %d, nước %s, quái vật %s, bản đồ VCMI",
+	"vcmi.randomMap.description.isHuman" : ", người chơi %s",
+	"vcmi.randomMap.description.townChoice" : ", %s town choice is %s",
+	"vcmi.randomMap.description.water.none" : "không",
+	"vcmi.randomMap.description.water.normal" : "bình thường",
+	"vcmi.randomMap.description.water.islands" : "đảo",
+	"vcmi.randomMap.description.monster.weak" : "yếu",
+	"vcmi.randomMap.description.monster.normal" : "bình thường",
+	"vcmi.randomMap.description.monster.strong" : "khỏe",
+
+	"vcmi.spellBook.search" : "tìm kiếm...",
+
+	"vcmi.spellResearch.canNotAfford" : "Bạn không đủ khả năng để thay thế {%SPELL1} với {%SPELL2}. Nhưng bạn có thể loại bỏ phép thuật này và nghiên cứu phép thuật khác.",
+	"vcmi.spellResearch.comeAgain" : "Hôm nay bạn đã nghiên cứu phép thuật. Hãy quay lại đây vào ngày mai.",
+	"vcmi.spellResearch.pay" : "Bạn có muốn thay thế phép {%SPELL1} bằng {%SPELL2} không? Hoặc loại bỏ phép thuật này và nghiên cứu phép thuật khác?",
+	"vcmi.spellResearch.research" : "Nghiên cứu phép thuật này",
+	"vcmi.spellResearch.skip" : "Loại bỏ phép thuật này",
+	"vcmi.spellResearch.abort" : "Hủy bỏ",
+	"vcmi.spellResearch.noMoreSpells" : "Không còn phép thuật để nghiên cứu.",
+
+	"vcmi.mainMenu.serverConnecting" : "Đang kết nối...",
+	"vcmi.mainMenu.serverAddressEnter" : "Nhập địa chỉ:",
+	"vcmi.mainMenu.serverConnectionFailed" : "Không thể kết nối",
+	"vcmi.mainMenu.serverClosing" : "Đang hủy kết nối...",
+	"vcmi.mainMenu.hostTCP" : "Chủ phòng TCP/IP",
+	"vcmi.mainMenu.joinTCP" : "Tham gia TCP/IP",
+
+	"vcmi.lobby.filepath" : "File path",
+	"vcmi.lobby.creationDate" : "Creation date",
+	"vcmi.lobby.scenarioName" : "Scenario name",
+	"vcmi.lobby.mapPreview" : "Map preview",
+	"vcmi.lobby.noPreview" : "no preview",
+	"vcmi.lobby.noUnderground" : "no underground",
+	"vcmi.lobby.sortDate" : "Sorts maps by change date",
+	"vcmi.lobby.backToLobby" : "Return to lobby",
+	"vcmi.lobby.author" : "Author",
+	"vcmi.lobby.handicap" : "Handicap",
+	"vcmi.lobby.handicap.resource" : "Gives players appropriate resources to start with in addition to the normal starting resources. Negative values are allowed, but are limited to 0 in total (the player never starts with negative resources).",
+	"vcmi.lobby.handicap.income" : "Changes the player's various incomes by the percentage. Is rounded up.",
+	"vcmi.lobby.handicap.growth" : "Changes the growth rate of creatures in the towns owned by the player. Is rounded up.",
+	"vcmi.lobby.deleteUnsupportedSave" : "{Unsupported saves found}\n\nVCMI has found %d saved games that are no longer supported, possibly due to differences in VCMI versions.\n\nDo you want to delete them?",
+	"vcmi.lobby.deleteSaveGameTitle" : "Select a Saved Game to delete",
+	"vcmi.lobby.deleteMapTitle" : "Select a Scenario to delete",
+	"vcmi.lobby.deleteFile" : "Do you want to delete following file?",
+	"vcmi.lobby.deleteFolder" : "Do you want to delete following folder?",
+	"vcmi.lobby.deleteMode" : "Switch to delete mode and back",
+
+	"vcmi.broadcast.failedLoadGame" : "Failed to load game",
+	"vcmi.broadcast.command" : "Use '!help' to list available commands",
+	"vcmi.broadcast.simturn.end" : "Simultaneous turns have ended",
+	"vcmi.broadcast.simturn.endBetween" : "Simultaneous turns between players %s and %s have ended",
+	"vcmi.broadcast.serverProblem" : "Server encountered a problem",
+	"vcmi.broadcast.gameTerminated" : "game was terminated",
+	"vcmi.broadcast.gameSavedAs" : "game saved as",
+	"vcmi.broadcast.noCheater" : "No cheaters registered!",
+	"vcmi.broadcast.playerCheater" : "Player %s is cheater!",
+	"vcmi.broadcast.statisticFile" : "Statistic files can be found in %s directory",
+	"vcmi.broadcast.help.commands" : "Available commands to host:",
+	"vcmi.broadcast.help.exit" : "'!exit' - immediately ends current game",
+	"vcmi.broadcast.help.kick" : "'!kick <player>' - kick specified player from the game",
+	"vcmi.broadcast.help.save" : "'!save <filename>' - save game under specified filename",
+	"vcmi.broadcast.help.statistic" : "'!statistic' - save game statistics as csv file",
+	"vcmi.broadcast.help.commandsAll" : "Available commands to all players:",
+	"vcmi.broadcast.help.help" : "'!help' - display this help",
+	"vcmi.broadcast.help.cheaters" : "'!cheaters' - list players that entered cheat command during game",
+	"vcmi.broadcast.help.vote" : "'!vote' - allows to change some game settings if all players vote for it",
+	"vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - allow simultaneous turns for specified number of days, or until contact",
+	"vcmi.broadcast.vote.force" : "'!vote simturns force X' - force simultaneous turns for specified number of days, blocking player contacts",
+	"vcmi.broadcast.vote.abort" : "'!vote simturns abort' - abort simultaneous turns once this turn ends",
+	"vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - prolong base timer for all players by specified number of seconds",
+	"vcmi.broadcast.vote.noActive" : "No active voting!",
+	"vcmi.broadcast.vote.yes" : "yes",
+	"vcmi.broadcast.vote.no" : "no",
+	"vcmi.broadcast.vote.notRecognized" : "Voting command not recognized!",
+	"vcmi.broadcast.vote.success.untilContacts" : "Voting successful. Simultaneous turns will run for %s more days, or until contact",
+	"vcmi.broadcast.vote.success.contactsBlocked" : "Voting successful. Simultaneous turns will run for %s more days. Contacts are blocked",
+	"vcmi.broadcast.vote.success.nextDay" : "Voting successful. Simultaneous turns will end on next day",
+	"vcmi.broadcast.vote.success.timer" : "Voting successful. Timer for all players has been prolonger for %s seconds",
+	"vcmi.broadcast.vote.aborted" : "Player voted against change. Voting aborted",
+	"vcmi.broadcast.vote.start.untilContacts" : "Started voting to allow simultaneous turns for %s more days",
+	"vcmi.broadcast.vote.start.contactsBlocked" : "Started voting to force simultaneous turns for %s more days",
+	"vcmi.broadcast.vote.start.nextDay" : "Started voting to end simultaneous turns starting from next day",
+	"vcmi.broadcast.vote.start.timer" : "Started voting to prolong timer for all players by %s seconds",
+	"vcmi.broadcast.vote.hint" : "Type '!vote yes' to agree to this change or '!vote no' to vote against it",
+		
+	"vcmi.lobby.login.title" : "VCMI Online Lobby",
+	"vcmi.lobby.login.username" : "Username:",
+	"vcmi.lobby.login.connecting" : "Connecting...",
+	"vcmi.lobby.login.error" : "Connection error: %s",
+	"vcmi.lobby.login.create" : "New Account",
+	"vcmi.lobby.login.login" : "Login",
+	"vcmi.lobby.login.as" : "Login as %s",
+	"vcmi.lobby.login.spectator" : "Spectator",
+	"vcmi.lobby.header.rooms" : "Game Rooms - %d",
+	"vcmi.lobby.header.channels" : "Chat Channels",
+	"vcmi.lobby.header.chat.global" : "Global Game Chat - %s", // %s -> language name
+	"vcmi.lobby.header.chat.match" : "Chat from previous game on %s", // %s -> game start date & time
+	"vcmi.lobby.header.chat.player" : "Private chat with %s", // %s -> nickname of another player
+	"vcmi.lobby.header.history" : "Your Previous Games",
+	"vcmi.lobby.header.players" : "Players Online - %d",
+	"vcmi.lobby.match.solo" : "Singleplayer Game",
+	"vcmi.lobby.match.duel" : "Game with %s", // %s -> nickname of another player
+	"vcmi.lobby.match.multi" : "%d players",
+	"vcmi.lobby.room.create" : "Create New Room",
+	"vcmi.lobby.room.players.limit" : "Players Limit",
+	"vcmi.lobby.room.description.public" : "Any player can join public room.",
+	"vcmi.lobby.room.description.private" : "Only invited players can join private room.",
+	"vcmi.lobby.room.description.new" : "To start the game, select a scenario or set up a random map.",
+	"vcmi.lobby.room.description.load" : "To start the game, use one of your saved games.",
+	"vcmi.lobby.room.description.limit" : "Up to %d players can enter your room, including you.",
+	"vcmi.lobby.invite.header" : "Invite Players",
+	"vcmi.lobby.invite.notification" : "Player has invited you to their game room. You can now join their private room.",
+	"vcmi.lobby.preview.title" : "Join Game Room",
+	"vcmi.lobby.preview.subtitle" : "Game on %s, hosted by %s", //TL Note: 1) name of map or RMG template 2) nickname of game host
+	"vcmi.lobby.preview.version" : "Game version:",
+	"vcmi.lobby.preview.players" : "Players:",
+	"vcmi.lobby.preview.mods" : "Used mods:",
+	"vcmi.lobby.preview.allowed" : "Join the game room?",
+	"vcmi.lobby.preview.error.header" : "Unable to join this room.",
+	"vcmi.lobby.preview.error.playing" : "You need to leave your current game first.",
+	"vcmi.lobby.preview.error.full" : "The room is already full.",
+	"vcmi.lobby.preview.error.busy" : "The room no longer accepts new players.",
+	"vcmi.lobby.preview.error.invite" : "You were not invited to this room.",
+	"vcmi.lobby.preview.error.mods" : "You are using different set of mods.",
+	"vcmi.lobby.preview.error.version" : "You are using different version of VCMI.",
+	"vcmi.lobby.room.new" : "New Game",
+	"vcmi.lobby.room.load" : "Load Game",
+	"vcmi.lobby.room.type" : "Room Type",
+	"vcmi.lobby.room.mode" : "Game Mode",
+	"vcmi.lobby.room.state.public" : "Public",
+	"vcmi.lobby.room.state.private" : "Private",
+	"vcmi.lobby.room.state.busy" : "In Game",
+	"vcmi.lobby.room.state.invited" : "Invited",
+	"vcmi.lobby.mod.state.compatible" : "Compatible",
+	"vcmi.lobby.mod.state.disabled" : "Must be enabled",
+	"vcmi.lobby.mod.state.version" : "Version mismatch",
+	"vcmi.lobby.mod.state.excessive" : "Must be disabled",
+	"vcmi.lobby.mod.state.missing" : "Not installed",
+	"vcmi.lobby.pvp.coin.hover" : "Coin",
+	"vcmi.lobby.pvp.coin.help" : "Flips a coin",
+	"vcmi.lobby.pvp.randomTown.hover" : "Random town",
+	"vcmi.lobby.pvp.randomTown.help" : "Write a random town in the chat",
+	"vcmi.lobby.pvp.randomTownVs.hover" : "Random town vs.",
+	"vcmi.lobby.pvp.randomTownVs.help" : "Write two random towns in the chat",
+	"vcmi.lobby.pvp.versus" : "vs.",
+
+	"vcmi.client.errors.invalidMap" : "{Bản đồ hoặc chiến dịch không hợp lệ}\n\nKhông thể bắt đầu trò chơi! Bản đồ hoặc chiến dịch đã chọn có thể không hợp lệ hoặc bị lỗi. Như sau:\n%s",
+	"vcmi.client.errors.missingCampaigns" : "{Thiếu tệp tin dữ liệu}\n\nKhông tìm thấy tệp tin dữ liệu của chiến dịch! Có thể bạn đang sử dụng các tệp tin dữ liệu Heroes 3 bị thiếu hoặc bị lỗi. Hãy thử cài đặt lại trò chơi.",
+	"vcmi.client.errors.modLoadingFailure" : "{Lỗi tải mod}\n\nĐã phát hiện ra lỗi nghiêm trọng khi tải mod! Trò chơi có thể không hoạt động chính xác hoặc bị văng! Hãy cập nhật hoặc tắt hóa các mod sau:\n\n",
+	"vcmi.server.errors.disconnected" : "{Mạng bị lỗi}\n\nĐã mất kết nối tới máy chủ trò chơi!",
+	"vcmi.server.errors.playerLeft" : "{Người chơi}\n\n%s đã ngắt kết nối khỏi trò chơi!", //%s -> player color
+	"vcmi.server.errors.existingProcess" : "Một chương trình máy chủ VCMI khác đang chạy. Hãy đóng nó trước khi bắt đầu một trò chơi mới.",
+	"vcmi.server.errors.modsToEnable"    : "{Các mod sau đây là bắt buộc}",
+	"vcmi.server.errors.modsToDisable"   : "{Bạn phải tắt các mod sau đây}",
+	"vcmi.server.errors.unknownEntity" : "Không tải được tệp tin đã lưu! Có lỗi chưa xác định trong tệp tin đã lưu '%s'! Tệp tin có thể không tương thích với phiên bản mod hiện đang cài đặt!",
+	"vcmi.server.errors.wrongIdentified"   : "Bạn được chỉ định là người chơi %s trong khi bạn muốn %s",
+	"vcmi.server.errors.notAllowed"   : "Bạn không được phép thực hiện hành động này!",
+	
+	"vcmi.dimensionDoor.seaToLandError" : "Không thể dùng phép Dimension Door để dịch chuyển từ dưới biển lên đất liền hoặc ngược lại.",
+
+	"vcmi.settingsMainWindow.generalTab.hover": "Chung",
+	"vcmi.settingsMainWindow.generalTab.help": "Chuyển sang bảng Chung, chứa các cài đặt liên quan đến phần chung trò chơi",
+	"vcmi.settingsMainWindow.battleTab.hover": "Chiến đấu",
+	"vcmi.settingsMainWindow.battleTab.help": "Chuyển sang bảng Chiến đấu, cho phép thiết lập hành vi trong trận đánh",
+	"vcmi.settingsMainWindow.adventureTab.hover": "Bản đồ",
+	"vcmi.settingsMainWindow.adventureTab.help": "Chuyển sang bảng Bản đồ (là nơi mà người chơi di chuyển tướng của họ)",
+
+	"vcmi.systemOptions.videoGroup" : "Thiết lập phim ảnh",
+	"vcmi.systemOptions.audioGroup" : "Thiết lập âm thanh",
+	"vcmi.systemOptions.otherGroup" : "Thiết lập khác", // unused right now
+	"vcmi.systemOptions.townsGroup" : "Thành phố",
+
+	"vcmi.statisticWindow.statistics" : "Thống Kê",
+	"vcmi.statisticWindow.tsvCopy" : "Dữ liệu vào bộ nhớ tạm",
+	"vcmi.statisticWindow.selectView" : "Chọn chế độ xem",
+	"vcmi.statisticWindow.value" : "Giá trị",
+	"vcmi.statisticWindow.title.overview" : "Tổng quan",
+	"vcmi.statisticWindow.title.resources" : "Tài nguyên",
+	"vcmi.statisticWindow.title.income" : "Thu nhập",
+	"vcmi.statisticWindow.title.numberOfHeroes" : "Số tướng",
+	"vcmi.statisticWindow.title.numberOfTowns" : "Số thành",
+	"vcmi.statisticWindow.title.numberOfArtifacts" : "Sô báu vật",
+	"vcmi.statisticWindow.title.numberOfDwellings" : "Số nhà quân",
+	"vcmi.statisticWindow.title.numberOfMines" : "Số lượng mỏ",
+	"vcmi.statisticWindow.title.armyStrength" : "Sức mạnh quân đội",
+	"vcmi.statisticWindow.title.experience" : "Kinh nghiệm",
+	"vcmi.statisticWindow.title.resourcesSpentArmy" : "Chí phí mua quân",
+	"vcmi.statisticWindow.title.resourcesSpentBuildings" : "Chi phí xây dựng",
+	"vcmi.statisticWindow.title.mapExplored" : "Tỉ lệ mở bản đồ",
+	"vcmi.statisticWindow.param.playerName" : "Tên người chơi",
+	"vcmi.statisticWindow.param.daysSurvived" : "Số ngày còn sống",
+	"vcmi.statisticWindow.param.maxHeroLevel" : "Cấp độ tướng tối đa",
+	"vcmi.statisticWindow.param.battleWinRatioHero" : "Tỉ lệ thắng (vs. tướng)",
+	"vcmi.statisticWindow.param.battleWinRatioNeutral" : "Tỉ lệ thắng (vs. trung lập)",
+	"vcmi.statisticWindow.param.battlesHero" : "Trận chiến (vs. tướng)",
+	"vcmi.statisticWindow.param.battlesNeutral" : "Trận chiến (vs. trung lập)",
+	"vcmi.statisticWindow.param.maxArmyStrength" : "Quân đội mạnh nhất",
+	"vcmi.statisticWindow.param.tradeVolume" : "Số lượng trao đổi",
+	"vcmi.statisticWindow.param.obeliskVisited" : "Cột tháp đã đến",
+	"vcmi.statisticWindow.icon.townCaptured" : "Chiếm thành",
+	"vcmi.statisticWindow.icon.strongestHeroDefeated" : "Tướng mạnh nhất của đối thủ bị đánh bại",
+	"vcmi.statisticWindow.icon.grailFound" : "Tìm thấy Grail",
+	"vcmi.statisticWindow.icon.defeated" : "Đã bị đánh bại",
+
+	"vcmi.systemOptions.fullscreenBorderless.hover": "Toàn màn hình (không viền)",
+	"vcmi.systemOptions.fullscreenBorderless.help": "{Toàn màn hình không viền}\n\nNếu chọn, VCMI sẽ chạy chế độ toàn màn hình không viền. Ở chế độ này, trò chơi sẽ luôn dùng độ phân giải của màn hình, bỏ qua độ phân giải đã chọn.",
+	"vcmi.systemOptions.fullscreenExclusive.hover": "Toàn màn hình (riêng biệt)",
+	"vcmi.systemOptions.fullscreenExclusive.help": "{Toàn màn hình}\n\nNếu chọn, VCMI sẽ chạy chế độ dành riêng cho toàn màn hình. Ở chế độ này, trò chơi sẽ chuyển độ phân giải của màn hình sang độ phân giải được chọn.",
+	"vcmi.systemOptions.resolutionButton.hover": "Độ phân giải: %wx%h",
+	"vcmi.systemOptions.resolutionButton.help": "{Chọn độ phân giải}\n\nĐổi độ phân giải trong trò chơi.",
+	"vcmi.systemOptions.resolutionMenu.hover": "Chọn độ phân giải",
+	"vcmi.systemOptions.resolutionMenu.help": "Đổi độ phân giải trong trò chơi.",
+	"vcmi.systemOptions.scalingButton.hover": "Phóng đại giao diện: %p%",
+	"vcmi.systemOptions.scalingButton.help": "{Phóng đại giao diện}\n\nĐổi độ phóng đại giao diện trong trò chơi.",
+	"vcmi.systemOptions.scalingMenu.hover": "Chọn độ phóng đại giao diện",
+	"vcmi.systemOptions.scalingMenu.help": "Đổi độ phóng đại giao diện trong trò chơi.",
+	"vcmi.systemOptions.longTouchButton.hover": "Khoảng thời gian chạm giữ: %d ms", // Ghi chú dịch: "ms" = "mili giây"
+	"vcmi.systemOptions.longTouchButton.help": "{Khoảng thời gian chạm giữ}\n\nKhi dùng màn hình cảm ứng, cửa sổ sẽ bật lên sau khi chạm màn hình trong 1 khoảng thời gian xác định, theo mili giây.",
+	"vcmi.systemOptions.longTouchMenu.hover": "Chọn khoảng thời gian chạm giữ",
+	"vcmi.systemOptions.longTouchMenu.help": "Đổi khoảng thời gian chạm giữ.",
+	"vcmi.systemOptions.longTouchMenu.entry": "%d mili giây",
+	"vcmi.systemOptions.framerateButton.hover": "Hiện FPS",
+	"vcmi.systemOptions.framerateButton.help": "{Hiện FPS}\n\nHiện khung hình mỗi giây ở góc cửa sổ trò chơi",
+	"vcmi.systemOptions.hapticFeedbackButton.hover": "Rung khi chạm",
+	"vcmi.systemOptions.hapticFeedbackButton.help": "{Rung khi chạm}\n\nBật/ tắt chế độ rung khi chạm.",
+	"vcmi.systemOptions.enableUiEnhancementsButton.hover"  : "Cải thiện giao diện",
+	"vcmi.systemOptions.enableUiEnhancementsButton.help"   : "{Cải thiện giao diện}\n\nThay đổi nhiều giao diện cho người chơi những trải nghiệm mới hơn. Chẳng hạn như nút ba lô, v.v. Tắt để có trải nghiệm cổ điển.",
+	"vcmi.systemOptions.enableLargeSpellbookButton.hover"  : "Sách phép rộng hơn",
+	"vcmi.systemOptions.enableLargeSpellbookButton.help"   : "{Sách phép rộng hơn}\n\nChứa được nhiều phép thuật hơn trên mỗi trang. Hoạt ảnh thay đổi trang của sách phép không hoạt động khi bật cài đặt này.",
+	"vcmi.systemOptions.audioMuteFocus.hover"  : "Tắt âm thanh",
+	"vcmi.systemOptions.audioMuteFocus.help"   : "{Tắt âm thanh}\n\nTắt tiếng khi cửa sổ không hoạt động. Ngoại trừ tin nhắn trong trò chơi và âm thanh lượt mới.",
+
+	"vcmi.adventureOptions.infoBarPick.hover": "Hiện thông báo ở bảng thông tin",
+	"vcmi.adventureOptions.infoBarPick.help": "{Hiện thông báo ở bảng thông tin}\n\nThông báo từ các điểm đến thăm sẽ hiện ở bảng thông tin, thay vì trong cửa sổ bật lên.",
+	"vcmi.adventureOptions.numericQuantities.hover": "Số lượng quân",
+	"vcmi.adventureOptions.numericQuantities.help": "{Số lượng quân}\n\nHiện số lượng quân của đối thủ dưới dạng số A-B.",
+	"vcmi.adventureOptions.forceMovementInfo.hover": "Luôn hiện chi phí di chuyển",
+	"vcmi.adventureOptions.forceMovementInfo.help": "{Luôn hiện chi phí di chuyển}\n\nLuôn hiện điểm di chuyển trong thanh trạng thái. (Thay vì chỉ xem khi nhấn giữ phím Alt)",
+	"vcmi.adventureOptions.showGrid.hover": "Hiện ô lưới",
+	"vcmi.adventureOptions.showGrid.help": "{Hiện ô lưới}\n\nHiện đường biên giữa các ô lưới trên bản đồ phiêu lưu.",
+	"vcmi.adventureOptions.borderScroll.hover": "Cuộn ở đường biên",
+	"vcmi.adventureOptions.borderScroll.help": "{Cuộn ở đường biên}\n\nCuộn bản đồ phiêu lưu ở đường biên. Nhấn giữ phím Ctrl để tạm dừng chức năng này.",
+	"vcmi.adventureOptions.infoBarCreatureManagement.hover": "Quản lí quân ở bảng thông tin",
+	"vcmi.adventureOptions.infoBarCreatureManagement.help": "{Quản lí quân ở bảng thông tin}\n\nCho phép sắp xếp quân ở bảng thông tin thay vì luân chuyển giữa các mục mặc định.",
+	"vcmi.adventureOptions.leftButtonDrag.hover": "Kéo chuột trái",
+	"vcmi.adventureOptions.leftButtonDrag.help": "{Kéo chuột trái}\n\nGiữ và kéo chuột trái khi di chuyển sẽ dịch chuyển bản đồ phiêu lưu.",
+	"vcmi.adventureOptions.rightButtonDrag.hover" : "Kéo chuột phải",
+	"vcmi.adventureOptions.rightButtonDrag.help" : "{Kéo chuột phải}\n\nGiữ và kéo chuột phải khi di chuyển sẽ dịch chuyển bản đồ phiêu lưu.",
+	"vcmi.adventureOptions.smoothDragging.hover" : "Kéo bản đồ dễ hơn",
+	"vcmi.adventureOptions.smoothDragging.help" : "{Kéo bản đồ dễ hơn}\n\nKhi kéo bản đồ có hiệu ứng mới sẽ mượt hơn.",
+	"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Bỏ đi hiệu ứng mờ dần",
+	"vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Bỏ đi hiệu ứng mờ dần}\n\nKhi được bật, sẽ lược bỏ các hiệu ứng mờ dần của đối tượng (như thu thập tài nguyên, lên tàu, v.v.). Đặc biệt hữu ích trong các trò chơi PvP. Để có tốc độ di chuyển tối đa, bỏ đi hiệu ứng sẽ hoạt động bất kể cài đặt này.",
+	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed1.help": "Đặt tốc độ cuộn bản đồ sang rất chậm.",
+	"vcmi.adventureOptions.mapScrollSpeed5.help": "Đặt tốc độ cuộn bản đồ sang rất nhanh.",
+	"vcmi.adventureOptions.mapScrollSpeed6.help": "Đặt tốc độ cuộn bản đồ sang tức thời.",
+	"vcmi.adventureOptions.hideBackground.hover" : "Ẩn ở chế độ nền",
+	"vcmi.adventureOptions.hideBackground.help" : "{Ẩn ở chế độ nền}\n\nẨn bản đồ phiêu lưu ở chế độ nền và thay vào đó là hiển thị họa tiết.",
+
+	"vcmi.battleOptions.queueSizeLabel.hover": "Hiện thứ tự lượt",
+	"vcmi.battleOptions.queueSizeNoneButton.hover": "Tắt",
+	"vcmi.battleOptions.queueSizeAutoButton.hover": "Tự động",
+	"vcmi.battleOptions.queueSizeSmallButton.hover": "Nhỏ",
+	"vcmi.battleOptions.queueSizeBigButton.hover": "Lớn",
+	"vcmi.battleOptions.queueSizeNoneButton.help": "Không hiện thứ tự lượt.",
+	"vcmi.battleOptions.queueSizeAutoButton.help": "Tự động điều chỉnh kích thước thứ tự lượt theo độ phân giải (Nhỏ được dùng khi chiều cao thấp hơn 700 px, ngược lại dùng Lớn).",
+	"vcmi.battleOptions.queueSizeSmallButton.help": "Đặt kích thước thứ tự lượt sang Nhỏ.",
+	"vcmi.battleOptions.queueSizeBigButton.help": "Đặt kích thước thứ tự lượt sang Lớn (không hỗ trợ nếu chiều cao nhỏ hơn 700 px).",
+	"vcmi.battleOptions.animationsSpeed1.hover": "",
+	"vcmi.battleOptions.animationsSpeed5.hover": "",
+	"vcmi.battleOptions.animationsSpeed6.hover": "",
+	"vcmi.battleOptions.animationsSpeed1.help": "Đặt tốc độ hình ảnh sang rất chậm.",
+	"vcmi.battleOptions.animationsSpeed5.help": "Đặt tốc độ hình ảnh sang rất nhanh.",
+	"vcmi.battleOptions.animationsSpeed6.help": "Đặt tốc độ hình ảnh sang tức thời.",
+	"vcmi.battleOptions.movementHighlightOnHover.hover": "Hiện di chuyển khi di chuột",
+	"vcmi.battleOptions.movementHighlightOnHover.help": "{Hiện di chuyển khi di chuột}\n\nHiện giới hạn di chuyển của quân khi di chuột lên chúng.",
+	"vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Hiện tầm bắn của cung thủ",
+	"vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Hiện tầm bắn của cung thủ khi di chuột}\n\nHiện tầm bắn của cung thủ khi di chuột lên chúng.",
+	"vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Hiện thông số tướng",
+	"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Hiện thông số tướng}\n\nBật/ tắt bảng chỉ số cơ bản và năng lượng của tướng.",
+	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Bỏ qua nhạc dạo đầu",
+	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Bỏ qua nhạc dạo đầu}\n\nKhông cần chờ hết nhạc khởi đầu mỗi trận đánh",
+	"vcmi.battleOptions.endWithAutocombat.hover": "Kết thúc trận chiến",
+	"vcmi.battleOptions.endWithAutocombat.help": "{Kết thúc trận chiến}\n\nTự động chiến đấu để kết thúc ngay lập tức",
+	"vcmi.battleOptions.showQuickSpell.hover": "Hiện bảng phép",
+	"vcmi.battleOptions.showQuickSpell.help": "{Hiện bảng phép}\n\nHiển thị bảng phép để chọn nhanh các phép thuật",
+
+	"vcmi.adventureMap.revisitObject.hover" : "Xem lại đối tượng",
+	"vcmi.adventureMap.revisitObject.help" : "{Xem lại đối tượng}\n\nNếu tướng đang đứng ở một đối tượng trên bản đồ, tướng có thể quay trở lại vị trí đó.",
+
+	"vcmi.battleWindow.pressKeyToSkipIntro" : "Bấm phím bất kì để bắt đầu trận đánh",
+	"vcmi.battleWindow.damageEstimation.melee": "Tấn công %CREATURE (%DAMAGE).",
+	"vcmi.battleWindow.damageEstimation.meleeKills": "Tấn công %CREATURE (%DAMAGE, %KILLS).",
+	"vcmi.battleWindow.damageEstimation.ranged": "Bắn %CREATURE (%SHOTS, %DAMAGE).",
+	"vcmi.battleWindow.damageEstimation.rangedKills": "Bắn %CREATURE (%SHOTS, %DAMAGE, %KILLS).",
+	"vcmi.battleWindow.damageEstimation.shots": "Còn %d lần",
+	"vcmi.battleWindow.damageEstimation.shots.1": "Còn %d lần",
+	"vcmi.battleWindow.damageEstimation.damage": "%d sát thương",
+	"vcmi.battleWindow.damageEstimation.damage.1": "%d sát thương",
+	"vcmi.battleWindow.damageEstimation.kills": "%d sẽ bị diệt",
+	"vcmi.battleWindow.damageEstimation.kills.1": "%d sẽ bị diệt",
+	
+	"vcmi.battleWindow.damageRetaliation.will": "Sẽ phản đòn ",
+	"vcmi.battleWindow.damageRetaliation.may": "Có thể phản đòn ",
+	"vcmi.battleWindow.damageRetaliation.never": "Không phản đòn.",
+	"vcmi.battleWindow.damageRetaliation.damage": "(%DAMAGE).",
+	"vcmi.battleWindow.damageRetaliation.damageKills": "(%DAMAGE, %KILLS).",
+	
+	"vcmi.battleWindow.killed": "Đã bị giết",
+	"vcmi.battleWindow.accurateShot.resultDescription.0": "%d %s đã bị giết bằng những phát bắn chính xác!",
+	"vcmi.battleWindow.accurateShot.resultDescription.1": "%d %s đã bị giết bằng một phát bắn chính xác!",
+	"vcmi.battleWindow.accurateShot.resultDescription.2": "%d %s đã bị giết bằng những phát bắn chính xác!",
+	"vcmi.battleWindow.endWithAutocombat": "Bạn có muốn kết thúc trận đấu bằng chế độ đánh tự động không?",
+
+	"vcmi.battleResultsWindow.applyResultsLabel" : "Chấp nhận kết quả trận đấu?",
+
+	"vcmi.tutorialWindow.title" : "Màn Hình Cảm Ứng",
+	"vcmi.tutorialWindow.decription.RightClick" : "Chạm và giữ phần tử mà bạn muốn bấm chuột phải. Chạm vào vùng trống để đóng.",
+	"vcmi.tutorialWindow.decription.MapPanning" : "Chạm và kéo bằng một ngón tay để di chuyển bản đồ.",
+	"vcmi.tutorialWindow.decription.MapZooming" : "Chụm hai ngón tay để thay đổi mức độ phóng to hoặc thu nhỏ bản đồ.",
+	"vcmi.tutorialWindow.decription.RadialWheel" : "Vuốt để mở thêm nhiều hành động khác nhau, chẳng hạn như quản lý quân/tướng và sắp xếp các thành.",
+	"vcmi.tutorialWindow.decription.BattleDirection" : "Để tấn công từ một hướng cụ thể, hãy vuốt theo hướng mà đòn tấn công sẽ đánh.",
+	"vcmi.tutorialWindow.decription.BattleDirectionAbort" : "Chỉ hướng tấn công có thể bị hủy nếu ngón tay ở quá xa.",
+	"vcmi.tutorialWindow.decription.AbortSpell" : "Chạm và giữ để hủy phép thuật.",
+
+	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover": "Hiện quân được mua",
+	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help": "{Hiện quân được mua}\n\nHiện quân được mua thay vì sinh trưởng trong sơ lược thành (góc trái dưới màn hình thành phố).",
+	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover": "Hiện sinh trưởng quân",
+	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.help": "{Hiện sinh trưởng quân}\n\nHiện sinh trưởng quân hàng tuần thay vì số lượng có sẵn trong sơ lược thành (góc trái dưới màn hình thành phố).",
+	"vcmi.otherOptions.compactTownCreatureInfo.hover": "Thu gọn thông tin quân",
+	"vcmi.otherOptions.compactTownCreatureInfo.help": "{Thu gọn thông tin quân}\n\nHiện thông tin quân nhỏ hơn trong sơ lược thành (góc trái dưới màn hình thành phố).",
+
+	"vcmi.townHall.missingBase"             : "Bạn phải xây %s trước đã",
+	"vcmi.townHall.noCreaturesToRecruit"    : "Không có quân để chiêu mộ!",
+
+	"vcmi.townStructure.bank.borrow" : "Nhìn thấy bạn vào nhà băng, một nhân viên đến gần bạn và nói: \"Chúng tôi có thể cho bạn vay 2500 vàng trong 5 ngày. Bạn phải trả lại cho chúng tôi 500 vàng mỗi ngày.\"",
+	"vcmi.townStructure.bank.payBack" : "Nhìn thấy bạn vào nhà băng, một nhân viên đến gần bạn và nói: \"Bạn đã nhận được khoản vay của mình. Hãy trả lại cho chúng tôi trước khi vay khoản mới.\"",
+
+	"vcmi.logicalExpressions.anyOf"  : "Bất kì cái nào sau đây:",
+	"vcmi.logicalExpressions.allOf"  : "Tất cả những cái sau:",
+	"vcmi.logicalExpressions.noneOf" : "Không có những cái sau:",
+
+	"vcmi.heroWindow.openCommander.hover" : "Mở cửa sổ thông tin chỉ huy",
+	"vcmi.heroWindow.openCommander.help"  : "Hiện thông tin chi tiết về chỉ huy của tướng này.",
+	"vcmi.heroWindow.openBackpack.hover" : "Mở cửa sổ ba lô báu vật",
+	"vcmi.heroWindow.openBackpack.help"  : "Mở cửa sổ để quản lý ba lô báu vật dễ dàng hơn.",
+	"vcmi.heroWindow.sortBackpackByCost.hover"  : "Sắp xếp theo giá",
+	"vcmi.heroWindow.sortBackpackByCost.help"  : "Sắp xếp các báu vật trong ba lô theo giá.",
+	"vcmi.heroWindow.sortBackpackBySlot.hover"  : "Sắp xếp theo vị trí",
+	"vcmi.heroWindow.sortBackpackBySlot.help"  : "Sắp xếp báu vật trong ba lô theo ô được trang bị.",
+	"vcmi.heroWindow.sortBackpackByClass.hover"  : "Sắp xếp theo loại",
+	"vcmi.heroWindow.sortBackpackByClass.help"  : "Sắp xếp các báu vật trong ba lô theo loại: Chính, Phụ, Cổ đại và Quý hiếm.",
+	"vcmi.heroWindow.fusingArtifact.fusing" : "Bạn đã sở hữu tất cả các món đồ cần thiết để hợp nhất %s. Bạn có muốn hợp nhất không? {Tất cả các món đồ sẽ được sử dụng khi hợp nhất.}",
+
+	"vcmi.tavernWindow.inviteHero"  : "Mới thêm tướng",
+
+	"vcmi.commanderWindow.artifactMessage" : "Bạn có muốn đưa báu vật này cho tướng không?",
+
+	"vcmi.creatureWindow.showBonuses.hover"    : "Chuyển sang chế độ xem phần thưởng",
+	"vcmi.creatureWindow.showBonuses.help"     : "Hiển thị tất cả các phần thưởng của chỉ huy.",
+	"vcmi.creatureWindow.showSkills.hover"     : "Chuyển sang chế độ xem kỹ năng",
+	"vcmi.creatureWindow.showSkills.help"      : "Hiển thị tất cả các kỹ năng đã học của chỉ huy.",
+	"vcmi.creatureWindow.returnArtifact.hover" : "Trả lại báu vật",
+	"vcmi.creatureWindow.returnArtifact.help"  : "Bấm vào nút này để trả lại báu vật về ba lô của tướng.",
+
+	"vcmi.questLog.hideComplete.hover" : "Ẩn nhiệm vụ đã hoàn thành",
+	"vcmi.questLog.hideComplete.help"  : "Ẩn tất cả các nhiệm vụ đã hoàn thành.",
+
+	"vcmi.randomMapTab.widgets.randomTemplate"      : "(Random)",
+	"vcmi.randomMapTab.widgets.templateLabel"        : "Mẫu có sẵn",
+	"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Cài đặt...",
+	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Chọn đội",
+	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Loại đường",
+
+	"vcmi.optionsTab.turnOptions.hover" : "Tùy chọn lượt",
+	"vcmi.optionsTab.turnOptions.help" : "Cài đặt tùy chọn hẹn giờ và lượt đi cùng lúc",
+
+	"vcmi.optionsTab.chessFieldBase.hover" : "Thời gian thêm",
+	"vcmi.optionsTab.chessFieldTurn.hover" : "Thời gian lượt",
+	"vcmi.optionsTab.chessFieldBattle.hover" : "Thời gian trận đấu",
+	"vcmi.optionsTab.chessFieldUnit.hover" : "Thời gian quân",
+	"vcmi.optionsTab.chessFieldBase.help" : "Dùng khi {Thời gian lượt} giảm về 0. Được đặt 1 lần khi bắt đầu trò chơi. Khi thời gian này giảm về 0 thì lượt của người chơi sẽ kết thúc.",
+	"vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Dùng trên bản đồ hoặc khi {Thời gian thêm} hết. Được đặt lại ở mỗi lượt. Còn thời gian cộng vào {Thời gian thêm} khi kết thúc lượt.",
+	"vcmi.optionsTab.chessFieldTurnDiscard.help" : "Dùng trên bản đồ hoặc khi {Thời gian chiến đấu} hết. Được đặt lại ở mỗi lượt. Thời gian chưa sử dụng sẽ bị mất.",
+	"vcmi.optionsTab.chessFieldBattle.help" : "Dùng trong trận chiến với AI hoặc trong trận đấu pvp khi {Thời gian quân} hết. Đặt lại sau mỗi trận.",
+	"vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Dùng khi chọn hành động cho quân trong trận đấu PvP. Còn thời gian cộng vào {Thời gian trận đấu} khi kết thúc lượt của quân.",
+	"vcmi.optionsTab.chessFieldUnitDiscard.help" : "Dùng khi chọn hành động cho quân trong trận đấu PvP. Đặt lại khi bắt đầu lượt của mỗi đơn vị. Thời gian chưa sử dụng sẽ bị mất.",
+
+	"vcmi.optionsTab.accumulate" : "Tích lũy",
+
+	"vcmi.optionsTab.simturnsTitle" : "Lượt đi cùng lúc",
+	"vcmi.optionsTab.simturnsMin.hover" : "Tối thiểu là",
+	"vcmi.optionsTab.simturnsMax.hover" : "Tối đa là",
+	"vcmi.optionsTab.simturnsAI.hover" : "(Thử nghiệm) Lượt chơi AI cùng lúc",
+	"vcmi.optionsTab.simturnsMin.help" : "Chơi cùng lượt trong số ngày được chỉ định. Cấm các hành vi thù địch giữa những người chơi trong thời gian này.",
+	"vcmi.optionsTab.simturnsMax.help" : "Chơi cùng lượt trong số ngày được chỉ định hoặc cho đến khi thực hiện hành vi thù địch với người chơi khác.",
+	"vcmi.optionsTab.simturnsAI.help" : "{Lượt đi của AI cùng lúc}\nTùy chọn Thử nghiệm. AI sẽ thực hiện các hành động cùng lúc với người chơi khi bật chế độ lượt đi cùng lúc.",
+
+	"vcmi.optionsTab.turnTime.select"     : "Chọn cài đặt giờ cho lượt",
+	"vcmi.optionsTab.turnTime.unlimited"  : "Không giới hạn thời gian",
+	"vcmi.optionsTab.turnTime.classic.1"  : "Thời gian: 1 phút",
+	"vcmi.optionsTab.turnTime.classic.2"  : "Thời gian: 2 phút",
+	"vcmi.optionsTab.turnTime.classic.5"  : "Thời gian: 5 phút",
+	"vcmi.optionsTab.turnTime.classic.10" : "Thời gian: 10 phút",
+	"vcmi.optionsTab.turnTime.classic.20" : "Thời gian: 20 phút",
+	"vcmi.optionsTab.turnTime.classic.30" : "Thời gian: 30 phút",
+	"vcmi.optionsTab.turnTime.chess.20"   : "Time: 20:00 + 10:00 + 02:00 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.16"   : "Time: 16:00 + 08:00 + 01:30 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.8"    : "Time: 08:00 + 04:00 + 01:00 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.4"    : "Time: 04:00 + 02:00 + 00:30 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.2"    : "Time: 02:00 + 01:00 + 00:15 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.1"    : "Time: 01:00 + 01:00 + 00:00 + 00:00",
+
+	"vcmi.optionsTab.simturns.select"         : "Cài đặt giờ cho lượt đi cùng lúc",
+	"vcmi.optionsTab.simturns.none"           : "Không có lượt đi cùng lúc",
+	"vcmi.optionsTab.simturns.tillContactMax" : "Turns: Cho đến khi xâm chiếm",
+	"vcmi.optionsTab.simturns.tillContact1"   : "Turns: 1 tuần, dừng khi xâm chiếm",
+	"vcmi.optionsTab.simturns.tillContact2"   : "Turns: 2 tuần, dừng khi xâm chiếm",
+	"vcmi.optionsTab.simturns.tillContact4"   : "Turns: 1 tháng, dừng khi xâm chiếm",
+	"vcmi.optionsTab.simturns.blocked1"       : "Turns: 1 tuần, cấm xâm chiếm",
+	"vcmi.optionsTab.simturns.blocked2"       : "Turns: 2 tuần, cấm xâm chiếm",
+	"vcmi.optionsTab.simturns.blocked4"       : "Turns: 1 tháng, cấm xâm chiếm",
+	
+	// Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language
+	// Using this information, VCMI will automatically select correct plural form for every possible amount
+	"vcmi.optionsTab.simturns.days.0" : " %d ngày",
+	"vcmi.optionsTab.simturns.days.1" : " %d ngày",
+	"vcmi.optionsTab.simturns.days.2" : " %d ngày",
+	"vcmi.optionsTab.simturns.weeks.0" : " %d tuần",
+	"vcmi.optionsTab.simturns.weeks.1" : " %d tuần",
+	"vcmi.optionsTab.simturns.weeks.2" : " %d tuần",
+	"vcmi.optionsTab.simturns.months.0" : " %d tháng",
+	"vcmi.optionsTab.simturns.months.1" : " %d tháng",
+	"vcmi.optionsTab.simturns.months.2" : " %d tháng",
+
+	"vcmi.optionsTab.extraOptions.hover" : "Tùy chọn mở rộng",
+	"vcmi.optionsTab.extraOptions.help" : "Mở thêm cài đặt bổ sung trong trò chơi",
+
+	"vcmi.optionsTab.cheatAllowed.hover" : "Cho phép sử dụng mã gian lận",
+	"vcmi.optionsTab.unlimitedReplay.hover" : "Xem lại trận đấu không giới hạn",
+	"vcmi.optionsTab.cheatAllowed.help" : "{Dùng mã gian lận}\nCho phép nhập các mã gian lận trong trò chơi.",
+	"vcmi.optionsTab.unlimitedReplay.help" : "{Xem lại trận đấu}\nKhông giới hạn số lần xem lại trận đấu.",
+
+	// Custom victory conditions for H3 campaigns and HotA maps
+	"vcmi.map.victoryCondition.daysPassed.toOthers" : "Đối thủ đã xoay xở để sinh tồn đến ngày hôm này. Họ giành chiến thắng!",
+	"vcmi.map.victoryCondition.daysPassed.toSelf" : "Chúc mừng! Bạn đã vượt khó để sinh tồn. Chiến thắng thuộc về bạn!",
+	"vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "Đối thủ đã tiêu diệt tất cả quái vật gây hại cho vùng đất này và họ giành chiến thắng!",
+	"vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Chúc mừng! Bạn đã đánh bại tất cả quái vật gây hại cho vùng đất này và giành chiến thắng!",
+	"vcmi.map.victoryCondition.collectArtifacts.message" : "Thu thập ba báu vật",
+	"vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Chúc mừng bạn có đủ ba báu vật! Tất cả đối thủ đã bị đánh bại và bạn có Angelic Alliance! Chiến thắng thuộc về bạn!",
+	"vcmi.map.victoryCondition.angelicAlliance.message" : "Đánh bại tất cả đối thủ và hớp nhất Angelic Alliance",
+	"vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf" : "Tiêu rồi, bạn đã để mất một phần của Angelic Alliance! Mọi thứ đã kết thúc!",
+
+	// few strings from WoG used by vcmi
+	"vcmi.stackExperience.description" : "» Kinh Nghiệm Của Đạo Quân «\n\nTên loại quân ................... : %s\nXếp hạng kinh nghiệm ................. : %s (%i)\nĐiểm kinh nghiệm ............... : %i\nĐiểm kinh nghiệm để lên hạng .. : %i\nKinh nghiệm tối đa mỗi trận đánh ... : %i%% (%i)\nSố quân hiện tại của đạo .... : %i\nChiêu mộ tối đa mới\n không bị giảm cấp .... : %i\nHệ số kinh nghiệm ........... : %.2f\nHệ số nâng cấp .............. : %.2f\nKinh nghiệm sau hạng 10 ........ : %i\nChiêu mộ mới tối đa còn lại ở hạng 10: %i",
+	"vcmi.stackExperience.rank.0" : "Lính Mới",
+	"vcmi.stackExperience.rank.1" : "Tập Sự",
+	"vcmi.stackExperience.rank.2" : "Lành Nghề",
+	"vcmi.stackExperience.rank.3" : "Khéo Léo",
+	"vcmi.stackExperience.rank.4" : "Thông Thạo",
+	"vcmi.stackExperience.rank.5" : "Kì Cựu",
+	"vcmi.stackExperience.rank.6" : "Lão Luyện",
+	"vcmi.stackExperience.rank.7" : "Chuyên Gia",
+	"vcmi.stackExperience.rank.8" : "Tinh Hoa",
+	"vcmi.stackExperience.rank.9" : "Bậc Thầy",
+	"vcmi.stackExperience.rank.10" : "Thiên Tài",
+	
+	// Strings for HotA Seer Hut / Quest Guards
+	"core.seerhut.quest.heroClass.complete.0" : "Bạn đúng là %s. Đây là món quà tôi dành cho bạn. Bạn có chấp nhận không?",
+	"core.seerhut.quest.heroClass.complete.1" : "Bạn đúng là %s. Đây là món quà tôi dành cho bạn. Bạn có chấp nhận không?",
+	"core.seerhut.quest.heroClass.complete.2" : "Bạn đúng là %s. Đây là món quà tôi dành cho bạn. Bạn có chấp nhận không?",
+	"core.seerhut.quest.heroClass.complete.3" : "Những lính gác nhận ra bạn là %s. Bạn có muốn đi qua ngay bây giờ không?",
+	"core.seerhut.quest.heroClass.complete.4" : "Những lính gác nhận ra bạn là %s. Bạn có muốn đi qua ngay bây giờ không?",
+	"core.seerhut.quest.heroClass.complete.5" : "Những lính gác nhận ra bạn là %s. Bạn có muốn đi qua ngay bây giờ không?",
+	"core.seerhut.quest.heroClass.description.0" : "Tìm %s cho %s",
+	"core.seerhut.quest.heroClass.description.1" : "Tìm %s cho %s",
+	"core.seerhut.quest.heroClass.description.2" : "Tìm %s cho %s",
+	"core.seerhut.quest.heroClass.description.3" : "Tìm %s để mở cổng",
+	"core.seerhut.quest.heroClass.description.4" : "Tìm %s để mở cổng",
+	"core.seerhut.quest.heroClass.description.5" : "Tìm %s để mở cổng",
+	"core.seerhut.quest.heroClass.hover.0" : "(tìm tướng thuộc nhóm %s)",
+	"core.seerhut.quest.heroClass.hover.1" : "(tìm tướng thuộc nhóm %s)",
+	"core.seerhut.quest.heroClass.hover.2" : "(tìm tướng thuộc nhóm %s)",
+	"core.seerhut.quest.heroClass.hover.3" : "(tìm tướng thuộc nhóm %s)",
+	"core.seerhut.quest.heroClass.hover.4" : "(tìm tướng thuộc nhóm %s)",
+	"core.seerhut.quest.heroClass.hover.5" : "(tìm tướng thuộc nhóm %s)",
+	"core.seerhut.quest.heroClass.receive.0" : "Tôi có một món quà cho %s.",
+	"core.seerhut.quest.heroClass.receive.1" : "Tôi có một món quà cho %s.",
+	"core.seerhut.quest.heroClass.receive.2" : "Tôi có một món quà cho %s.",
+	"core.seerhut.quest.heroClass.receive.3" : "Những người lính gác ở đây nói rằng họ chỉ cho %s đi qua.",
+	"core.seerhut.quest.heroClass.receive.4" : "Những người lính gác ở đây nói rằng họ chỉ cho %s đi qua.",
+	"core.seerhut.quest.heroClass.receive.5" : "Những người lính gác ở đây nói rằng họ chỉ cho %s đi qua.",
+	"core.seerhut.quest.heroClass.visit.0" : "Bạn không phải là %s. Tôi không có gì cho bạn. Cút đi!",
+	"core.seerhut.quest.heroClass.visit.1" : "Bạn không phải là %s. Tôi không có gì cho bạn. Cút đi!",
+	"core.seerhut.quest.heroClass.visit.2" : "Bạn không phải là %s. Tôi không có gì cho bạn. Cút đi!",
+	"core.seerhut.quest.heroClass.visit.3" : "Những người lính gác ở đây chỉ cho %s đi qua.",
+	"core.seerhut.quest.heroClass.visit.4" : "Những người lính gác ở đây chỉ cho %s đi qua.",
+	"core.seerhut.quest.heroClass.visit.5" : "Những người lính gác ở đây chỉ cho %s đi qua.",
+	
+	"core.seerhut.quest.reachDate.complete.0" : "Bây giờ tôi đang rảnh. Đây là thứ tôi có cho bạn. Bạn có muốn nhận nó không?",
+	"core.seerhut.quest.reachDate.complete.1" : "Bây giờ tôi đang rảnh. Đây là thứ tôi có cho bạn. Bạn có muốn nhận nó không?",
+	"core.seerhut.quest.reachDate.complete.2" : "Bây giờ tôi đang rảnh. Đây là thứ tôi có cho bạn. Bạn có muốn nhận nó không?",
+	"core.seerhut.quest.reachDate.complete.3" : "Bạn được phép đi qua ngay bây giờ. Bạn có muốn đi qua không?",
+	"core.seerhut.quest.reachDate.complete.4" : "Bạn được phép đi qua ngay bây giờ. Bạn có muốn đi qua không?",
+	"core.seerhut.quest.reachDate.complete.5" : "Bạn được phép đi qua ngay bây giờ. Bạn có muốn đi qua không?",
+	"core.seerhut.quest.reachDate.description.0" : "Đợi đến %s %s",
+	"core.seerhut.quest.reachDate.description.1" : "Đợi đến %s %s",
+	"core.seerhut.quest.reachDate.description.2" : "Đợi đến %s %s",
+	"core.seerhut.quest.reachDate.description.3" : "Đợi đến %s để mở cổng",
+	"core.seerhut.quest.reachDate.description.4" : "Đợi đến %s để mở cổng",
+	"core.seerhut.quest.reachDate.description.5" : "Đợi đến %s để mở cổng",
+	"core.seerhut.quest.reachDate.hover.0" : "(Hãy quay lại đây vào %s)",
+	"core.seerhut.quest.reachDate.hover.1" : "(Hãy quay lại đây vào %s)",
+	"core.seerhut.quest.reachDate.hover.2" : "(Hãy quay lại đây vào %s)",
+	"core.seerhut.quest.reachDate.hover.3" : "(Hãy quay lại đây vào %s)",
+	"core.seerhut.quest.reachDate.hover.4" : "(Hãy quay lại đây vào %s)",
+	"core.seerhut.quest.reachDate.hover.5" : "(Hãy quay lại đây vào %s)",
+	"core.seerhut.quest.reachDate.receive.0" : "Tôi đang bận. Hãy quay lại đây vào %s",
+	"core.seerhut.quest.reachDate.receive.1" : "Tôi đang bận. Hãy quay lại đây vào %s",
+	"core.seerhut.quest.reachDate.receive.2" : "Tôi đang bận. Hãy quay lại đây vào %s",
+	"core.seerhut.quest.reachDate.receive.3" : "Đóng cổng đến %s.",
+	"core.seerhut.quest.reachDate.receive.4" : "Đóng cổng đến %s.",
+	"core.seerhut.quest.reachDate.receive.5" : "Đóng cổng đến %s.",
+	"core.seerhut.quest.reachDate.visit.0" : "Tôi đang bận. Hãy quay lại đây vào %s.",
+	"core.seerhut.quest.reachDate.visit.1" : "Tôi đang bận. Hãy quay lại đây vào %s.",
+	"core.seerhut.quest.reachDate.visit.2" : "Tôi đang bận. Hãy quay lại đây vào %s.",
+	"core.seerhut.quest.reachDate.visit.3" : "Đóng cổng đến %s.",
+	"core.seerhut.quest.reachDate.visit.4" : "Đóng cổng đến %s.",
+	"core.seerhut.quest.reachDate.visit.5" : "Đóng cổng đến %s.",
+	
+	"mapObject.core.hillFort.object.description" : "Nâng cấp quân cấp 1 - 4 với chi phí thấp hơn ở trong thành.",
+	
+	"core.bonus.ADDITIONAL_ATTACK.name": "Đánh 2 lần",
+	"core.bonus.ADDITIONAL_ATTACK.description": "Tấn công hai lần",
+	"core.bonus.ADDITIONAL_RETALIATION.name": "Phản đòn thêm",
+	"core.bonus.ADDITIONAL_RETALIATION.description": "Có thể phàn đòn thêm ${val} lần",
+	"core.bonus.AIR_IMMUNITY.name": "Kháng gió",
+	"core.bonus.AIR_IMMUNITY.description": "Kháng tất cả phép gió trong trường học phép thuật",
+	"core.bonus.ATTACKS_ALL_ADJACENT.name": "Đánh xung quanh",
+	"core.bonus.ATTACKS_ALL_ADJACENT.description": "Tấn công tất cả kẻ địch xung quanh",
+	"core.bonus.BLOCKS_RETALIATION.name": "Không bị phản đòn",
+	"core.bonus.BLOCKS_RETALIATION.description": "Kẻ địch không thể phản đòn",
+	"core.bonus.BLOCKS_RANGED_RETALIATION.name": "Bắn không phản",
+	"core.bonus.BLOCKS_RANGED_RETALIATION.description": "Kẻ địch không thể bắn phản đòn",
+	"core.bonus.CATAPULT.name": "Công thành",
+	"core.bonus.CATAPULT.description": "Tấn công tường thành",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Giảm (${val}) năng lượng",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Giảm ${val} năng lượng khi tướng dùng phép",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Tốn thêm (${val}) năng lượng",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Tướng địch dùng phép tốn thêm ${val} năng lượng",
+	"core.bonus.CHARGE_IMMUNITY.name": "Bộ binh",
+	"core.bonus.CHARGE_IMMUNITY.description": "Kháng lại kỹ năng đặc biệt của Cavalier và Champion",
+	"core.bonus.DARKNESS.name": "Bóng tối che phủ",
+	"core.bonus.DARKNESS.description": "Tạo ra bóng tối với bán kính ${val}",
+	"core.bonus.DEATH_STARE.name": "Ánh mắt tử thần (${val}%)",
+	"core.bonus.DEATH_STARE.description": "Có ${val}% cơ hội giết chết kẻ địch",
+	"core.bonus.DEFENSIVE_STANCE.name": "Thưởng phòng thủ",
+	"core.bonus.DEFENSIVE_STANCE.description": "+${val} Phòng thủ khi khi đang thế thủ",
+	"core.bonus.DESTRUCTION.name": "Đòn tận diệt",
+	"core.bonus.DESTRUCTION.description": "Có ${val}% cơ hội giết thêm kẻ địch sau khi đánh",
+	"core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Đòn chí mạng",
+	"core.bonus.DOUBLE_DAMAGE_CHANCE.description": "Có ${val}% cơ hội gây sát thương gấp đôi khi đánh",
+	"core.bonus.DRAGON_NATURE.name": "Rồng",
+	"core.bonus.DRAGON_NATURE.description": "Quân có thuộc tính Rồng",
+	"core.bonus.EARTH_IMMUNITY.name": "Kháng đất",
+	"core.bonus.EARTH_IMMUNITY.description": "Kháng tất cả phép đất trong trường học phép thuật",
+	"core.bonus.ENCHANTER.name": "Niệm phép",
+	"core.bonus.ENCHANTER.description": "Có thể dùng phép mass ${subtype.spell} mỗi lượt",
+	"core.bonus.ENCHANTED.name": "Niệm phép",
+	"core.bonus.ENCHANTED.description": "Bị ảnh hưởng vĩnh viễn bởi phép ${subtype.spell}",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.name": "Giảm tấn công (${val}%)",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.description": "Khi bị tấn công, giảm ${val}% tấn công của kẻ địch",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Giảm phòng thủ (${val}%)",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Khi tấn công, giảm ${val}% phòng thủ của kẻ địch",
+	"core.bonus.FIRE_IMMUNITY.name": "Kháng lửa",
+	"core.bonus.FIRE_IMMUNITY.description": "Kháng tất cả phép lửa trong trường học phép thuật",
+	"core.bonus.FIRE_SHIELD.name": "Khiên lửa (${val}%)",
+	"core.bonus.FIRE_SHIELD.description": "Phản lại một phần sát thương khi cận chiến",
+	"core.bonus.FIRST_STRIKE.name": "Đòn đánh phủ đầu",
+	"core.bonus.FIRST_STRIKE.description": "Đạo quân này phản đòn trước khi bị tấn công",
+	"core.bonus.FEAR.name": "Sợ hãi",
+	"core.bonus.FEAR.description": "Gây sợ hãi cho một đạo quân địch",
+	"core.bonus.FEARLESS.name": "Không sợ",
+	"core.bonus.FEARLESS.description": "Kháng lại kỹ năng gây sợ hãi",
+	"core.bonus.FEROCITY.name": "Hung ác",
+	"core.bonus.FEROCITY.description": "Tấn công thêm ${val} lần nữa nếu giết chết kẻ địch",
+	"core.bonus.FLYING.name": "Bay",
+	"core.bonus.FLYING.description": "Bay khi di chuyển (vượt chướng ngại vật)",
+	"core.bonus.FREE_SHOOTING.name": "Bắn gần",
+	"core.bonus.FREE_SHOOTING.description": "Có thể bắn khi cận chiến",
+	"core.bonus.GARGOYLE.name": "Gargoyle",
+	"core.bonus.GARGOYLE.description": "Không thể hồi sinh hoặc chữa lành",
+	"core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Giảm (${val}%) sát thương",
+	"core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Giảm sát thương vật lý từ các đòn đánh xa hoặc cận chiến",
+	"core.bonus.HATE.name": "Ghét ${subtype.creature}",
+	"core.bonus.HATE.description": "Gây thêm ${val}% sát thương cho ${subtype.creature}",
+	"core.bonus.HEALER.name": "Hồi máu",
+	"core.bonus.HEALER.description": "Hồi máu cho các đơn vị đồng minh",
+	"core.bonus.HP_REGENERATION.name": "Tự hồi máu",
+	"core.bonus.HP_REGENERATION.description": "Tự hồi ${val} máu mỗi lượt",
+	"core.bonus.JOUSTING.name": "Kị binh",
+	"core.bonus.JOUSTING.description": "Tăng +${val}% sát thương theo bước đi qua mỗi ô lục giác",
+	"core.bonus.KING.name": "King",
+	"core.bonus.KING.description": "Dễ bị kích sát ở cấp ${val} hoặc cao hơn",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.name": "Kháng phép 1-${val}",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.description": "Kháng phép cấp 1-${val}",
+	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Hạn chế tầm bắn",
+	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Không thể bắn các mục tiêu xa hơn ${val} ô",
+	"core.bonus.LIFE_DRAIN.name": "Hút sinh lực (${val}%)",
+	"core.bonus.LIFE_DRAIN.description": "Hút ${val}% sát thương gây ra chuyển thành sinh lực",
+	"core.bonus.MANA_CHANNELING.name": "Chuyển năng lượng ${val}%",
+	"core.bonus.MANA_CHANNELING.description": "Cho tướng của bạn ${val}% năng lượng khi tướng địch sử dụng",
+	"core.bonus.MANA_DRAIN.name": "Hút năng lượng",
+	"core.bonus.MANA_DRAIN.description": "Hút ${val} năng lượng mỗi lượt",
+	"core.bonus.MAGIC_MIRROR.name": "Gương ma thuật (${val}%)",
+	"core.bonus.MAGIC_MIRROR.description": "Có ${val}% cơ hội phản lại một đòn phép sang một đơn vị quân địch",
+	"core.bonus.MAGIC_RESISTANCE.name": "Kháng phép (${val}%)",
+	"core.bonus.MAGIC_RESISTANCE.description": "Có ${val}% cơ hội kháng lại một phép của kẻ địch",
+	"core.bonus.MIND_IMMUNITY.name": "Kháng phép tâm trí",
+	"core.bonus.MIND_IMMUNITY.description": "Kháng tất cả các phép tâm trí",
+	"core.bonus.NO_DISTANCE_PENALTY.name": "Bắn xa",
+	"core.bonus.NO_DISTANCE_PENALTY.description": "Gây ra sát thương tối đa ở mọi khoảng cách",
+	"core.bonus.NO_MELEE_PENALTY.name": "Cận chiến",
+	"core.bonus.NO_MELEE_PENALTY.description": "Quân không bị giảm sát thương khi cận chiến",
+	"core.bonus.NO_MORALE.name": "Nhuệ khí",
+	"core.bonus.NO_MORALE.description": "Quân không bị ảnh hưởng bởi nhuệ khí",
+	"core.bonus.NO_WALL_PENALTY.name": "Bắn xuyên tường",
+	"core.bonus.NO_WALL_PENALTY.description": "Gây ra sát thương tối đa khi công thành",
+	"core.bonus.NON_LIVING.name": "Non living",
+	"core.bonus.NON_LIVING.description": "Miễn nhiễm với nhiều hiệu ứng",
+	"core.bonus.RANDOM_SPELLCASTER.name": "Dùng phép ngẫu nhiên",
+	"core.bonus.RANDOM_SPELLCASTER.description": "Có thể dùng phép ngẫu nhiên trong trận đấu",
+	"core.bonus.RANGED_RETALIATION.name": "Phản đòn từ xa",
+	"core.bonus.RANGED_RETALIATION.description": "Có thể phản đòn từ xa khi bị bắn",
+	"core.bonus.RECEPTIVE.name": "Hấp thụ",
+	"core.bonus.RECEPTIVE.description": "Hập thụ các phép thuật thân thiện",
+	"core.bonus.REBIRTH.name": "Tái sinh (${val}%)",
+	"core.bonus.REBIRTH.description": "${val}% số quân sẽ hồi sinh sau khi chết",
+	"core.bonus.RETURN_AFTER_STRIKE.name": "Đánh và Trở lại",
+	"core.bonus.RETURN_AFTER_STRIKE.description": "Quay trở lại vị trí ban đầu sau khi đánh",
+	"core.bonus.REVENGE.name": "Trả thù",
+	"core.bonus.REVENGE.description": "Gây thêm sát thương theo lượng máu của kẻ địch bị mất",
+	"core.bonus.SHOOTER.name": "Bắn xa",
+	"core.bonus.SHOOTER.description": "Quân có thể bắn từ xa",
+	"core.bonus.SHOOTS_ALL_ADJACENT.name": "Bắn xung quanh",
+	"core.bonus.SHOOTS_ALL_ADJACENT.description": "Bắn tất cả các mục tiêu trong một khu vực nhỏ",
+	"core.bonus.SOUL_STEAL.name": "Bắt linh hồn",
+	"core.bonus.SOUL_STEAL.description": "Tăng thêm ${val} quân mới với mỗi quân địch bị giết",
+	"core.bonus.SPELLCASTER.name": "Dùng phép có ích",
+	"core.bonus.SPELLCASTER.description": "Có thể dùng phép ${subtype.spell}",
+	"core.bonus.SPELL_AFTER_ATTACK.name": "Dùng phép trước",
+	"core.bonus.SPELL_AFTER_ATTACK.description": "Có ${val}% cơ hội dùng phép ${subtype.spell} trước khi đánh",
+	"core.bonus.SPELL_BEFORE_ATTACK.name": "Dùng phép sau",
+	"core.bonus.SPELL_BEFORE_ATTACK.description": "Có ${val}% cơ hội dùng phép ${subtype.spell} sau khi đánh",
+	"core.bonus.SPELL_IMMUNITY.name": "Kháng phép",
+	"core.bonus.SPELL_IMMUNITY.description": "Kháng phép ${subtype.spell}",
+	"core.bonus.SPELL_LIKE_ATTACK.name": "Đánh bằng phép",
+	"core.bonus.SPELL_LIKE_ATTACK.description": "Tấn công bằng phép ${subtype.spell}",
+	"core.bonus.SPELL_RESISTANCE_AURA.name": "Hào quang kháng phép",
+	"core.bonus.SPELL_RESISTANCE_AURA.description": "Quân ở gần sẽ nhận được ${val}% kháng phép",
+	"core.bonus.SUMMON_GUARDIANS.name": "Gọi bảo vệ",
+	"core.bonus.SUMMON_GUARDIANS.description": "Khi bắt đầu trận sẽ triệu hồi ${subtype.creature} (${val}%)",
+	"core.bonus.SYNERGY_TARGET.name": "Hợp lực",
+	"core.bonus.SYNERGY_TARGET.description": "Quân này dễ bị ảnh hưởng bởi nhiều hiệu ứng",
+	"core.bonus.TWO_HEX_ATTACK_BREATH.name": "Đánh hai ô",
+	"core.bonus.TWO_HEX_ATTACK_BREATH.description": "Tấn công bằng hơi thở (xuyên 2 ô)",
+	"core.bonus.THREE_HEADED_ATTACK.name": "Ba đầu",
+	"core.bonus.THREE_HEADED_ATTACK.description": "Tấn công cả quân liền kề mục tiêu",
+	"core.bonus.TRANSMUTATION.name": "Biến đổi",
+	"core.bonus.TRANSMUTATION.description": "Có ${val}% cơ hội biến đổi quân mục tiêu thành dạng khác",
+	"core.bonus.UNDEAD.name": "Âm binh",
+	"core.bonus.UNDEAD.description": "Quân là âm binh",
+	"core.bonus.UNLIMITED_RETALIATIONS.name": "Phản đòn vô hạn",
+	"core.bonus.UNLIMITED_RETALIATIONS.description": "Có thể phản đòn không giới hạn",
+	"core.bonus.WATER_IMMUNITY.name": "Kháng nước",
+	"core.bonus.WATER_IMMUNITY.description": "Kháng tất cả phép nước trong trường học phép thuật",
+	"core.bonus.WIDE_BREATH.name": "Đánh nhiều ô",
+	"core.bonus.WIDE_BREATH.description": "Tấn công bằng hơi thở (xuyên nhiều ô)",
+	"core.bonus.DISINTEGRATE.name": "Hủy xác",
+	"core.bonus.DISINTEGRATE.description": "Xác chết biến mất sau khi bị giết",
+	"core.bonus.INVINCIBLE.name": "Bất bại",
+	"core.bonus.INVINCIBLE.description": "Miễn nhiễm với mọi hiệu ứng",
+	"core.bonus.MECHANICAL.name": "Máy",
+	"core.bonus.MECHANICAL.description": "Miễn nhiễm với nhiều hiệu ứng, có thể sửa chữa",
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.name": "Đánh ba hướng",
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.description": "Tấn công bằng hơi thở (ra ba hướng)",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "Kháng phép",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.air": "Kháng phép gió",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.fire": "Kháng phép lửa",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.water": "Kháng phép nước",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.earth": "Kháng phép đất",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description": "Giảm ${val}% sát thương của tất cả phép thuật.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.air": "Giảm ${val}% sát thương từ phép gió.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.fire": "Giảm ${val}% sát thương từ phép lửa.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.water": "Giảm ${val}% sát thương từ phép nước.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.earth": "Giảm ${val}% sát thương từ phép đất.",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name": "Kháng phép",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.air": "Kháng phép gió",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.fire": "Kháng phép lửa",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.water": "Kháng phép nước",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.earth": "Kháng phép đất",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description": "Đơn vị này kháng tất cả phép thuật",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.air": "Đơn vị này kháng tất cả phép gió",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.fire": "Đơn vị này kháng tất cả phép lửa",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.water": "Đơn vị này kháng tất cả phép nước",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.earth": "Đơn vị này kháng tất cả phép đất",
+	"core.bonus.OPENING_BATTLE_SPELL.name": "Khởi đầu với phép",
+	"core.bonus.OPENING_BATTLE_SPELL.description": "Dùng phép ${subtype.spell} khi bắt đầu trận chiến"
 }

+ 2 - 2
Mods/vcmi/mod.json

@@ -111,9 +111,9 @@
 	},
 
 	"vietnamese": {
-		"name": "VCMI essential files",
+		"name": "VCMI - Tập tin chính",
 		"description": "Các tập tin cần thiết để chạy VCMI",
-		"author": "Vũ Đắc Hoàng Ân",
+		"author": "Hunter8x - Bé Còi",
 		"skipValidation": true,
 		"translations": [
 			"config/vietnamese.json"

+ 1 - 1
client/NetPacksClient.cpp

@@ -821,7 +821,7 @@ void ApplyClientNetPackVisitor::visitBattleSetActiveStack(BattleSetActiveStack &
 
 	const CStack *activated = gs.getBattle(pack.battleID)->battleGetStackByID(pack.stack);
 	PlayerColor playerToCall; //pack.player that will move activated stack
-	if(activated->hasBonusOfType(BonusType::HYPNOTIZED))
+	if(activated->isHypnotized())
 	{
 		playerToCall = gs.getBattle(pack.battleID)->getSide(BattleSide::ATTACKER).color == activated->unitOwner()
 			? gs.getBattle(pack.battleID)->getSide(BattleSide::DEFENDER).color

+ 7 - 1
client/mainmenu/CMainMenu.cpp

@@ -76,7 +76,13 @@ CMenuScreen::CMenuScreen(const JsonNode & configNode)
 {
 	OBJECT_CONSTRUCTION;
 
-	background = std::make_shared<CPicture>(ImagePath::fromJson(config["background"]));
+	const auto& bgConfig = config["background"];
+	if (bgConfig.isVector() && !bgConfig.Vector().empty())
+		background = std::make_shared<CPicture>(ImagePath::fromJson(*RandomGeneratorUtil::nextItem(bgConfig.Vector(), CRandomGenerator::getDefault())));
+
+	if (bgConfig.isString())
+		background = std::make_shared<CPicture>(ImagePath::fromJson(bgConfig));
+
 	if(config["scalable"].Bool())
 		background->scaleTo(GH.screenDimensions());
 

+ 8 - 11
client/mapView/MapOverlayLogVisualizer.cpp

@@ -17,6 +17,7 @@
 #include "../render/Colors.h"
 #include "../render/EFont.h"
 #include "../render/IFont.h"
+#include "../render/IScreenHandler.h"
 #include "../render/IRenderHandler.h"
 #include "../render/Graphics.h"
 #include "../gui/TextAlignment.h"
@@ -30,24 +31,20 @@ MapOverlayLogVisualizer::MapOverlayLogVisualizer(Canvas & target, std::shared_pt
 
 void MapOverlayLogVisualizer::drawLine(int3 start, int3 end)
 {
-	const Point offset = Point(30, 30);
-
 	auto level = model->getLevel();
 
 	if(start.z != level || end.z != level)
 		return;
 
-	auto pStart = model->getTargetTileArea(start).topLeft();
-	auto pEnd = model->getTargetTileArea(end).topLeft();
-	auto viewPort = target.getRenderArea();
-
-	pStart.x += 3;
-	pEnd.x -= 3;
+	int scaling = GH.screenHandler().getScalingFactor();
+	auto pStart = model->getTargetTileArea(start).center();
+	auto pEnd = model->getTargetTileArea(end).center();
+	Rect viewPortRaw = target.getRenderArea();
+	Rect viewPort(viewPortRaw.topLeft() / scaling, viewPortRaw.dimensions() / scaling );
 
-	pStart += offset;
-	pEnd += offset;
+	Point workaroundOffset(8,8); // not sure why it is needed. Removing leads to incorrect clipping near view edges
 
-	if(viewPort.isInside(pStart) && viewPort.isInside(pEnd))
+	if(viewPort.isInside(pStart + workaroundOffset) && viewPort.isInside(pEnd + workaroundOffset))
 	{
 		target.drawLine(pStart, pEnd, ColorRGBA(255, 255, 0), ColorRGBA(255, 0, 0));
 	}

+ 1 - 1
client/mapView/MapView.cpp

@@ -65,7 +65,7 @@ void BasicMapView::render(Canvas & target, bool fullUpdate)
 	tilesCache->update(controller->getContext());
 	tilesCache->render(controller->getContext(), targetClipped, fullUpdate);
 
-	MapOverlayLogVisualizer r(target, model);
+	MapOverlayLogVisualizer r(targetClipped, model);
 	logVisual->visualize(r);
 }
 

+ 32 - 7
client/media/CVideoHandler.cpp

@@ -59,8 +59,18 @@ static si64 lodSeek(void * opaque, si64 pos, int whence)
 	return data->seek(pos);
 }
 
+static void logFFmpegError(int errorCode)
+{
+	std::array<char, AV_ERROR_MAX_STRING_SIZE> errorMessage{};
+	av_strerror(errorCode, errorMessage.data(), errorMessage.size());
+
+	logGlobal->warn("Failed to open video file! Reason: %s", errorMessage.data());
+}
+
 [[noreturn]] static void throwFFmpegError(int errorCode)
 {
+	logFFmpegError(errorCode);
+
 	std::array<char, AV_ERROR_MAX_STRING_SIZE> errorMessage{};
 	av_strerror(errorCode, errorMessage.data(), errorMessage.size());
 
@@ -95,7 +105,7 @@ bool FFMpegStream::openInput(const VideoPath & videoToOpen)
 	return input != nullptr;
 }
 
-void FFMpegStream::openContext()
+bool FFMpegStream::openContext()
 {
 	static const int BUFFER_SIZE = 4096;
 	input->seek(0);
@@ -109,13 +119,21 @@ void FFMpegStream::openContext()
 	int avfopen = avformat_open_input(&formatContext, "dummyFilename", nullptr, nullptr);
 
 	if(avfopen != 0)
-		throwFFmpegError(avfopen);
+	{
+		logFFmpegError(avfopen);
+		return false;
+	}
 
 	// Retrieve stream information
 	int findStreamInfo = avformat_find_stream_info(formatContext, nullptr);
 
 	if(avfopen < 0)
-		throwFFmpegError(findStreamInfo);
+	{
+		logFFmpegError(findStreamInfo);
+		return false;
+	}
+
+	return true;
 }
 
 void FFMpegStream::openCodec(int desiredStreamIndex)
@@ -169,10 +187,13 @@ const AVFrame * FFMpegStream::getCurrentFrame() const
 	return frame;
 }
 
-void CVideoInstance::openVideo()
+bool CVideoInstance::openVideo()
 {
-	openContext();
+	if (!openContext())
+		return false;
+
 	openCodec(findVideoStream());
+	return true;
 }
 
 void CVideoInstance::prepareOutput(float scaleFactor, bool useTextureOutput)
@@ -526,7 +547,9 @@ std::pair<std::unique_ptr<ui8 []>, si64> CAudioInstance::extractAudio(const Vide
 {
 	if (!openInput(videoToOpen))
 		return { nullptr, 0};
-	openContext();
+
+	if (!openContext())
+		return { nullptr, 0};
 
 	int audioStreamIndex = findAudioStream();
 	if (audioStreamIndex == -1)
@@ -653,7 +676,9 @@ std::unique_ptr<IVideoInstance> CVideoPlayer::open(const VideoPath & name, float
 	if (!result->openInput(name))
 		return nullptr;
 
-	result->openVideo();
+	if (!result->openVideo())
+		return nullptr;
+
 	result->prepareOutput(scaleFactor, false);
 	result->loadNextFrame(); // prepare 1st frame
 

+ 2 - 2
client/media/CVideoHandler.h

@@ -42,7 +42,7 @@ class FFMpegStream : boost::noncopyable
 	AVFrame * frame = nullptr;
 
 protected:
-	void openContext();
+	bool openContext();
 	void openCodec(int streamIndex);
 
 	int findVideoStream() const;
@@ -91,7 +91,7 @@ public:
 	CVideoInstance();
 	~CVideoInstance();
 
-	void openVideo();
+	bool openVideo();
 	bool loadNextFrame();
 
 	double timeStamp() final;

+ 0 - 2
client/renderSDL/CTrueTypeFont.cpp

@@ -20,8 +20,6 @@
 #include "../../lib/filesystem/Filesystem.h"
 #include "../../lib/texts/TextOperations.h"
 
-#include <SDL_ttf.h>
-
 std::pair<std::unique_ptr<ui8[]>, ui64> CTrueTypeFont::loadData(const JsonNode & config)
 {
 	std::string filename = "Data/" + config["file"].String();

+ 2 - 2
client/renderSDL/CTrueTypeFont.h

@@ -11,14 +11,14 @@
 
 #include "../render/IFont.h"
 
+#include <SDL_ttf.h>
+
 VCMI_LIB_NAMESPACE_BEGIN
 class JsonNode;
 VCMI_LIB_NAMESPACE_END
 
 class CBitmapFont;
 
-using TTF_Font = struct _TTF_Font;
-
 class CTrueTypeFont final : public IFont
 {
 	const std::pair<std::unique_ptr<ui8[]>, ui64> data;

+ 1 - 1
client/renderSDL/ScreenHandler.cpp

@@ -350,7 +350,7 @@ EUpscalingFilter ScreenHandler::loadUpscalingFilter() const
 	};
 
 	auto filterName = settings["video"]["upscalingFilter"].String();
-	auto filter = upscalingFilterTypes.at(filterName);
+	auto filter = upscalingFilterTypes.count(filterName) ? upscalingFilterTypes.at(filterName) : EUpscalingFilter::AUTO;
 
 	if (filter != EUpscalingFilter::AUTO)
 		return filter;

+ 8 - 2
client/windows/CHeroOverview.cpp

@@ -19,9 +19,11 @@
 #include "../render/IImage.h"
 #include "../renderSDL/RenderHandler.h"
 #include "../widgets/CComponentHolder.h"
+#include "../widgets/Slider.h"
 #include "../widgets/Images.h"
 #include "../widgets/TextControls.h"
 #include "../widgets/GraphicalPrimitiveCanvas.h"
+#include "../eventsSDL/InputHandler.h"
 
 #include "../../lib/IGameSettings.h"
 #include "../../lib/entities/hero/CHeroHandler.h"
@@ -99,7 +101,9 @@ void CHeroOverview::genControls()
     // hero biography
     r = Rect(borderOffset, 5 * borderOffset + yOffset + 148, 284, 130);
     backgroundRectangles.push_back(std::make_shared<TransparentFilledRectangle>(r.resize(1), rectangleColor, borderColor));
-    labelHeroBiography = std::make_shared<CMultiLineLabel>(r.resize(-borderOffset), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIdx]->getBiographyTranslated());
+    labelHeroBiography = std::make_shared<CTextBox>((*CGI->heroh)[heroIdx]->getBiographyTranslated(), r.resize(-borderOffset), CSlider::EStyle::BROWN, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
+    if(labelHeroBiography->slider && GH.input().getCurrentInputMode() != InputMode::TOUCH)
+        labelHeroBiography->slider->clearScrollBounds();
 
     // speciality name
     r = Rect(2 * borderOffset + 44, 6 * borderOffset + yOffset + 278, 235, 44);
@@ -115,7 +119,9 @@ void CHeroOverview::genControls()
     // speciality description
     r = Rect(borderOffset, 7 * borderOffset + yOffset + 322, 284, 85);
     backgroundRectangles.push_back(std::make_shared<TransparentFilledRectangle>(r.resize(1), rectangleColor, borderColor));
-	labelSpecialityDescription = std::make_shared<CMultiLineLabel>(r.resize(-borderOffset), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIdx]->getSpecialtyDescriptionTranslated());
+	labelSpecialityDescription = std::make_shared<CTextBox>((*CGI->heroh)[heroIdx]->getSpecialtyDescriptionTranslated(), r.resize(-borderOffset), CSlider::EStyle::BROWN, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
+    if(labelSpecialityDescription->slider && GH.input().getCurrentInputMode() != InputMode::TOUCH)
+        labelSpecialityDescription->slider->clearScrollBounds();
 
     // army title
     r = Rect(302, borderOffset + yOffset, 292, 30);

+ 2 - 2
client/windows/CHeroOverview.h

@@ -39,7 +39,7 @@ class CHeroOverview : public CWindowObject
     std::shared_ptr<CLabel> labelTitle;
     std::shared_ptr<CAnimImage> imageHero;
     std::shared_ptr<CLabel> labelHeroName;
-    std::shared_ptr<CMultiLineLabel> labelHeroBiography;
+    std::shared_ptr<CTextBox> labelHeroBiography;
     std::shared_ptr<CLabel> labelHeroClass;
     std::shared_ptr<CLabel> labelHeroSpeciality;
     std::shared_ptr<CAnimImage> imageSpeciality;
@@ -47,7 +47,7 @@ class CHeroOverview : public CWindowObject
     std::vector<std::shared_ptr<CAnimImage>> imageSkill;
     std::vector<std::shared_ptr<CLabel>> labelSkillFooter;
     std::shared_ptr<CLabel> labelSpecialityName;
-    std::shared_ptr<CMultiLineLabel> labelSpecialityDescription;
+    std::shared_ptr<CTextBox> labelSpecialityDescription;
 
     std::shared_ptr<CLabel> labelArmyTitle;
     std::vector<std::shared_ptr<CAnimImage>> imageArmy;

+ 12 - 3
client/windows/CMessage.cpp

@@ -28,6 +28,7 @@
 #include "../../lib/texts/TextOperations.h"
 
 constexpr int RIGHT_CLICK_POPUP_MIN_SIZE = 100;
+constexpr int RIGHT_CLICK_POPUP_MAX_HEIGHT_TEXTONLY = 450;
 constexpr int SIDE_MARGIN = 11;
 constexpr int TOP_MARGIN = 20;
 constexpr int BOTTOM_MARGIN = 16;
@@ -260,9 +261,17 @@ void CMessage::drawIWindow(CInfoWindow * ret, std::string text, PlayerColor play
 	if(ret->buttons.empty() && !ret->components)
 	{
 		// use more compact form for right-click popup with no buttons / components
-
-		ret->pos.w = std::max(RIGHT_CLICK_POPUP_MIN_SIZE, ret->text->label->textSize.x + 2 * SIDE_MARGIN);
-		ret->pos.h = std::max(RIGHT_CLICK_POPUP_MIN_SIZE, ret->text->label->textSize.y + TOP_MARGIN + BOTTOM_MARGIN);
+		if(ret->text->slider)
+		{
+			ret->text->resize(Point(ret->text->pos.w, std::min(ret->text->label->textSize.y, RIGHT_CLICK_POPUP_MAX_HEIGHT_TEXTONLY)));
+			ret->pos.w = std::max(RIGHT_CLICK_POPUP_MIN_SIZE, ret->text->pos.w + 2 * SIDE_MARGIN);
+			ret->pos.h = std::max(RIGHT_CLICK_POPUP_MIN_SIZE, ret->text->pos.h + TOP_MARGIN + BOTTOM_MARGIN);
+		}
+		else
+		{
+			ret->pos.w = std::max(RIGHT_CLICK_POPUP_MIN_SIZE, ret->text->label->textSize.x + 2 * SIDE_MARGIN);
+			ret->pos.h = std::max(RIGHT_CLICK_POPUP_MIN_SIZE, ret->text->label->textSize.y + TOP_MARGIN + BOTTOM_MARGIN);
+		}
 	}
 	else
 	{

+ 121 - 26
client/windows/InfoWindows.cpp

@@ -31,12 +31,13 @@
 #include "../../CCallback.h"
 
 #include "../../lib/CConfigHandler.h"
-#include "../ConditionalWait.h"
 #include "../../lib/gameState/InfoAboutArmy.h"
 #include "../../lib/mapObjects/CGCreature.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
+#include "../../lib/mapObjects/CQuest.h"
 #include "../../lib/mapObjects/MiscObjects.h"
+#include "../ConditionalWait.h"
 
 CSelWindow::CSelWindow( const std::string & Text, PlayerColor player, int charperline, const std::vector<std::shared_ptr<CSelectableComponent>> & comps, const std::vector<std::pair<AnimationPath, CFunctionList<void()>>> & Buttons, QueryID askID)
 {
@@ -333,12 +334,10 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGCreature * creature)
 	fitToScreen(10);
 }
 
-TeleporterPopup::TeleporterPopup(const Point & position, const CGTeleport * teleporter)
-	: CWindowObject(BORDERED | RCLICK_POPUP)
+MinimapWithIcons::MinimapWithIcons(const Point & position)
 {
 	OBJECT_CONSTRUCTION;
-	pos.w = 322;
-	pos.h = 200;
+	pos += position;
 
 	Rect areaSurface(11, 41, 144, 144);
 	Rect areaUnderground(167, 41, 144, 144);
@@ -346,16 +345,14 @@ TeleporterPopup::TeleporterPopup(const Point & position, const CGTeleport * tele
 	Rect borderSurface(10, 40, 147, 147);
 	Rect borderUnderground(166, 40, 147, 147);
 
-	bool singleLevelMap = LOCPLINT->cb->getMapSize().y == 0;
+	bool singleLevelMap = LOCPLINT->cb->getMapSize().z == 1;
 
 	if (singleLevelMap)
 	{
-		areaSurface.x += 144;
-		borderSurface.x += 144;
+		areaSurface.x += 78;
+		borderSurface.x += 78;
 	}
 
-	filledBackground = std::make_shared<FilledTexturePlayerColored>(Rect(0, 0, pos.w, pos.h));
-
 	backgroundSurface = std::make_shared<TransparentFilledRectangle>(borderSurface, Colors::TRANSPARENCY, Colors::YELLOW);
 	surface = std::make_shared<CMinimapInstance>(areaSurface.topLeft(), areaSurface.dimensions(), 0);
 
@@ -364,8 +361,43 @@ TeleporterPopup::TeleporterPopup(const Point & position, const CGTeleport * tele
 		backgroundUnderground = std::make_shared<TransparentFilledRectangle>(borderUnderground, Colors::TRANSPARENCY, Colors::YELLOW);
 		undergroud = std::make_shared<CMinimapInstance>(areaUnderground.topLeft(), areaUnderground.dimensions(), 1);
 	}
+}
+
+void MinimapWithIcons::addIcon(const int3 & coordinates, const ImagePath & image )
+{
+	OBJECT_CONSTRUCTION;
+
+	Rect areaSurface(11, 41, 144, 144);
+	Rect areaUnderground(167, 41, 144, 144);
+	bool singleLevelMap = LOCPLINT->cb->getMapSize().z == 1;
+	if (singleLevelMap)
+		areaSurface.x += 78;
+
+	int positionX = 144 * coordinates.x / LOCPLINT->cb->getMapSize().x;
+	int positionY = 144 * coordinates.y / LOCPLINT->cb->getMapSize().y;
+
+	Point iconPosition(positionX, positionY);
 
+	iconPosition -= Point(8,8); // compensate for 16x16 icon half-size
+
+	if (coordinates.z == 0)
+		iconPosition += areaSurface.topLeft();
+	else
+		iconPosition += areaUnderground.topLeft();
+
+	iconsOverlay.push_back(std::make_shared<CPicture>(image, iconPosition));
+}
+
+TeleporterPopup::TeleporterPopup(const Point & position, const CGTeleport * teleporter)
+	: CWindowObject(BORDERED | RCLICK_POPUP)
+{
+	OBJECT_CONSTRUCTION;
+	pos.w = 322;
+	pos.h = 200;
+
+	filledBackground = std::make_shared<FilledTexturePlayerColored>(Rect(0, 0, pos.w, pos.h));
 	labelTitle = std::make_shared<CLabel>(pos.w / 2, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, teleporter->getPopupText(LOCPLINT->playerID));
+	minimap = std::make_shared<MinimapWithIcons>(Point(0,0));
 
 	const auto & entrances = teleporter->getAllEntrances();
 	const auto & exits = teleporter->getAllExits();
@@ -382,32 +414,89 @@ TeleporterPopup::TeleporterPopup(const Point & position, const CGTeleport * tele
 			continue;
 
 		int3 position = exitObject->visitablePos();
+		ImagePath image;
 
-		int positionX = 144 * position.x / LOCPLINT->cb->getMapSize().x;
-		int positionY = 144 * position.y / LOCPLINT->cb->getMapSize().y;
+		if (!vstd::contains(entrances, exit))
+			image = ImagePath::builtin("minimapIcons/portalExit");
+		else if (!vstd::contains(exits, exit))
+			image = ImagePath::builtin("minimapIcons/portalEntrance");
+		else
+			image = ImagePath::builtin("minimapIcons/portalBidirectional");
 
-		Point iconPosition(positionX, positionY);
+		minimap->addIcon(position, image);
+	}
+	center(position);
+	fitToScreen(10);
+}
 
-		iconPosition -= Point(8,8); // compensate for 16x16 icon half-size
+KeymasterPopup::KeymasterPopup(const Point & position, const CGKeys * keymasterOrGuard)
+	: CWindowObject(BORDERED | RCLICK_POPUP)
+{
+	OBJECT_CONSTRUCTION;
+	pos.w = 322;
+	pos.h = 220;
 
-		if (position.z == 0)
-			iconPosition += areaSurface.topLeft();
-		else
-			iconPosition += areaUnderground.topLeft();
+	filledBackground = std::make_shared<FilledTexturePlayerColored>(Rect(0, 0, pos.w, pos.h));
+	labelTitle = std::make_shared<CLabel>(pos.w / 2, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, keymasterOrGuard->getObjectName());
+	labelDescription = std::make_shared<CLabel>(pos.w / 2, 40, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, keymasterOrGuard->getObjectDescription(LOCPLINT->playerID));
+	minimap = std::make_shared<MinimapWithIcons>(Point(0,20));
 
-		ImagePath image;
+	const auto allObjects = LOCPLINT->cb->getAllVisitableObjs();
 
-		if (!vstd::contains(entrances, exit))
-			image = ImagePath::builtin("portalExit");
-		else if (!vstd::contains(exits, exit))
-			image = ImagePath::builtin("portalEntrance");
-		else
-			image = ImagePath::builtin("portalBidirectional");
+	for (const auto mapObject : allObjects)
+	{
+		if (!mapObject)
+			continue;
 
-		iconsOverlay.push_back(std::make_shared<CPicture>(image, iconPosition));
+		switch (mapObject->ID)
+		{
+			case Obj::KEYMASTER:
+				if (mapObject->subID == keymasterOrGuard->subID)
+					minimap->addIcon(mapObject->visitablePos(), ImagePath::builtin("minimapIcons/keymaster"));
+				break;
+			case Obj::BORDERGUARD:
+				if (mapObject->subID == keymasterOrGuard->subID)
+					minimap->addIcon(mapObject->visitablePos(), ImagePath::builtin("minimapIcons/borderguard"));
+				break;
+			case Obj::BORDER_GATE:
+				if (mapObject->subID == keymasterOrGuard->subID)
+					minimap->addIcon(mapObject->visitablePos(), ImagePath::builtin("minimapIcons/bordergate"));
+				break;
+		}
 	}
+	center(position);
+	fitToScreen(10);
+}
+
+ObeliskPopup::ObeliskPopup(const Point & position, const CGObelisk * obelisk)
+	: CWindowObject(BORDERED | RCLICK_POPUP)
+{
+	OBJECT_CONSTRUCTION;
+	pos.w = 322;
+	pos.h = 220;
+
+	filledBackground = std::make_shared<FilledTexturePlayerColored>(Rect(0, 0, pos.w, pos.h));
+	labelTitle = std::make_shared<CLabel>(pos.w / 2, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, obelisk->getObjectName());
+	labelDescription = std::make_shared<CLabel>(pos.w / 2, 40, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, obelisk->getObjectDescription(LOCPLINT->playerID));
+	minimap = std::make_shared<MinimapWithIcons>(Point(0,20));
 
+	const auto allObjects = LOCPLINT->cb->getAllVisitableObjs();
+
+	for (const auto mapObject : allObjects)
+	{
+		if (!mapObject)
+			continue;
+
+		if (mapObject->ID != Obj::OBELISK)
+			continue;
+
+		if (mapObject->wasVisited(LOCPLINT->playerID))
+			minimap->addIcon(mapObject->visitablePos(), ImagePath::builtin("minimapIcons/obeliskVisited"));
+		else
+			minimap->addIcon(mapObject->visitablePos(), ImagePath::builtin("minimapIcons/obelisk"));
+	}
 	center(position);
+	fitToScreen(10);
 }
 
 std::shared_ptr<WindowBase>
@@ -439,6 +528,12 @@ CRClickPopup::createCustomInfoWindow(Point position, const CGObjectInstance * sp
 		case Obj::SUBTERRANEAN_GATE:
 		case Obj::WHIRLPOOL:
 			return std::make_shared<TeleporterPopup>(position, dynamic_cast<const CGTeleport *>(specific));
+		case Obj::KEYMASTER:
+		case Obj::BORDERGUARD:
+		case Obj::BORDER_GATE:
+			return std::make_shared<KeymasterPopup>(position, dynamic_cast<const CGKeys *>(specific));
+		case Obj::OBELISK:
+			return std::make_shared<ObeliskPopup>(position, dynamic_cast<const CGObelisk *>(specific));
 		default:
 			return std::shared_ptr<WindowBase>();
 	}

+ 36 - 4
client/windows/InfoWindows.h

@@ -21,6 +21,8 @@ class CGHeroInstance;
 class CGGarrison;
 class CGCreature;
 class CGTeleport;
+class CGKeys;
+class CGObelisk;
 
 VCMI_LIB_NAMESPACE_END
 
@@ -116,20 +118,50 @@ public:
 	CSelWindow(const std::string & text, PlayerColor player, int charperline, const std::vector<std::shared_ptr<CSelectableComponent>> & comps, const std::vector<std::pair<AnimationPath,CFunctionList<void()> > > &Buttons, QueryID askID);
 };
 
-class TeleporterPopup : public CWindowObject
+class MinimapWithIcons : public CIntObject
 {
-	std::shared_ptr<FilledTexturePlayerColored> filledBackground;
-
 	std::shared_ptr<TransparentFilledRectangle> backgroundSurface;
 	std::shared_ptr<TransparentFilledRectangle> backgroundUnderground;
 
 	std::shared_ptr<CMinimapInstance> surface;
 	std::shared_ptr<CMinimapInstance> undergroud;
 
+	std::vector<std::shared_ptr<CPicture>> iconsOverlay;
+
+public:
+	MinimapWithIcons(const Point & position);
+
+	void addIcon(const int3 & coordinates, const ImagePath & image);
+};
+
+class TeleporterPopup : public CWindowObject
+{
+	std::shared_ptr<FilledTexturePlayerColored> filledBackground;
+	std::shared_ptr<MinimapWithIcons> minimap;
 	std::shared_ptr<CLabel> labelTitle;
 
-	std::vector<std::shared_ptr<CPicture>> iconsOverlay;
 public:
 	TeleporterPopup(const Point & position, const CGTeleport * teleporter);
 };
 
+class KeymasterPopup : public CWindowObject
+{
+	std::shared_ptr<FilledTexturePlayerColored> filledBackground;
+	std::shared_ptr<MinimapWithIcons> minimap;
+	std::shared_ptr<CLabel> labelTitle;
+	std::shared_ptr<CLabel> labelDescription;
+
+public:
+	KeymasterPopup(const Point & position, const CGKeys * keymasterOrGuard);
+};
+
+class ObeliskPopup : public CWindowObject
+{
+	std::shared_ptr<FilledTexturePlayerColored> filledBackground;
+	std::shared_ptr<MinimapWithIcons> minimap;
+	std::shared_ptr<CLabel> labelTitle;
+	std::shared_ptr<CLabel> labelDescription;
+
+public:
+	ObeliskPopup(const Point & position, const CGObelisk * obelisk);
+};

+ 25 - 20
config/ai/nkai/nkai-settings.json

@@ -38,13 +38,14 @@
 		"maxPriorityPass" : 10,
 		"mainHeroTurnDistanceLimit" : 10,
 		"scoutHeroTurnDistanceLimit" : 5,
+		"threatTurnDistanceLimit" : 1,
 		"maxGoldPressure" : 0.3,
-		"updateHitmapOnTileReveal" : false,
+		"updateHitmapOnTileReveal" : true,
 		"useTroopsFromGarrisons" : true,
 		"openMap": true,
-		"allowObjectGraph": true,
-		"pathfinderBucketsCount" : 4, // old value: 3,
-		"pathfinderBucketSize" : 8, // old value: 7,
+		"allowObjectGraph": false,
+		"pathfinderBucketsCount" : 3,
+		"pathfinderBucketSize" : 7,
 		"retreatThresholdRelative" : 0,
 		"retreatThresholdAbsolute" : 0,
 		"safeAttackRatio" : 1.1,
@@ -58,13 +59,14 @@
 		"maxPriorityPass" : 10,
 		"mainHeroTurnDistanceLimit" : 10,
 		"scoutHeroTurnDistanceLimit" : 5,
+		"threatTurnDistanceLimit" : 4,
 		"maxGoldPressure" : 0.3,
-		"updateHitmapOnTileReveal" : false,
+		"updateHitmapOnTileReveal" : true,
 		"useTroopsFromGarrisons" : true,
 		"openMap": true,
-		"allowObjectGraph": true,
-		"pathfinderBucketsCount" : 4, // old value: 3,
-		"pathfinderBucketSize" : 8, // old value: 7,
+		"allowObjectGraph": false,
+		"pathfinderBucketsCount" : 3,
+		"pathfinderBucketSize" : 7,
 		"retreatThresholdRelative" : 0.1,
 		"retreatThresholdAbsolute" : 5000,
 		"safeAttackRatio" : 1.1,
@@ -78,13 +80,14 @@
 		"maxPriorityPass" : 10,
 		"mainHeroTurnDistanceLimit" : 10,
 		"scoutHeroTurnDistanceLimit" : 5,
+		"threatTurnDistanceLimit" : 5,
 		"maxGoldPressure" : 0.3,
-		"updateHitmapOnTileReveal" : false,
+		"updateHitmapOnTileReveal" : true,
 		"useTroopsFromGarrisons" : true,
 		"openMap": true,
-		"allowObjectGraph": true,
-		"pathfinderBucketsCount" : 4, // old value: 3,
-		"pathfinderBucketSize" : 8, // old value: 7,
+		"allowObjectGraph": false,
+		"pathfinderBucketsCount" : 3,
+		"pathfinderBucketSize" : 7,
 		"retreatThresholdRelative" : 0.3,
 		"retreatThresholdAbsolute" : 10000,
 		"safeAttackRatio" : 1.1,
@@ -98,13 +101,14 @@
 		"maxPriorityPass" : 10,
 		"mainHeroTurnDistanceLimit" : 10,
 		"scoutHeroTurnDistanceLimit" : 5,
+		"threatTurnDistanceLimit" : 5,
 		"maxGoldPressure" : 0.3,
-		"updateHitmapOnTileReveal" : false,
+		"updateHitmapOnTileReveal" : true,
 		"useTroopsFromGarrisons" : true,
 		"openMap": true,
-		"allowObjectGraph": true,
-		"pathfinderBucketsCount" : 4, // old value: 3,
-		"pathfinderBucketSize" : 8, // old value: 7,
+		"allowObjectGraph": false,
+		"pathfinderBucketsCount" : 3,
+		"pathfinderBucketSize" : 7,
 		"retreatThresholdRelative" : 0.3,
 		"retreatThresholdAbsolute" : 10000,
 		"safeAttackRatio" : 1.1,
@@ -118,13 +122,14 @@
 		"maxPriorityPass" : 10,
 		"mainHeroTurnDistanceLimit" : 10,
 		"scoutHeroTurnDistanceLimit" : 5,
+		"threatTurnDistanceLimit" : 5,
 		"maxGoldPressure" : 0.3,
-		"updateHitmapOnTileReveal" : false,
+		"updateHitmapOnTileReveal" : true,
 		"useTroopsFromGarrisons" : true,
 		"openMap": true,
-		"allowObjectGraph": true,
-		"pathfinderBucketsCount" : 4, // old value: 3,
-		"pathfinderBucketSize" : 8, // old value: 7,
+		"allowObjectGraph": false,
+		"pathfinderBucketsCount" : 3,
+		"pathfinderBucketSize" : 7,
 		"retreatThresholdRelative" : 0.3,
 		"retreatThresholdAbsolute" : 10000,
 		"safeAttackRatio" : 1.1,

+ 1 - 1
config/bonuses.json

@@ -305,7 +305,7 @@
 	{
 		"graphics":
 		{
-			"icon":  "zvs/Lib1.res/E_SHOOT"
+			"icon":  "zvs/Lib1.res/LIM_SHOOT"
 		}
 	},
 

+ 5 - 24
config/gameConfig.json

@@ -305,7 +305,11 @@
 			// if heroes are invitable in tavern
 			"tavernInvite"            : false,
 			// minimal primary skills for heroes
-			"minimalPrimarySkills": [ 0, 0, 1, 1]
+			"minimalPrimarySkills": [ 0, 0, 1, 1],
+			/// movement points hero can get on start of the turn when on land, depending on speed of slowest creature (0-based list)
+			"movementPointsLand" : [ 1500, 1500, 1500, 1500, 1560, 1630, 1700, 1760, 1830, 1900, 1960, 2000 ],
+			/// movement points hero can get on start of the turn when on sea, depending on speed of slowest creature (0-based list)
+			"movementPointsSea" : [ 1500 ]
 		},
 
 		"towns":
@@ -560,29 +564,6 @@
 					"type" : "MANA_PER_KNOWLEDGE_PERCENTAGE", //1000% mana per knowledge
 					"val" : 1000,
 					"valueType" : "BASE_NUMBER"
-				},
-				"landMovement" :
-				{
-					"type" : "MOVEMENT", //Basic land movement
-					"subtype" : "heroMovementLand",
-					"val" : 1300,
-					"valueType" : "BASE_NUMBER",
-					"updater" : {
-						"type" : "ARMY_MOVEMENT", //Enable army movement bonus
-						"parameters" : [
-							20, // Movement points for lowest speed numerator
-							3,  // Movement points for lowest speed denominator
-							10, // Resulting value, rounded down, will be multiplied by this number
-							700 // All army movement bonus cannot be higher than this value (so, max movement will be 1300 + 700 for this settings)
-						]
-					}
-				},
-				"seaMovement" :
-				{
-					"type" : "MOVEMENT", //Basic sea movement
-					"subtype" : "heroMovementSea",
-					"val" : 1500,
-					"valueType" : "BASE_NUMBER"
 				}
 			}
 		},

+ 3 - 1
config/schemas/gameSettings.json

@@ -44,7 +44,9 @@
 				"startingStackChances" :      { "type" : "array" },
 				"backpackSize" :              { "type" : "number" },
 				"tavernInvite" :              { "type" : "boolean" },
-				"minimalPrimarySkills" :      { "type" : "array" }
+				"minimalPrimarySkills" :      { "type" : "array" },
+				"movementPointsLand" :        { "type" : "array" },
+				"movementPointsSea" :         { "type" : "array" }
 			}
 		},
 		"towns" : {

+ 6 - 0
debian/changelog

@@ -4,6 +4,12 @@ vcmi (1.7.0) jammy; urgency=medium
 
  -- Ivan Savenko <[email protected]>  Fri, 30 May 2025 12:00:00 +0200
 
+vcmi (1.6.3) jammy; urgency=medium
+
+  * New upstream release
+
+ -- Ivan Savenko <[email protected]>  Fri, 10 Jan 2025 12:00:00 +0200
+
 vcmi (1.6.2) jammy; urgency=medium
 
   * New upstream release

+ 1 - 1
docs/Readme.md

@@ -1,9 +1,9 @@
 # VCMI Project
 
 [![VCMI](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg?branch=develop&event=push)](https://github.com/vcmi/vcmi/actions/workflows/github.yml?query=branch%3Adevelop+event%3Apush)
-[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.6.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.6.0)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.6.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.6.1)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.6.2/total)](https://github.com/vcmi/vcmi/releases/tag/1.6.2)
+[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.6.3/total)](https://github.com/vcmi/vcmi/releases/tag/1.6.3)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases)
 
 VCMI is an open-source recreation of Heroes of Might & Magic III engine, giving it new and extended possibilities.

+ 3 - 2
include/vcmi/Creature.h

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

+ 0 - 4
include/vcmi/FactionMember.h

@@ -44,10 +44,6 @@ public:
 	 Returns defence of creature or hero.
 	*/
 	virtual int getDefense(bool ranged) const;
-	/**
-	 Returns primskill of creature or hero.
-	*/
-	int getPrimSkillLevel(PrimarySkill id) const;
 	/**
 	 Returns morale of creature or hero. Taking absolute bonuses into account.
 	 For now, uses range from EGameSettings

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

@@ -91,6 +91,7 @@
 	<launchable type="desktop-id">vcmilauncher.desktop</launchable>
 	<releases>
 		<release version="1.7.0" date="2025-05-30" type="development"/>
+		<release version="1.6.3" date="2025-01-10" type="stable"/>
 		<release version="1.6.2" date="2025-01-03" type="stable"/>
 		<release version="1.6.1" date="2024-12-27" type="stable"/>
 		<release version="1.6.0" date="2024-12-20" type="stable"/>

+ 13 - 9
launcher/translation/german.ts

@@ -74,7 +74,7 @@
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="227"/>
         <source>Configuration files directory</source>
-        <translation>Verzeichnis der Konfiguarions-Dateien</translation>
+        <translation>Verzeichnis der Konfigurationsdateien</translation>
     </message>
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="290"/>
@@ -492,7 +492,7 @@ Installation erfolgreich heruntergeladen?</translation>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="996"/>
         <source>Handle back as right mouse button</source>
-        <translation type="unfinished"></translation>
+        <translation>Behandle &quot;Zurück&quot; als rechte Maustaste</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="1102"/>
@@ -823,7 +823,7 @@ Exklusiver Vollbildmodus - das Spiel nimmt den gesamten Bildschirm ein und verwe
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="144"/>
         <source>Heroes Chronicles %1 - %2</source>
-        <translation type="unfinished">Heroes Chronicles %1 - %2</translation>
+        <translation>Heroes Chronicles %1 - %2</translation>
     </message>
 </context>
 <context>
@@ -1163,11 +1163,13 @@ Fehlerursache: </translation>
 Exe (%n bytes):
 %1</source>
         <comment>param is hash</comment>
-        <translation type="unfinished">
+        <translation>
+            <numerusform>SHA1-Hash der bereitgestellten Dateien:
+Exe (%n Bytes):
+%1</numerusform>
             <numerusform>SHA1-Hash der bereitgestellten Dateien:
 Exe (%n Bytes):
 %1</numerusform>
-            <numerusform></numerusform>
         </translation>
     </message>
     <message numerus="yes">
@@ -1176,11 +1178,13 @@ Exe (%n Bytes):
 Bin (%n bytes):
 %1</source>
         <comment>param is hash</comment>
-        <translation type="unfinished">
+        <translation>
+            <numerusform>
+Bin (%n Bytes):
+%1</numerusform>
             <numerusform>
 Bin (%n Bytes):
 %1</numerusform>
-            <numerusform></numerusform>
         </translation>
     </message>
     <message>
@@ -1357,7 +1361,7 @@ Bin (%n Bytes):
     <message>
         <location filename="../mainwindow_moc.cpp" line="46"/>
         <source>Error starting executable</source>
-        <translation type="unfinished">Fehler beim Starten der ausführbaren Datei</translation>
+        <translation>Fehler beim Starten der ausführbaren Datei</translation>
     </message>
     <message>
         <location filename="../mainwindow_moc.cpp" line="287"/>
@@ -1454,7 +1458,7 @@ Bin (%n Bytes):
     <message>
         <location filename="../modManager/modstatecontroller.cpp" line="248"/>
         <source>Mod data was not found</source>
-        <translation type="unfinished"></translation>
+        <translation>Mod-Daten wurden nicht gefunden</translation>
     </message>
     <message>
         <location filename="../modManager/modstatecontroller.cpp" line="252"/>

+ 3 - 3
launcher/translation/polish.ts

@@ -492,7 +492,7 @@ Zainstalować pomyślnie pobrane?</translation>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="996"/>
         <source>Handle back as right mouse button</source>
-        <translation type="unfinished"></translation>
+        <translation>Przycisk wstecz jako prawy przycisk myszy</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="1102"/>
@@ -1360,7 +1360,7 @@ Bin (%n bajtów):
     <message>
         <location filename="../mainwindow_moc.cpp" line="46"/>
         <source>Error starting executable</source>
-        <translation type="unfinished">Błąd podczas uruchamiania pliku wykonywalnego</translation>
+        <translation>Błąd podczas uruchamiania pliku wykonywalnego</translation>
     </message>
     <message>
         <location filename="../mainwindow_moc.cpp" line="287"/>
@@ -1457,7 +1457,7 @@ Bin (%n bajtów):
     <message>
         <location filename="../modManager/modstatecontroller.cpp" line="248"/>
         <source>Mod data was not found</source>
-        <translation type="unfinished"></translation>
+        <translation>Nie znaleziono danych moda</translation>
     </message>
     <message>
         <location filename="../modManager/modstatecontroller.cpp" line="252"/>

+ 13 - 8
lib/BasicTypes.cpp

@@ -69,14 +69,6 @@ int AFactionMember::getMaxDamage(bool ranged) const
 	return getBonusBearer()->valOfBonuses(selector, cachingStr);
 }
 
-int AFactionMember::getPrimSkillLevel(PrimarySkill id) const
-{
-	auto allSkills = getBonusBearer()->getBonusesOfType(BonusType::PRIMARY_SKILL);
-	int ret = allSkills->valOfBonuses(Selector::subtype()(BonusSubtypeID(id)));
-	int minSkillValue = VLC->engineSettings()->getVectorValue(EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS, id.getNum());
-	return std::max(ret, minSkillValue); //otherwise, some artifacts may cause negative skill value effect, sp=0 works in old saves
-}
-
 int AFactionMember::moraleValAndBonusList(TConstBonusListPtr & bonusList) const
 {
 	int32_t maxGoodMorale = VLC->engineSettings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE).size();
@@ -160,6 +152,19 @@ ui32 ACreature::getMovementRange() const
 	return getBonusBearer()->valOfBonuses(BonusType::STACKS_SPEED);
 }
 
+int32_t ACreature::getInitiative(int turn) const
+{
+	if (turn == 0)
+	{
+		return getBonusBearer()->valOfBonuses(BonusType::STACKS_SPEED);
+	}
+	else
+	{
+		const std::string cachingStrSS = "type_STACKS_SPEED_turns_" + std::to_string(turn);
+		return getBonusBearer()->valOfBonuses(Selector::type()(BonusType::STACKS_SPEED).And(Selector::turns(turn)), cachingStrSS);
+	}
+}
+
 ui32 ACreature::getMovementRange(int turn) const
 {
 	if (turn == 0)

+ 2 - 2
lib/CMakeLists.txt

@@ -65,12 +65,12 @@ set(lib_MAIN_SRCS
 	battle/Unit.cpp
 
 	bonuses/Bonus.cpp
+	bonuses/BonusCache.cpp
 	bonuses/BonusEnum.cpp
 	bonuses/BonusList.cpp
 	bonuses/BonusParams.cpp
 	bonuses/BonusSelector.cpp
 	bonuses/BonusCustomTypes.cpp
-	bonuses/CBonusProxy.cpp
 	bonuses/CBonusSystemNode.cpp
 	bonuses/IBonusBearer.cpp
 	bonuses/Limiters.cpp
@@ -435,12 +435,12 @@ set(lib_MAIN_HEADERS
 	battle/Unit.h
 
 	bonuses/Bonus.h
+	bonuses/BonusCache.h
 	bonuses/BonusEnum.h
 	bonuses/BonusList.h
 	bonuses/BonusParams.h
 	bonuses/BonusSelector.h
 	bonuses/BonusCustomTypes.h
-	bonuses/CBonusProxy.h
 	bonuses/CBonusSystemNode.h
 	bonuses/IBonusBearer.h
 	bonuses/Limiters.h

+ 2 - 0
lib/GameSettings.cpp

@@ -76,6 +76,8 @@ const std::vector<GameSettings::SettingOption> GameSettings::settingProperties =
 		{EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS,             "heroes",    "retreatOnWinWithoutTroops"            },
 		{EGameSettings::HEROES_STARTING_STACKS_CHANCES,                   "heroes",    "startingStackChances"                 },
 		{EGameSettings::HEROES_TAVERN_INVITE,                             "heroes",    "tavernInvite"                         },
+		{EGameSettings::HEROES_MOVEMENT_POINTS_LAND,                      "heroes",    "movementPointsLand"                   },
+		{EGameSettings::HEROES_MOVEMENT_POINTS_SEA,                       "heroes",    "movementPointsSea"                    },
 		{EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE,                     "mapFormat", "armageddonsBlade"                     },
 		{EGameSettings::MAP_FORMAT_CHRONICLES,                            "mapFormat", "chronicles"                           },
 		{EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS,                     "mapFormat", "hornOfTheAbyss"                       },

+ 2 - 0
lib/IGameSettings.h

@@ -49,6 +49,8 @@ enum class EGameSettings
 	HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS,
 	HEROES_STARTING_STACKS_CHANCES,
 	HEROES_TAVERN_INVITE,
+	HEROES_MOVEMENT_POINTS_LAND,
+	HEROES_MOVEMENT_POINTS_SEA,
 	MAP_FORMAT_ARMAGEDDONS_BLADE,
 	MAP_FORMAT_CHRONICLES,
 	MAP_FORMAT_HORN_OF_THE_ABYSS,

+ 0 - 30
lib/TerrainHandler.cpp

@@ -151,36 +151,6 @@ std::vector<JsonNode> TerrainTypeHandler::loadLegacyData()
 	return result;
 }
 
-bool TerrainType::isLand() const
-{
-	return !isWater();
-}
-
-bool TerrainType::isWater() const
-{
-	return passabilityType & PassabilityType::WATER;
-}
-
-bool TerrainType::isRock() const
-{
-	return passabilityType & PassabilityType::ROCK;
-}
-
-bool TerrainType::isPassable() const
-{
-	return !isRock();
-}
-
-bool TerrainType::isSurface() const
-{
-	return passabilityType & PassabilityType::SURFACE;
-}
-
-bool TerrainType::isUnderground() const
-{
-	return passabilityType & PassabilityType::SUBTERRANEAN;
-}
-
 bool TerrainType::isTransitionRequired() const
 {
 	return transitionRequired;

+ 36 - 7
lib/TerrainHandler.h

@@ -83,14 +83,13 @@ public:
 
 	TerrainType() = default;
 
-	bool isLand() const;
-	bool isWater() const;
-	bool isRock() const;
+	inline bool isLand() const;
+	inline bool isWater() const;
+	inline bool isRock() const;
+	inline bool isPassable() const;
+	inline bool isSurface() const;
+	inline bool isUnderground() const;
 
-	bool isPassable() const;
-
-	bool isSurface() const;
-	bool isUnderground() const;
 	bool isTransitionRequired() const;
 };
 
@@ -112,4 +111,34 @@ public:
 	std::vector<JsonNode> loadLegacyData() override;
 };
 
+inline bool TerrainType::isLand() const
+{
+	return !isWater();
+}
+
+inline bool TerrainType::isWater() const
+{
+	return passabilityType & PassabilityType::WATER;
+}
+
+inline bool TerrainType::isRock() const
+{
+	return passabilityType & PassabilityType::ROCK;
+}
+
+inline bool TerrainType::isPassable() const
+{
+	return !isRock();
+}
+
+inline bool TerrainType::isSurface() const
+{
+	return passabilityType & PassabilityType::SURFACE;
+}
+
+inline bool TerrainType::isUnderground() const
+{
+	return passabilityType & PassabilityType::SUBTERRANEAN;
+}
+
 VCMI_LIB_NAMESPACE_END

+ 1 - 15
lib/battle/CBattleInfoCallback.cpp

@@ -714,18 +714,7 @@ bool CBattleInfoCallback::battleCanShoot(const battle::Unit * attacker) const
 	if (!attacker->canShoot())
 		return false;
 
-	//forgetfulness
-	TConstBonusListPtr forgetfulList = attacker->getBonusesOfType(BonusType::FORGETFULL);
-	if(!forgetfulList->empty())
-	{
-		int forgetful = forgetfulList->totalValue();
-
-		//advanced+ level
-		if(forgetful > 1)
-			return false;
-	}
-
-	return !battleIsUnitBlocked(attacker) || attacker->hasBonusOfType(BonusType::FREE_SHOOTING);
+	return attacker->canShootBlocked() || !battleIsUnitBlocked(attacker);
 }
 
 bool CBattleInfoCallback::battleCanTargetEmptyHex(const battle::Unit * attacker) const
@@ -1732,9 +1721,6 @@ bool CBattleInfoCallback::battleIsUnitBlocked(const battle::Unit * unit) const
 {
 	RETURN_IF_NOT_BATTLE(false);
 
-	if(unit->hasBonusOfType(BonusType::SIEGE_WEAPON)) //siege weapons cannot be blocked
-		return false;
-
 	for(const auto * adjacent : battleAdjacentUnits(unit))
 	{
 		if(adjacent->unitOwner() != unit->unitOwner()) //blocked by enemy stack

+ 1 - 1
lib/battle/CBattleInfoEssentials.cpp

@@ -404,7 +404,7 @@ PlayerColor CBattleInfoEssentials::battleGetOwner(const battle::Unit * unit) con
 
 	PlayerColor initialOwner = getBattle()->getSidePlayer(unit->unitSide());
 
-	if(unit->hasBonusOfType(BonusType::HYPNOTIZED))
+	if(unit->isHypnotized())
 		return otherPlayer(initialOwner);
 	else
 		return initialOwner;

+ 77 - 53
lib/battle/CUnitState.cpp

@@ -26,7 +26,7 @@ namespace battle
 CAmmo::CAmmo(const battle::Unit * Owner, CSelector totalSelector):
 	used(0),
 	owner(Owner),
-	totalProxy(Owner, std::move(totalSelector))
+	totalProxy(Owner, totalSelector)
 {
 	reset();
 }
@@ -34,7 +34,6 @@ CAmmo::CAmmo(const battle::Unit * Owner, CSelector totalSelector):
 CAmmo & CAmmo::operator= (const CAmmo & other)
 {
 	used = other.used;
-	totalProxy = other.totalProxy;
 	return *this;
 }
 
@@ -60,7 +59,7 @@ void CAmmo::reset()
 
 int32_t CAmmo::total() const
 {
-	return totalProxy->totalValue();
+	return totalProxy.getValue();
 }
 
 void CAmmo::use(int32_t amount)
@@ -85,20 +84,13 @@ void CAmmo::serializeJson(JsonSerializeFormat & handler)
 ///CShots
 CShots::CShots(const battle::Unit * Owner)
 	: CAmmo(Owner, Selector::type()(BonusType::SHOTS)),
-	shooter(Owner, BonusType::SHOOTER)
+	shooter(Owner, Selector::type()(BonusType::SHOOTER))
 {
 }
 
-CShots & CShots::operator=(const CShots & other)
-{
-	CAmmo::operator=(other);
-	shooter = other.shooter;
-	return *this;
-}
-
 bool CShots::isLimited() const
 {
-	return !shooter.getHasBonus() || !env->unitHasAmmoCart(owner);
+	return !shooter.hasBonus() || !env->unitHasAmmoCart(owner);
 }
 
 void CShots::setEnv(const IUnitEnvironment * env_)
@@ -108,7 +100,7 @@ void CShots::setEnv(const IUnitEnvironment * env_)
 
 int32_t CShots::total() const
 {
-	if(shooter.getHasBonus())
+	if(shooter.hasBonus())
 		return CAmmo::total();
 	else
 		return 0;
@@ -124,23 +116,23 @@ CCasts::CCasts(const battle::Unit * Owner):
 CRetaliations::CRetaliations(const battle::Unit * Owner)
 	: CAmmo(Owner, Selector::type()(BonusType::ADDITIONAL_RETALIATION)),
 	totalCache(0),
-	noRetaliation(Owner, Selector::type()(BonusType::SIEGE_WEAPON).Or(Selector::type()(BonusType::HYPNOTIZED)).Or(Selector::type()(BonusType::NO_RETALIATION)), "CRetaliations::noRetaliation"),
-	unlimited(Owner, BonusType::UNLIMITED_RETALIATIONS)
+	noRetaliation(Owner, Selector::type()(BonusType::SIEGE_WEAPON).Or(Selector::type()(BonusType::HYPNOTIZED)).Or(Selector::type()(BonusType::NO_RETALIATION))),
+	unlimited(Owner, Selector::type()(BonusType::UNLIMITED_RETALIATIONS))
 {
 }
 
 bool CRetaliations::isLimited() const
 {
-	return !unlimited.getHasBonus() || noRetaliation.getHasBonus();
+	return !unlimited.hasBonus() || noRetaliation.hasBonus();
 }
 
 int32_t CRetaliations::total() const
 {
-	if(noRetaliation.getHasBonus())
+	if(noRetaliation.hasBonus())
 		return 0;
 
 	//after dispel bonus should remain during current round
-	int32_t val = 1 + totalProxy->totalValue();
+	int32_t val = 1 + totalProxy.getValue();
 	vstd::amax(totalCache, val);
 	return totalCache;
 }
@@ -341,13 +333,9 @@ CUnitState::CUnitState():
 	counterAttacks(this),
 	health(this),
 	shots(this),
-	totalAttacks(this, Selector::type()(BonusType::ADDITIONAL_ATTACK), 1),
-	minDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin)), 0),
-	maxDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax)), 0),
-	attack(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK)), 0),
-	defence(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE)), 0),
-	inFrenzy(this, Selector::type()(BonusType::IN_FRENZY)),
-	cloneLifetimeMarker(this, Selector::type()(BonusType::NONE).And(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(SpellID(SpellID::CLONE)))), "CUnitState::cloneLifetimeMarker"),
+	stackSpeedPerTurn(this, Selector::type()(BonusType::STACKS_SPEED), BonusCacheMode::VALUE),
+	immobilizedPerTurn(this, Selector::type()(BonusType::SIEGE_WEAPON).Or(Selector::type()(BonusType::BIND_EFFECT)), BonusCacheMode::PRESENCE),
+	bonusCache(this),
 	cloneID(-1)
 {
 
@@ -372,15 +360,8 @@ CUnitState & CUnitState::operator=(const CUnitState & other)
 	waitedThisTurn = other.waitedThisTurn;
 	casts = other.casts;
 	counterAttacks = other.counterAttacks;
-	health = other.health;
 	shots = other.shots;
-	totalAttacks = other.totalAttacks;
-	minDamage = other.minDamage;
-	maxDamage = other.maxDamage;
-	attack = other.attack;
-	defence = other.defence;
-	inFrenzy = other.inFrenzy;
-	cloneLifetimeMarker = other.cloneLifetimeMarker;
+	health = other.health;
 	cloneID = other.cloneID;
 	position = other.position;
 	return *this;
@@ -542,9 +523,16 @@ bool CUnitState::isCaster() const
 	return casts.total() > 0;//do not check specific cast abilities here
 }
 
+bool CUnitState::canShootBlocked() const
+{
+	return bonusCache.getBonusValue(UnitBonusValuesProxy::HAS_FREE_SHOOTING);
+}
+
 bool CUnitState::canShoot() const
 {
-	return shots.canUse(1);
+	return
+		shots.canUse(1) &&
+		bonusCache.getBonusValue(UnitBonusValuesProxy::FORGETFULL) <= 1; //advanced+ level
 }
 
 bool CUnitState::isShooter() const
@@ -579,6 +567,11 @@ int64_t CUnitState::getTotalHealth() const
 	return health.total();
 }
 
+uint32_t CUnitState::getMaxHealth() const
+{
+	return std::max(1, bonusCache.getBonusValue(UnitBonusValuesProxy::STACK_HEALTH));
+}
+
 BattleHex CUnitState::getPosition() const
 {
 	return position;
@@ -591,11 +584,20 @@ void CUnitState::setPosition(BattleHex hex)
 
 int32_t CUnitState::getInitiative(int turn) const
 {
-	if (turn == 0)
-		return valOfBonuses(BonusType::STACKS_SPEED);
+	return stackSpeedPerTurn.getValue(turn);
+}
 
-	std::string cachingStr = "type_STACKS_SPEED_turns_" + std::to_string(turn);
-	return valOfBonuses(Selector::type()(BonusType::STACKS_SPEED).And(Selector::turns(turn)), cachingStr);
+ui32 CUnitState::getMovementRange(int turn) const
+{
+	if (immobilizedPerTurn.getValue(0) != 0)
+		return 0;
+
+	return stackSpeedPerTurn.getValue(0);
+}
+
+ui32 CUnitState::getMovementRange() const
+{
+	return getMovementRange(0);
 }
 
 uint8_t CUnitState::getRangedFullDamageDistance() const
@@ -693,47 +695,69 @@ BattlePhases::Type CUnitState::battleQueuePhase(int turn) const
 	}
 }
 
+bool CUnitState::isHypnotized() const
+{
+	return bonusCache.getBonusValue(UnitBonusValuesProxy::HYPNOTIZED);
+}
+
 int CUnitState::getTotalAttacks(bool ranged) const
 {
-	return ranged ? totalAttacks.getRangedValue() : totalAttacks.getMeleeValue();
+	return 1 + (ranged ?
+		bonusCache.getBonusValue(UnitBonusValuesProxy::TOTAL_ATTACKS_RANGED):
+		bonusCache.getBonusValue(UnitBonusValuesProxy::TOTAL_ATTACKS_MELEE));
 }
 
 int CUnitState::getMinDamage(bool ranged) const
 {
-	return ranged ? minDamage.getRangedValue() : minDamage.getMeleeValue();
+	return ranged ?
+		bonusCache.getBonusValue(UnitBonusValuesProxy::MIN_DAMAGE_RANGED):
+		bonusCache.getBonusValue(UnitBonusValuesProxy::MIN_DAMAGE_MELEE);
+
 }
 
 int CUnitState::getMaxDamage(bool ranged) const
 {
-	return ranged ? maxDamage.getRangedValue() : maxDamage.getMeleeValue();
+	return ranged ?
+		bonusCache.getBonusValue(UnitBonusValuesProxy::MAX_DAMAGE_RANGED):
+		bonusCache.getBonusValue(UnitBonusValuesProxy::MAX_DAMAGE_MELEE);
 }
 
 int CUnitState::getAttack(bool ranged) const
 {
-	int ret = ranged ? attack.getRangedValue() : attack.getMeleeValue();
+	int attack = ranged ?
+		bonusCache.getBonusValue(UnitBonusValuesProxy::ATTACK_RANGED):
+		bonusCache.getBonusValue(UnitBonusValuesProxy::ATTACK_MELEE);
 
-	if(!inFrenzy->empty())
+	int frenzy = bonusCache.getBonusValue(UnitBonusValuesProxy::IN_FRENZY);
+	if(frenzy != 0)
 	{
-		double frenzyPower = static_cast<double>(inFrenzy->totalValue()) / 100;
-		frenzyPower *= static_cast<double>(ranged ? defence.getRangedValue() : defence.getMeleeValue());
-		ret += static_cast<int>(frenzyPower);
+		int defence = ranged ?
+			bonusCache.getBonusValue(UnitBonusValuesProxy::DEFENCE_RANGED):
+			bonusCache.getBonusValue(UnitBonusValuesProxy::DEFENCE_MELEE);
+
+		int frenzyBonus = frenzy * defence / 100;
+		attack += frenzyBonus;
 	}
 
-	vstd::amax(ret, 0);
-	return ret;
+	vstd::amax(attack, 0);
+	return attack;
 }
 
 int CUnitState::getDefense(bool ranged) const
 {
-	if(!inFrenzy->empty())
+	int frenzy = bonusCache.getBonusValue(UnitBonusValuesProxy::IN_FRENZY);
+
+	if(frenzy != 0)
 	{
 		return 0;
 	}
 	else
 	{
-		int ret = ranged ? defence.getRangedValue() : defence.getMeleeValue();
-		vstd::amax(ret, 0);
-		return ret;
+		int defence = ranged ?
+						  bonusCache.getBonusValue(UnitBonusValuesProxy::DEFENCE_RANGED):
+						  bonusCache.getBonusValue(UnitBonusValuesProxy::DEFENCE_MELEE);
+		vstd::amax(defence, 0);
+		return defence;
 	}
 }
 
@@ -886,7 +910,7 @@ void CUnitState::afterNewRound()
 
 	if(alive() && isClone())
 	{
-		if(!cloneLifetimeMarker.getHasBonus())
+		if(!bonusCache.hasBonus(UnitBonusValuesProxy::CLONE_MARKER))
 			makeGhost();
 	}
 }

+ 20 - 30
lib/battle/CUnitState.h

@@ -11,7 +11,7 @@
 #pragma once
 
 #include "Unit.h"
-#include "../bonuses/CBonusProxy.h"
+#include "../bonuses/BonusCache.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -32,10 +32,6 @@ class DLL_LINKAGE CAmmo
 public:
 	explicit CAmmo(const battle::Unit * Owner, CSelector totalSelector);
 
-	//only copy construction is allowed for acquire(), serializeJson should be used for any other "assignment"
-	CAmmo(const CAmmo & other) = default;
-	CAmmo(CAmmo && other) = delete;
-
 	CAmmo & operator=(const CAmmo & other);
 	CAmmo & operator=(CAmmo && other) = delete;
 
@@ -50,15 +46,14 @@ public:
 protected:
 	int32_t used;
 	const battle::Unit * owner;
-	CBonusProxy totalProxy;
+	BonusValueCache totalProxy;
 };
 
 class DLL_LINKAGE CShots : public CAmmo
 {
 public:
 	explicit CShots(const battle::Unit * Owner);
-	CShots(const CShots & other) = default;
-	CShots & operator=(const CShots & other);
+
 	bool isLimited() const override;
 	int32_t total() const override;
 
@@ -66,23 +61,20 @@ public:
 private:
 	const IUnitEnvironment * env;
 
-	CCheckProxy shooter;
+	BonusValueCache shooter;
 };
 
 class DLL_LINKAGE CCasts : public CAmmo
 {
 public:
 	explicit CCasts(const battle::Unit * Owner);
-	CCasts(const CCasts & other) = default;
-	CCasts & operator=(const CCasts & other) = default;
 };
 
 class DLL_LINKAGE CRetaliations : public CAmmo
 {
 public:
 	explicit CRetaliations(const battle::Unit * Owner);
-	CRetaliations(const CRetaliations & other) = default;
-	CRetaliations & operator=(const CRetaliations & other) = default;
+
 	bool isLimited() const override;
 	int32_t total() const override;
 	void reset() override;
@@ -91,8 +83,8 @@ public:
 private:
 	mutable int32_t totalCache;
 
-	CCheckProxy noRetaliation;
-	CCheckProxy unlimited;
+	BonusValueCache noRetaliation;
+	BonusValueCache unlimited;
 };
 
 class DLL_LINKAGE CHealth
@@ -154,11 +146,6 @@ public:
 	CHealth health;
 	CShots shots;
 
-	CTotalsProxy totalAttacks;
-
-	CTotalsProxy minDamage;
-	CTotalsProxy maxDamage;
-
 	///id of alive clone of this stack clone if any
 	si32 cloneID;
 
@@ -205,11 +192,14 @@ public:
 	bool isFrozen() const override;
 	bool isValidTarget(bool allowDead = false) const override;
 
+	bool isHypnotized() const override;
+
 	bool isClone() const override;
 	bool hasClone() const override;
 
 	bool canCast() const override;
 	bool isCaster() const override;
+	bool canShootBlocked() const override;
 	bool canShoot() const override;
 	bool isShooter() const override;
 
@@ -218,6 +208,7 @@ public:
 	int32_t getFirstHPleft() const override;
 	int64_t getAvailableHealth() const override;
 	int64_t getTotalHealth() const override;
+	uint32_t getMaxHealth() const override;
 
 	BattleHex getPosition() const override;
 	void setPosition(BattleHex hex) override;
@@ -225,6 +216,9 @@ public:
 	uint8_t getRangedFullDamageDistance() const;
 	uint8_t getShootingRangeDistance() const;
 
+	ui32 getMovementRange(int turn) const override;
+	ui32 getMovementRange() const override;
+
 	bool canMove(int turn = 0) const override;
 	bool defended(int turn = 0) const override;
 	bool moved(int turn = 0) const override;
@@ -268,11 +262,9 @@ public:
 private:
 	const IUnitEnvironment * env;
 
-	CTotalsProxy attack;
-	CTotalsProxy defence;
-	CBonusProxy inFrenzy;
-
-	CCheckProxy cloneLifetimeMarker;
+	BonusCachePerTurn immobilizedPerTurn;
+	BonusCachePerTurn stackSpeedPerTurn;
+	UnitBonusValuesProxy bonusCache;
 
 	void reset();
 };
@@ -282,12 +274,11 @@ class DLL_LINKAGE CUnitStateDetached : public CUnitState
 public:
 	explicit CUnitStateDetached(const IUnitInfo * unit_, const IBonusBearer * bonus_);
 
-	TConstBonusListPtr getAllBonuses(const CSelector & selector, const CSelector & limit,
-		const std::string & cachingStr = "") const override;
+	CUnitStateDetached & operator= (const CUnitState & other);
 
-	int64_t getTreeVersion() const override;
+	TConstBonusListPtr getAllBonuses(const CSelector & selector, const CSelector & limit, const std::string & cachingStr = "") const override;
 
-	CUnitStateDetached & operator= (const CUnitState & other);
+	int64_t getTreeVersion() const override;
 
 	uint32_t unitId() const override;
 	BattleSide unitSide() const override;
@@ -297,7 +288,6 @@ public:
 
 	SlotID unitSlot() const override;
 
-
 	int32_t unitBaseAmount() const override;
 
 	void spendMana(ServerCallback * server, const int spellCost) const override;

+ 3 - 2
lib/battle/Unit.h

@@ -84,11 +84,14 @@ public:
 	bool isTurret() const;
 	virtual bool isValidTarget(bool allowDead = false) const = 0; //non-turret non-ghost stacks (can be attacked or be object of magic effect)
 
+	virtual bool isHypnotized() const = 0;
+
 	virtual bool isClone() const = 0;
 	virtual bool hasClone() const = 0;
 
 	virtual bool canCast() const = 0;
 	virtual bool isCaster() const = 0;
+	virtual bool canShootBlocked() const = 0;
 	virtual bool canShoot() const = 0;
 	virtual bool isShooter() const = 0;
 
@@ -112,8 +115,6 @@ public:
 	virtual BattleHex getPosition() const = 0;
 	virtual void setPosition(BattleHex hex) = 0;
 
-	virtual int32_t getInitiative(int turn = 0) const = 0;
-
 	virtual bool canMove(int turn = 0) const = 0; //if stack can move
 	virtual bool defended(int turn = 0) const = 0;
 	virtual bool moved(int turn = 0) const = 0; //if stack was already moved this turn

+ 212 - 0
lib/bonuses/BonusCache.cpp

@@ -0,0 +1,212 @@
+/*
+ * BonusCache.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#include "StdInc.h"
+#include "BonusCache.h"
+#include "IBonusBearer.h"
+
+#include "BonusSelector.h"
+#include "BonusList.h"
+
+#include "../VCMI_Lib.h"
+#include "../IGameSettings.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+int BonusCacheBase::getBonusValueImpl(BonusCacheEntry & currentValue, const CSelector & selector, BonusCacheMode mode) const
+{
+	if (target->getTreeVersion() == currentValue.version)
+	{
+		return currentValue.value;
+	}
+	else
+	{
+		// NOTE: following code theoretically can fail if bonus tree was changed by another thread between two following lines
+		// However, this situation should not be possible - gamestate modification should only happen in single-treaded mode with locked gamestate mutex
+		int newValue;
+
+		if (mode == BonusCacheMode::VALUE)
+			newValue = target->valOfBonuses(selector);
+		else
+			newValue = target->hasBonus(selector);
+		currentValue.value = newValue;
+		currentValue.version = target->getTreeVersion();
+
+		return newValue;
+	}
+}
+
+BonusValueCache::BonusValueCache(const IBonusBearer * target, const CSelector & selector)
+	:BonusCacheBase(target),selector(selector)
+{}
+
+int BonusValueCache::getValue() const
+{
+	return getBonusValueImpl(value, selector, BonusCacheMode::VALUE);
+}
+
+bool BonusValueCache::hasBonus() const
+{
+	return getBonusValueImpl(value, selector, BonusCacheMode::PRESENCE);
+}
+
+MagicSchoolMasteryCache::MagicSchoolMasteryCache(const IBonusBearer * target)
+	:target(target)
+{}
+
+void MagicSchoolMasteryCache::update() const
+{
+	static const CSelector allBonusesSelector = Selector::type()(BonusType::MAGIC_SCHOOL_SKILL);
+	static const std::array schoolsSelector = {
+		Selector::subtype()(SpellSchool::ANY),
+		Selector::subtype()(SpellSchool::AIR),
+		Selector::subtype()(SpellSchool::FIRE),
+		Selector::subtype()(SpellSchool::WATER),
+		Selector::subtype()(SpellSchool::EARTH),
+	};
+
+	auto list = target->getBonuses(allBonusesSelector);
+	for (int i = 0; i < schoolsSelector.size(); ++i)
+		schools[i] = list->valOfBonuses(schoolsSelector[i]);
+
+	version = target->getTreeVersion();
+}
+
+int32_t MagicSchoolMasteryCache::getMastery(const SpellSchool & school) const
+{
+	if (target->getTreeVersion() != version)
+		update();
+	return schools[school.num + 1];
+}
+
+PrimarySkillsCache::PrimarySkillsCache(const IBonusBearer * target)
+	:target(target)
+{}
+
+void PrimarySkillsCache::update() const
+{
+	static const CSelector primarySkillsSelector = Selector::type()(BonusType::PRIMARY_SKILL);
+	static const CSelector attackSelector = Selector::subtype()(PrimarySkill::ATTACK);
+	static const CSelector defenceSelector = Selector::subtype()(PrimarySkill::DEFENSE);
+	static const CSelector spellPowerSelector = Selector::subtype()(PrimarySkill::SPELL_POWER);
+	static const CSelector knowledgeSelector = Selector::subtype()(PrimarySkill::KNOWLEDGE);
+
+	std::array<int, 4> minValues = {
+		VLC->engineSettings()->getVectorValue(EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS, PrimarySkill::ATTACK),
+		VLC->engineSettings()->getVectorValue(EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS, PrimarySkill::DEFENSE),
+		VLC->engineSettings()->getVectorValue(EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS, PrimarySkill::SPELL_POWER),
+		VLC->engineSettings()->getVectorValue(EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS, PrimarySkill::KNOWLEDGE)
+	};
+
+	auto list = target->getBonuses(primarySkillsSelector);
+	skills[PrimarySkill::ATTACK] = std::max(minValues[PrimarySkill::ATTACK], list->valOfBonuses(attackSelector));
+	skills[PrimarySkill::DEFENSE] = std::max(minValues[PrimarySkill::DEFENSE], list->valOfBonuses(defenceSelector));
+	skills[PrimarySkill::SPELL_POWER] = std::max(minValues[PrimarySkill::SPELL_POWER], list->valOfBonuses(spellPowerSelector));
+	skills[PrimarySkill::KNOWLEDGE] = std::max(minValues[PrimarySkill::KNOWLEDGE], list->valOfBonuses(knowledgeSelector));
+
+	version = target->getTreeVersion();
+}
+
+const std::array<std::atomic<int32_t>, 4> & PrimarySkillsCache::getSkills() const
+{
+	if (target->getTreeVersion() != version)
+		update();
+	return skills;
+}
+
+int BonusCachePerTurn::getValueUncached(int turns) const
+{
+	std::lock_guard lock(bonusListMutex);
+
+	int nodeTreeVersion = target->getTreeVersion();
+
+	if (bonusListVersion != nodeTreeVersion)
+	{
+		bonusList = target->getBonuses(selector);
+		bonusListVersion = nodeTreeVersion;
+	}
+
+	if (mode == BonusCacheMode::VALUE)
+	{
+		if (turns != 0)
+			return bonusList->valOfBonuses(Selector::turns(turns));
+		else
+			return bonusList->totalValue();
+	}
+	else
+	{
+		if (turns != 0)
+			return bonusList->getFirst(Selector::turns(turns)) != nullptr;
+		else
+			return !bonusList->empty();
+	}
+}
+
+int BonusCachePerTurn::getValue(int turns) const
+{
+	int nodeTreeVersion = target->getTreeVersion();
+
+	if (turns < cachedTurns)
+	{
+		auto & entry = cache[turns];
+		if (entry.version == nodeTreeVersion)
+		{
+			// best case: value is in cache and up-to-date
+			return entry.value;
+		}
+		else
+		{
+			// else - compute value and update it in the cache
+			int newValue = getValueUncached(turns);
+			entry.value = newValue;
+			entry.version = nodeTreeVersion;
+			return newValue;
+		}
+	}
+	else
+	{
+		// non-cacheable value - compute and return (should be 0 / close to 0 calls)
+		return getValueUncached(turns);
+	}
+}
+
+const UnitBonusValuesProxy::SelectorsArray * UnitBonusValuesProxy::generateSelectors()
+{
+	static const CSelector additionalAttack = Selector::type()(BonusType::ADDITIONAL_ATTACK);
+	static const CSelector selectorMelee = Selector::effectRange()(BonusLimitEffect::NO_LIMIT).Or(Selector::effectRange()(BonusLimitEffect::ONLY_MELEE_FIGHT));
+	static const CSelector selectorRanged = Selector::effectRange()(BonusLimitEffect::NO_LIMIT).Or(Selector::effectRange()(BonusLimitEffect::ONLY_DISTANCE_FIGHT));
+	static const CSelector minDamage = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin));
+	static const CSelector maxDamage = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax));
+	static const CSelector attack = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK));
+	static const CSelector defence = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE));
+
+	static const UnitBonusValuesProxy::SelectorsArray selectors = {
+		additionalAttack.And(selectorMelee), //TOTAL_ATTACKS_MELEE,
+		additionalAttack.And(selectorRanged), //TOTAL_ATTACKS_RANGED,
+		minDamage.And(selectorMelee), //MIN_DAMAGE_MELEE,
+		minDamage.And(selectorRanged), //MIN_DAMAGE_RANGED,
+		maxDamage.And(selectorMelee), //MAX_DAMAGE_MELEE,
+		maxDamage.And(selectorRanged), //MAX_DAMAGE_RANGED,
+		attack.And(selectorMelee),//ATTACK_MELEE,
+		attack.And(selectorRanged),//ATTACK_RANGED,
+		defence.And(selectorMelee),//DEFENCE_MELEE,
+		defence.And(selectorRanged),//DEFENCE_RANGED,
+		Selector::type()(BonusType::IN_FRENZY),//IN_FRENZY,
+		Selector::type()(BonusType::HYPNOTIZED),//HYPNOTIZED,
+		Selector::type()(BonusType::FORGETFULL),//FORGETFULL,
+		Selector::type()(BonusType::FREE_SHOOTING).Or(Selector::type()(BonusType::SIEGE_WEAPON)),//HAS_FREE_SHOOTING,
+		Selector::type()(BonusType::STACK_HEALTH),//STACK_HEALTH,
+		Selector::type()(BonusType::NONE).And(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(SpellID(SpellID::CLONE))))
+	};
+
+	return &selectors;
+}
+
+VCMI_LIB_NAMESPACE_END

+ 201 - 0
lib/bonuses/BonusCache.h

@@ -0,0 +1,201 @@
+/*
+ * BonusCache.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 "BonusSelector.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+enum class BonusCacheMode : int8_t
+{
+	VALUE, // total value of bonus will be cached
+	PRESENCE, // presence of bonus will be cached
+};
+
+/// Internal base class with no own cache
+class BonusCacheBase
+{
+protected:
+	const IBonusBearer * target;
+
+	explicit BonusCacheBase(const IBonusBearer * target):
+		target(target)
+	{}
+
+	struct BonusCacheEntry
+	{
+		std::atomic<int64_t> version = 0;
+		std::atomic<int64_t> value = 0;
+
+		BonusCacheEntry() = default;
+		BonusCacheEntry(const BonusCacheEntry & other)
+			: version(other.version.load())
+			, value(other.value.load())
+		{
+		}
+		BonusCacheEntry & operator =(const BonusCacheEntry & other)
+		{
+			version = other.version.load();
+			value = other.value.load();
+			return *this;
+		}
+	};
+
+	int getBonusValueImpl(BonusCacheEntry & currentValue, const CSelector & selector, BonusCacheMode) const;
+};
+
+/// Cache that tracks a single query to bonus system
+class BonusValueCache : public BonusCacheBase
+{
+	CSelector selector;
+	mutable BonusCacheEntry value;
+public:
+	BonusValueCache(const IBonusBearer * target, const CSelector & selector);
+	int getValue() const;
+	bool hasBonus() const;
+};
+
+/// Cache that can track a list of queries to bonus system
+template<size_t SIZE>
+class BonusValuesArrayCache : public BonusCacheBase
+{
+public:
+	using SelectorsArray = std::array<const CSelector, SIZE>;
+
+	BonusValuesArrayCache(const IBonusBearer * target, const SelectorsArray * selectors)
+		: BonusCacheBase(target)
+		, selectors(selectors)
+	{}
+
+	int getBonusValue(int index) const
+	{
+		return getBonusValueImpl(cache[index], (*selectors)[index], BonusCacheMode::VALUE);
+	}
+
+	int hasBonus(int index) const
+	{
+		return getBonusValueImpl(cache[index], (*selectors)[index], BonusCacheMode::PRESENCE);
+	}
+
+private:
+	using CacheArray = std::array<BonusCacheEntry, SIZE>;
+
+	const SelectorsArray * selectors;
+	mutable CacheArray cache;
+};
+
+class UnitBonusValuesProxy
+{
+public:
+	enum ECacheKeys : int8_t
+	{
+		TOTAL_ATTACKS_MELEE,
+		TOTAL_ATTACKS_RANGED,
+
+		MIN_DAMAGE_MELEE,
+		MIN_DAMAGE_RANGED,
+		MAX_DAMAGE_MELEE,
+		MAX_DAMAGE_RANGED,
+
+		ATTACK_MELEE,
+		ATTACK_RANGED,
+
+		DEFENCE_MELEE,
+		DEFENCE_RANGED,
+
+		IN_FRENZY,
+		HYPNOTIZED,
+		FORGETFULL,
+		HAS_FREE_SHOOTING,
+		STACK_HEALTH,
+
+		CLONE_MARKER,
+
+		TOTAL_KEYS,
+	};
+	static constexpr size_t KEYS_COUNT = static_cast<size_t>(ECacheKeys::TOTAL_KEYS);
+
+	using SelectorsArray = BonusValuesArrayCache<KEYS_COUNT>::SelectorsArray;
+
+	UnitBonusValuesProxy(const IBonusBearer * Target):
+		cache(Target, generateSelectors())
+	{}
+
+	int getBonusValue(ECacheKeys which) const
+	{
+		auto index = static_cast<size_t>(which);
+		return cache.getBonusValue(index);
+	}
+
+	int hasBonus(ECacheKeys which) const
+	{
+		auto index = static_cast<size_t>(which);
+		return cache.hasBonus(index);
+	}
+
+private:
+	const SelectorsArray * generateSelectors();
+
+	BonusValuesArrayCache<KEYS_COUNT> cache;
+};
+
+/// Cache that tracks values of primary skill values in bonus system
+class PrimarySkillsCache
+{
+	const IBonusBearer * target;
+	mutable std::atomic<int64_t> version = 0;
+	mutable std::array<std::atomic<int32_t>, 4> skills;
+
+	void update() const;
+public:
+	PrimarySkillsCache(const IBonusBearer * target);
+
+	const std::array<std::atomic<int32_t>, 4> & getSkills() const;
+};
+
+/// Cache that tracks values of spell school mastery in bonus system
+class MagicSchoolMasteryCache
+{
+	const IBonusBearer * target;
+	mutable std::atomic<int64_t> version = 0;
+	mutable std::array<std::atomic<int32_t>, 4+1> schools;
+
+	void update() const;
+public:
+	MagicSchoolMasteryCache(const IBonusBearer * target);
+
+	int32_t getMastery(const SpellSchool & school) const;
+};
+
+/// Cache that tracks values for different values of bonus duration
+class BonusCachePerTurn : public BonusCacheBase
+{
+	static constexpr int cachedTurns = 8;
+
+	const CSelector selector;
+	mutable TConstBonusListPtr bonusList;
+	mutable std::mutex bonusListMutex;
+	mutable std::atomic<int64_t> bonusListVersion = 0;
+	mutable std::array<BonusCacheEntry, cachedTurns> cache;
+	const BonusCacheMode mode;
+
+	int getValueUncached(int turns) const;
+public:
+	BonusCachePerTurn(const IBonusBearer * target, const CSelector & selector, BonusCacheMode mode)
+		: BonusCacheBase(target)
+		, selector(selector)
+		, mode(mode)
+	{}
+
+	int getValue(int turns) const;
+};
+
+VCMI_LIB_NAMESPACE_END

+ 5 - 4
lib/bonuses/BonusList.cpp

@@ -83,10 +83,10 @@ void BonusList::stackBonuses()
 	}
 }
 
-int BonusList::totalValue() const
+int BonusList::totalValue(int baseValue) const
 {
 	if (bonuses.empty())
-		return 0;
+		return baseValue;
 
 	struct BonusCollection
 	{
@@ -104,6 +104,7 @@ int BonusList::totalValue() const
 	};
 
 	BonusCollection accumulated;
+	accumulated.base = baseValue;
 	int indexMaxCount = 0;
 	int indexMinCount = 0;
 
@@ -208,12 +209,12 @@ void BonusList::getAllBonuses(BonusList &out) const
 		out.push_back(b);
 }
 
-int BonusList::valOfBonuses(const CSelector &select) const
+int BonusList::valOfBonuses(const CSelector &select, int baseValue) const
 {
 	BonusList ret;
 	CSelector limit = nullptr;
 	getBonuses(ret, select, limit);
-	return ret.totalValue();
+	return ret.totalValue(baseValue);
 }
 
 JsonNode BonusList::toJsonNode() const

+ 2 - 2
lib/bonuses/BonusList.h

@@ -58,14 +58,14 @@ public:
 
 	// BonusList functions
 	void stackBonuses();
-	int totalValue() const;
+	int totalValue(int baseValue = 0) const;
 	void getBonuses(BonusList &out, const CSelector &selector, const CSelector &limit = nullptr) const;
 	void getAllBonuses(BonusList &out) const;
 
 	//special find functions
 	std::shared_ptr<Bonus> getFirst(const CSelector &select);
 	std::shared_ptr<const Bonus> getFirst(const CSelector &select) const;
-	int valOfBonuses(const CSelector &select) const;
+	int valOfBonuses(const CSelector &select, int baseValue = 0) const;
 
 	// conversion / output
 	JsonNode toJsonNode() const;

+ 0 - 221
lib/bonuses/CBonusProxy.cpp

@@ -1,221 +0,0 @@
-/*
- * CBonusProxy.cpp, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-
-#include "StdInc.h"
-#include "BonusList.h"
-#include "CBonusProxy.h"
-#include "IBonusBearer.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-///CBonusProxy
-CBonusProxy::CBonusProxy(const IBonusBearer * Target, CSelector Selector):
-	bonusListCachedLast(0),
-	target(Target),
-	selector(std::move(Selector)),
-	currentBonusListIndex(0)
-{
-}
-
-CBonusProxy::CBonusProxy(const CBonusProxy & other):
-	bonusListCachedLast(other.bonusListCachedLast),
-	target(other.target),
-	selector(other.selector),
-	currentBonusListIndex(other.currentBonusListIndex)
-{
-	bonusList[currentBonusListIndex] = other.bonusList[currentBonusListIndex];
-}
-
-CBonusProxy::CBonusProxy(CBonusProxy && other) noexcept:
-	bonusListCachedLast(0),
-	target(other.target),
-	currentBonusListIndex(0)
-{
-	std::swap(bonusListCachedLast, other.bonusListCachedLast);
-	std::swap(selector, other.selector);
-	std::swap(bonusList, other.bonusList);
-	std::swap(currentBonusListIndex, other.currentBonusListIndex);
-}
-
-CBonusProxy & CBonusProxy::operator=(const CBonusProxy & other)
-{
-	boost::lock_guard<boost::mutex> lock(swapGuard);
-
-	selector = other.selector;
-	swapBonusList(other.bonusList[other.currentBonusListIndex]);
-	bonusListCachedLast = other.bonusListCachedLast;
-
-	return *this;
-}
-
-CBonusProxy & CBonusProxy::operator=(CBonusProxy && other) noexcept
-{
-	std::swap(bonusListCachedLast, other.bonusListCachedLast);
-	std::swap(selector, other.selector);
-	std::swap(bonusList, other.bonusList);
-	std::swap(currentBonusListIndex, other.currentBonusListIndex);
-
-	return *this;
-}
-
-void CBonusProxy::swapBonusList(TConstBonusListPtr other) const
-{
-	// The idea here is to avoid changing active bonusList while it can be read by a different thread.
-	// Because such use of shared ptr is not thread safe
-	// So to avoid this we change the second offline instance and swap active index
-	auto newCurrent = 1 - currentBonusListIndex;
-	bonusList[newCurrent] = std::move(other);
-	currentBonusListIndex = newCurrent;
-}
-
-TConstBonusListPtr CBonusProxy::getBonusList() const
-{
-	auto needUpdateBonusList = [&]() -> bool
-	{
-		return target->getTreeVersion() != bonusListCachedLast || !bonusList[currentBonusListIndex];
-	};
-
-	// avoid locking if everything is up-to-date
-	if(needUpdateBonusList())
-	{
-		boost::lock_guard<boost::mutex>lock(swapGuard);
-
-		if(needUpdateBonusList())
-		{
-			//TODO: support limiters
-			swapBonusList(target->getAllBonuses(selector, Selector::all));
-			bonusListCachedLast = target->getTreeVersion();
-		}
-	}
-
-	return bonusList[currentBonusListIndex];
-}
-
-const BonusList * CBonusProxy::operator->() const
-{
-	return getBonusList().get();
-}
-
-CTotalsProxy::CTotalsProxy(const IBonusBearer * Target, CSelector Selector, int InitialValue):
-	CBonusProxy(Target, std::move(Selector)),
-	initialValue(InitialValue),
-	meleeCachedLast(0),
-	meleeValue(0),
-	rangedCachedLast(0),
-	rangedValue(0)
-{
-}
-
-CTotalsProxy::CTotalsProxy(const CTotalsProxy & other)
-	: CBonusProxy(other),
-	initialValue(other.initialValue),
-	meleeCachedLast(other.meleeCachedLast),
-	meleeValue(other.meleeValue),
-	rangedCachedLast(other.rangedCachedLast),
-	rangedValue(other.rangedValue)
-{
-}
-
-int CTotalsProxy::getValue() const
-{
-	const auto treeVersion = target->getTreeVersion();
-
-	if(treeVersion != valueCachedLast)
-	{
-		auto bonuses = getBonusList();
-
-		value = initialValue + bonuses->totalValue();
-		valueCachedLast = treeVersion;
-	}
-	return value;
-}
-
-int CTotalsProxy::getValueAndList(TConstBonusListPtr & outBonusList) const
-{
-	const auto treeVersion = target->getTreeVersion();
-	outBonusList = getBonusList();
-
-	if(treeVersion != valueCachedLast)
-	{
-		value = initialValue + outBonusList->totalValue();
-		valueCachedLast = treeVersion;
-	}
-	return value;
-}
-
-int CTotalsProxy::getMeleeValue() const
-{
-	static const auto limit = Selector::effectRange()(BonusLimitEffect::NO_LIMIT).Or(Selector::effectRange()(BonusLimitEffect::ONLY_MELEE_FIGHT));
-
-	const auto treeVersion = target->getTreeVersion();
-
-	if(treeVersion != meleeCachedLast)
-	{
-		auto bonuses = target->getBonuses(selector, limit);
-		meleeValue = initialValue + bonuses->totalValue();
-		meleeCachedLast = treeVersion;
-	}
-
-	return meleeValue;
-}
-
-int CTotalsProxy::getRangedValue() const
-{
-	static const auto limit = Selector::effectRange()(BonusLimitEffect::NO_LIMIT).Or(Selector::effectRange()(BonusLimitEffect::ONLY_DISTANCE_FIGHT));
-
-	const auto treeVersion = target->getTreeVersion();
-
-	if(treeVersion != rangedCachedLast)
-	{
-		auto bonuses = target->getBonuses(selector, limit);
-		rangedValue = initialValue + bonuses->totalValue();
-		rangedCachedLast = treeVersion;
-	}
-
-	return rangedValue;
-}
-
-///CCheckProxy
-CCheckProxy::CCheckProxy(const IBonusBearer * Target, BonusType bonusType):
-	target(Target),
-	selector(Selector::type()(bonusType)),
-	cachingStr("type_" + std::to_string(static_cast<int>(bonusType))),
-	cachedLast(0),
-	hasBonus(false)
-{
-
-}
-
-CCheckProxy::CCheckProxy(const IBonusBearer * Target, CSelector Selector, const std::string & cachingStr):
-	target(Target),
-	selector(std::move(Selector)),
-	cachedLast(0),
-	cachingStr(cachingStr),
-	hasBonus(false)
-{
-}
-
-//This constructor should be placed here to avoid side effects
-CCheckProxy::CCheckProxy(const CCheckProxy & other) = default;
-
-bool CCheckProxy::getHasBonus() const
-{
-	const auto treeVersion = target->getTreeVersion();
-
-	if(treeVersion != cachedLast)
-	{
-		hasBonus = target->hasBonus(selector, cachingStr);
-		cachedLast = treeVersion;
-	}
-
-	return hasBonus;
-}
-
-VCMI_LIB_NAMESPACE_END

+ 0 - 92
lib/bonuses/CBonusProxy.h

@@ -1,92 +0,0 @@
-/*
- * CBonusProxy.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 "Bonus.h"
-#include "BonusSelector.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-class DLL_LINKAGE CBonusProxy
-{
-public:
-	CBonusProxy(const IBonusBearer * Target, CSelector Selector);
-	CBonusProxy(const CBonusProxy & other);
-	CBonusProxy(CBonusProxy && other) noexcept;
-
-	CBonusProxy & operator=(CBonusProxy && other) noexcept;
-	CBonusProxy & operator=(const CBonusProxy & other);
-	const BonusList * operator->() const;
-	TConstBonusListPtr getBonusList() const;
-
-protected:
-	CSelector selector;
-	const IBonusBearer * target;
-	mutable int64_t bonusListCachedLast;
-	mutable TConstBonusListPtr bonusList[2];
-	mutable int currentBonusListIndex;
-	mutable boost::mutex swapGuard;
-	void swapBonusList(TConstBonusListPtr other) const;
-};
-
-class DLL_LINKAGE CTotalsProxy : public CBonusProxy
-{
-public:
-	CTotalsProxy(const IBonusBearer * Target, CSelector Selector, int InitialValue);
-	CTotalsProxy(const CTotalsProxy & other);
-	CTotalsProxy(CTotalsProxy && other) = delete;
-
-	CTotalsProxy & operator=(const CTotalsProxy & other) = default;
-	CTotalsProxy & operator=(CTotalsProxy && other) = delete;
-
-	int getMeleeValue() const;
-	int getRangedValue() const;
-	int getValue() const;
-	/**
-	Returns total value of all selected bonuses and sets bonusList as a pointer to the list of selected bonuses
-	@param bonusList is the out list of all selected bonuses
-	@return total value of all selected bonuses and 0 otherwise
-	*/
-	int getValueAndList(TConstBonusListPtr & bonusList) const;
-
-private:
-	int initialValue;
-
-	mutable int64_t valueCachedLast = 0;
-	mutable int value = 0;
-
-	mutable int64_t meleeCachedLast;
-	mutable int meleeValue;
-
-	mutable int64_t rangedCachedLast;
-	mutable int rangedValue;
-};
-
-class DLL_LINKAGE CCheckProxy
-{
-public:
-	CCheckProxy(const IBonusBearer * Target, CSelector Selector, const std::string & cachingStr);
-	CCheckProxy(const IBonusBearer * Target, BonusType bonusType);
-	CCheckProxy(const CCheckProxy & other);
-	CCheckProxy& operator= (const CCheckProxy & other) = default;
-
-	bool getHasBonus() const;
-
-private:
-	const IBonusBearer * target;
-	std::string cachingStr;
-	CSelector selector;
-
-	mutable int64_t cachedLast;
-	mutable bool hasBonus;
-};
-
-VCMI_LIB_NAMESPACE_END

+ 1 - 1
lib/bonuses/IBonusBearer.cpp

@@ -105,7 +105,7 @@ bool IBonusBearer::hasBonusOfType(BonusType type, BonusSubtypeID subtype) const
 
 bool IBonusBearer::hasBonusFrom(BonusSource source, BonusSourceID sourceID) const
 {
-	std::string cachingStr = "source_" + std::to_string(static_cast<int>(source)) + "_" + sourceID.toString();
+	std::string cachingStr = "source_" + std::to_string(static_cast<int>(source)) + "_" + std::to_string(sourceID.getNum());
 	return hasBonus(Selector::source(source,sourceID), cachingStr);
 }
 

+ 0 - 12
lib/bonuses/Updaters.cpp

@@ -111,18 +111,6 @@ ArmyMovementUpdater::ArmyMovementUpdater(int base, int divider, int multiplier,
 
 std::shared_ptr<Bonus> ArmyMovementUpdater::createUpdatedBonus(const std::shared_ptr<Bonus> & b, const CBonusSystemNode & context) const
 {
-	if(b->type == BonusType::MOVEMENT && context.getNodeType() == CBonusSystemNode::HERO)
-	{
-		auto speed = static_cast<const CGHeroInstance &>(context).getLowestCreatureSpeed();
-		si32 armySpeed = speed * base / divider;
-		auto counted = armySpeed * multiplier;
-		auto newBonus = std::make_shared<Bonus>(*b);
-		newBonus->source = BonusSource::ARMY;
-		newBonus->val += vstd::amin(counted, max);
-		return newBonus;
-	}
-	if(b->type != BonusType::MOVEMENT)
-		logGlobal->error("ArmyMovementUpdater should only be used for MOVEMENT bonus!");
 	return b;
 }
 

+ 205 - 52
lib/campaign/CampaignHandler.cpp

@@ -171,6 +171,26 @@ void CampaignHandler::readHeaderFromJson(CampaignHeader & ret, JsonNode & reader
 	ret.outroVideo = VideoPath::fromJson(reader["outroVideo"]);
 }
 
+JsonNode CampaignHandler::writeHeaderToJson(CampaignHeader & header)
+{
+	JsonNode node;
+	node["version"].Integer() = static_cast<ui64>(CampaignVersion::VCMI);
+	node["regions"] = CampaignRegions::toJson(header.campaignRegions);
+	node["name"].String() = header.name.toString();
+	node["description"].String() = header.description.toString();
+	node["author"].String() = header.author.toString();
+	node["authorContact"].String() = header.authorContact.toString();
+	node["campaignVersion"].String() = header.campaignVersion.toString();
+	node["creationDateTime"].Integer() = header.creationDateTime;
+	node["allowDifficultySelection"].Bool() = header.difficultyChosenByPlayer;
+	node["music"].String() = header.music.getName();
+	node["loadingBackground"].String() = header.loadingBackground.getName();
+	node["videoRim"].String() = header.videoRim.getName();
+	node["introVideo"].String() = header.introVideo.getName();
+	node["outroVideo"].String() = header.outroVideo.getName();
+	return node;
+}
+
 CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader)
 {
 	auto prologEpilogReader = [](JsonNode & identifier) -> CampaignScenarioPrologEpilog
@@ -203,56 +223,86 @@ CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader)
 	return ret;
 }
 
+JsonNode CampaignHandler::writeScenarioToJson(const CampaignScenario & scenario)
+{
+	auto prologEpilogWriter = [](const CampaignScenarioPrologEpilog & elem) -> JsonNode
+	{
+		JsonNode node;
+		if(elem.hasPrologEpilog)
+		{
+			node["video"].String() = elem.prologVideo.getName();
+			node["music"].String() = elem.prologMusic.getName();
+			node["voice"].String() = elem.prologVoice.getName();
+			node["text"].String() = elem.prologText.toString();
+		}
+		return node;
+	};
+
+	JsonNode node;
+	node["map"].String() = scenario.mapName;
+	for(auto & g : scenario.preconditionRegions)
+		node["preconditions"].Vector().push_back(JsonNode(static_cast<ui32>(g)));
+	node["color"].Integer() = scenario.regionColor;
+	node["difficulty"].Integer() = scenario.difficulty;
+	node["regionText"].String() = scenario.regionText.toString();
+	node["prolog"] = prologEpilogWriter(scenario.prolog);
+	node["epilog"] = prologEpilogWriter(scenario.epilog);
+
+	writeScenarioTravelToJson(node, scenario.travelOptions);
+
+	return node;
+}
+
+static const std::map<std::string, CampaignStartOptions> startOptionsMap = {
+	{"none", CampaignStartOptions::NONE},
+	{"bonus", CampaignStartOptions::START_BONUS},
+	{"crossover", CampaignStartOptions::HERO_CROSSOVER},
+	{"hero", CampaignStartOptions::HERO_OPTIONS}
+};
+
+static const std::map<std::string, CampaignBonusType> bonusTypeMap = {
+	{"spell", CampaignBonusType::SPELL},
+	{"creature", CampaignBonusType::MONSTER},
+	{"building", CampaignBonusType::BUILDING},
+	{"artifact", CampaignBonusType::ARTIFACT},
+	{"scroll", CampaignBonusType::SPELL_SCROLL},
+	{"primarySkill", CampaignBonusType::PRIMARY_SKILL},
+	{"secondarySkill", CampaignBonusType::SECONDARY_SKILL},
+	{"resource", CampaignBonusType::RESOURCE},
+	//{"prevHero", CScenarioTravel::STravelBonus::EBonusType::HEROES_FROM_PREVIOUS_SCENARIO},
+	//{"hero", CScenarioTravel::STravelBonus::EBonusType::HERO},
+};
+
+static const std::map<std::string, ui32> primarySkillsMap = {
+	{"attack", 0},
+	{"defence", 8},
+	{"spellpower", 16},
+	{"knowledge", 24},
+};
+
+static const std::map<std::string, ui16> heroSpecialMap = {
+	{"strongest", 0xFFFD},
+	{"generated", 0xFFFE},
+	{"random", 0xFFFF}
+};
+
+static const std::map<std::string, ui8> resourceTypeMap = {
+	//FD - wood+ore
+	//FE - mercury+sulfur+crystal+gem
+	{"wood", 0},
+	{"mercury", 1},
+	{"ore", 2},
+	{"sulfur", 3},
+	{"crystal", 4},
+	{"gems", 5},
+	{"gold", 6},
+	{"common", 0xFD},
+	{"rare", 0xFE}
+};
+
 CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader)
 {
 	CampaignTravel ret;
-
-	std::map<std::string, CampaignStartOptions> startOptionsMap = {
-		{"none", CampaignStartOptions::NONE},
-		{"bonus", CampaignStartOptions::START_BONUS},
-		{"crossover", CampaignStartOptions::HERO_CROSSOVER},
-		{"hero", CampaignStartOptions::HERO_OPTIONS}
-	};
-	
-	std::map<std::string, CampaignBonusType> bonusTypeMap = {
-		{"spell", CampaignBonusType::SPELL},
-		{"creature", CampaignBonusType::MONSTER},
-		{"building", CampaignBonusType::BUILDING},
-		{"artifact", CampaignBonusType::ARTIFACT},
-		{"scroll", CampaignBonusType::SPELL_SCROLL},
-		{"primarySkill", CampaignBonusType::PRIMARY_SKILL},
-		{"secondarySkill", CampaignBonusType::SECONDARY_SKILL},
-		{"resource", CampaignBonusType::RESOURCE},
-		//{"prevHero", CScenarioTravel::STravelBonus::EBonusType::HEROES_FROM_PREVIOUS_SCENARIO},
-		//{"hero", CScenarioTravel::STravelBonus::EBonusType::HERO},
-	};
-	
-	std::map<std::string, ui32> primarySkillsMap = {
-		{"attack", 0},
-		{"defence", 8},
-		{"spellpower", 16},
-		{"knowledge", 24},
-	};
-	
-	std::map<std::string, ui16> heroSpecialMap = {
-		{"strongest", 0xFFFD},
-		{"generated", 0xFFFE},
-		{"random", 0xFFFF}
-	};
-	
-	std::map<std::string, ui8> resourceTypeMap = {
-		//FD - wood+ore
-		//FE - mercury+sulfur+crystal+gem
-		{"wood", 0},
-		{"mercury", 1},
-		{"ore", 2},
-		{"sulfur", 3},
-		{"crystal", 4},
-		{"gems", 5},
-		{"gold", 6},
-		{"common", 0xFD},
-		{"rare", 0xFE}
-	};
 	
 	for(auto & k : reader["heroKeeps"].Vector())
 	{
@@ -278,7 +328,7 @@ CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader)
 			logGlobal->warn("VCMP Loading: keepArtifacts contains unresolved identifier %s", k.String());
 	}
 
-	ret.startOptions = startOptionsMap[reader["startOptions"].String()];
+	ret.startOptions = startOptionsMap.at(reader["startOptions"].String());
 	switch(ret.startOptions)
 	{
 	case CampaignStartOptions::NONE:
@@ -290,11 +340,11 @@ CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader)
 			for(auto & bjson : reader["bonuses"].Vector())
 			{
 				CampaignBonus bonus;
-				bonus.type = bonusTypeMap[bjson["what"].String()];
+				bonus.type = bonusTypeMap.at(bjson["what"].String());
 				switch (bonus.type)
 				{
 					case CampaignBonusType::RESOURCE:
-						bonus.info1 = resourceTypeMap[bjson["type"].String()];
+						bonus.info1 = resourceTypeMap.at(bjson["type"].String());
 						bonus.info2 = bjson["amount"].Integer();
 						break;
 						
@@ -305,7 +355,7 @@ CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader)
 						break;
 						
 					default:
-						if(int heroId = heroSpecialMap[bjson["hero"].String()])
+						if(int heroId = heroSpecialMap.at(bjson["hero"].String()))
 							bonus.info1 = heroId;
 						else
 							if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", bjson["hero"].String()))
@@ -368,7 +418,7 @@ CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader)
 				bonus.type = CampaignBonusType::HERO;
 				bonus.info1 = bjson["playerColor"].Integer(); //player color
 				
-				if(int heroId = heroSpecialMap[bjson["hero"].String()])
+				if(int heroId = heroSpecialMap.at(bjson["hero"].String()))
 					bonus.info2 = heroId;
 				else
 					if (auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", bjson["hero"].String()))
@@ -390,6 +440,109 @@ CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader)
 	return ret;
 }
 
+void CampaignHandler::writeScenarioTravelToJson(JsonNode & node, const CampaignTravel & travel)
+{
+	if(travel.whatHeroKeeps.experience)
+		node["heroKeeps"].Vector().push_back(JsonNode("experience"));
+	if(travel.whatHeroKeeps.primarySkills)
+		node["heroKeeps"].Vector().push_back(JsonNode("primarySkills"));
+	if(travel.whatHeroKeeps.secondarySkills)
+		node["heroKeeps"].Vector().push_back(JsonNode("secondarySkills"));
+	if(travel.whatHeroKeeps.spells)
+		node["heroKeeps"].Vector().push_back(JsonNode("spells"));
+	if(travel.whatHeroKeeps.artifacts)
+		node["heroKeeps"].Vector().push_back(JsonNode("artifacts"));
+	for(auto & c : travel.monstersKeptByHero)
+		node["keepCreatures"].Vector().push_back(JsonNode(CreatureID::encode(c)));
+	for(auto & a : travel.artifactsKeptByHero)
+		node["keepArtifacts"].Vector().push_back(JsonNode(ArtifactID::encode(a)));
+	node["startOptions"].String() = vstd::reverseMap(startOptionsMap)[travel.startOptions];
+
+	switch(travel.startOptions)
+	{
+	case CampaignStartOptions::NONE:
+		break;
+	case CampaignStartOptions::START_BONUS:
+		{
+			node["playerColor"].String() = PlayerColor::encode(travel.playerColor);
+			for(auto & bonus : travel.bonusesToChoose)
+			{
+				JsonNode bnode;
+				bnode["what"].String() = vstd::reverseMap(bonusTypeMap)[bonus.type];
+				switch (bonus.type)
+				{
+					case CampaignBonusType::RESOURCE:
+						bnode["type"].String() = vstd::reverseMap(resourceTypeMap)[bonus.info1];
+						bnode["amount"].Integer() = bonus.info2;
+						break;
+					case CampaignBonusType::BUILDING:
+						bnode["type"].String() = EBuildingType::names[bonus.info1];
+						break;
+					default:
+						if(vstd::contains(vstd::reverseMap(heroSpecialMap), bonus.info1))
+							bnode["hero"].String() = vstd::reverseMap(heroSpecialMap)[bonus.info1];
+						else
+							bnode["hero"].String() = HeroTypeID::encode(bonus.info1);
+						bnode["amount"].Integer() = bonus.info3;
+						switch(bonus.type)
+						{
+							case CampaignBonusType::SPELL:
+								bnode["type"].String() = SpellID::encode(bonus.info2);
+								break;
+							case CampaignBonusType::MONSTER:
+								bnode["type"].String() = CreatureID::encode(bonus.info2);
+								break;
+							case CampaignBonusType::SECONDARY_SKILL:
+								bnode["type"].String() = SecondarySkill::encode(bonus.info2);
+								break;
+							case CampaignBonusType::ARTIFACT:
+								bnode["type"].String() = ArtifactID::encode(bonus.info2);
+								break;
+							case CampaignBonusType::SPELL_SCROLL:
+								bnode["type"].String() = SpellID::encode(bonus.info2);
+								break;
+							case CampaignBonusType::PRIMARY_SKILL:
+								for(auto & ps : primarySkillsMap)
+									bnode[ps.first].Integer() = (bonus.info2 >> ps.second) & 0xff;
+								break;
+							default:
+								bnode["type"].Integer() = bonus.info2;
+						}
+						break;
+				}
+				node["bonuses"].Vector().push_back(bnode);
+			}
+			break;
+		}
+	case CampaignStartOptions::HERO_CROSSOVER:
+		{
+			for(auto & bonus : travel.bonusesToChoose)
+			{
+				JsonNode bnode;
+				bnode["playerColor"].Integer() = bonus.info1;
+				bnode["scenario"].Integer() = bonus.info2;
+				node["bonuses"].Vector().push_back(bnode);
+			}
+			break;
+		}
+	case CampaignStartOptions::HERO_OPTIONS:
+		{
+			for(auto & bonus : travel.bonusesToChoose)
+			{
+				JsonNode bnode;
+				bnode["playerColor"].Integer() = bonus.info1;
+
+				if(vstd::contains(vstd::reverseMap(heroSpecialMap), bonus.info2))
+					bnode["hero"].String() = vstd::reverseMap(heroSpecialMap)[bonus.info2];
+				else
+					bnode["hero"].String() = HeroTypeID::encode(bonus.info2);
+				
+				node["bonuses"].Vector().push_back(bnode);
+			}
+			break;
+		}
+	}
+}
 
 void CampaignHandler::readHeaderFromMemory( CampaignHeader & ret, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding )
 {

+ 7 - 0
lib/campaign/CampaignHandler.h

@@ -26,6 +26,9 @@ class DLL_LINKAGE CampaignHandler
 	static CampaignScenario readScenarioFromJson(JsonNode & reader);
 	static CampaignTravel readScenarioTravelFromJson(JsonNode & reader);
 
+	//writer for VCMI campaigns (*.vcmp)
+	static void writeScenarioTravelToJson(JsonNode & node, const CampaignTravel & travel);
+
 	//parsers for original H3C campaigns
 	static void readHeaderFromMemory(CampaignHeader & target, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding);
 	static CampaignScenario readScenarioFromMemory(CBinaryReader & reader, CampaignHeader & header);
@@ -43,6 +46,10 @@ public:
 	static std::unique_ptr<Campaign> getHeader( const std::string & name); //name - name of appropriate file
 
 	static std::shared_ptr<CampaignState> getCampaign(const std::string & name); //name - name of appropriate file
+
+	//writer for VCMI campaigns (*.vcmp)
+	static JsonNode writeHeaderToJson(CampaignHeader & header);
+	static JsonNode writeScenarioToJson(const CampaignScenario & scenario);
 };
 
 VCMI_LIB_NAMESPACE_END

+ 35 - 0
lib/campaign/CampaignState.cpp

@@ -45,6 +45,22 @@ CampaignRegions::RegionDescription CampaignRegions::RegionDescription::fromJson(
 	return rd;
 }
 
+JsonNode CampaignRegions::RegionDescription::toJson(CampaignRegions::RegionDescription & rd)
+{
+	JsonNode node;
+	node["infix"].String() = rd.infix;
+	node["x"].Float() = rd.pos.x;
+	node["y"].Float() = rd.pos.y;
+	if(rd.labelPos != std::nullopt)
+	{
+		node["labelPos"]["x"].Float() = (*rd.labelPos).x;
+		node["labelPos"]["y"].Float() = (*rd.labelPos).y;
+	}
+	else
+		node["labelPos"].clear();
+	return node;
+}
+
 CampaignRegions CampaignRegions::fromJson(const JsonNode & node)
 {
 	CampaignRegions cr;
@@ -59,6 +75,25 @@ CampaignRegions CampaignRegions::fromJson(const JsonNode & node)
 	return cr;
 }
 
+JsonNode CampaignRegions::toJson(CampaignRegions cr)
+{
+	JsonNode node;
+	node["prefix"].String() = cr.campPrefix;
+	node["colorSuffixLength"].Float() = cr.colorSuffixLength;
+	if(!cr.campSuffix.size())
+		node["suffix"].clear();
+	else
+		node["suffix"].Vector() = JsonVector{ JsonNode(cr.campSuffix[0]), JsonNode(cr.campSuffix[1]), JsonNode(cr.campSuffix[2]) };
+	if(cr.campBackground.empty())
+		node["background"].clear();
+	else
+		node["background"].String() = cr.campBackground;
+	node["desc"].Vector() = JsonVector();
+	for(auto & region : cr.regions)
+		node["desc"].Vector().push_back(CampaignRegions::RegionDescription::toJson(region));
+	return node;
+}
+
 CampaignRegions CampaignRegions::getLegacy(int campId)
 {
 	static std::vector<CampaignRegions> campDescriptions;

+ 2 - 0
lib/campaign/CampaignState.h

@@ -59,6 +59,7 @@ class DLL_LINKAGE CampaignRegions
 		}
 
 		static CampaignRegions::RegionDescription fromJson(const JsonNode & node);
+		static JsonNode toJson(CampaignRegions::RegionDescription & rd);
 	};
 
 	std::vector<RegionDescription> regions;
@@ -86,6 +87,7 @@ public:
 	}
 
 	static CampaignRegions fromJson(const JsonNode & node);
+	static JsonNode toJson(CampaignRegions cr);
 	static CampaignRegions getLegacy(int campId);
 };
 

+ 8 - 1
lib/constants/EntityIdentifiers.h

@@ -325,7 +325,14 @@ private:
 public:
 	static Type getDwellingFromLevel(int level, int upgradeIndex)
 	{
-		return getDwellings()[upgradeIndex][level];
+		try
+		{
+			return getDwellings().at(upgradeIndex).at(level);
+		}
+		catch (const std::out_of_range & e)
+		{
+			return Type::NONE;
+		}
 	}
 
 	static int getLevelFromDwelling(BuildingIDBase dwelling)

+ 1 - 1
lib/gameState/CGameState.cpp

@@ -566,7 +566,7 @@ void CGameState::placeStartingHeroes()
 				continue;
 
 			HeroTypeID heroTypeId = pickNextHeroType(playerColor);
-			if(playerSettingPair.second.hero == HeroTypeID::NONE)
+			if(playerSettingPair.second.hero == HeroTypeID::NONE || playerSettingPair.second.hero == HeroTypeID::RANDOM)
 				playerSettingPair.second.hero = heroTypeId;
 
 			placeStartingHero(playerColor, HeroTypeID(heroTypeId), playerInfo.posOfMainTown);

+ 19 - 1
lib/json/JsonRandom.cpp

@@ -35,6 +35,21 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
+std::string JsonRandomizationException::cleanupJson(const JsonNode & value)
+{
+	std::string result = value.toCompactString();
+	for (size_t i = 0; i < result.size(); ++i)
+		if (result[i] == '\n')
+			result[i] = ' ';
+
+	return result;
+}
+
+JsonRandomizationException::JsonRandomizationException(const std::string & message, const JsonNode & input)
+	: std::runtime_error(message + " Input was: " + cleanupJson(input))
+{}
+
+
 	si32 JsonRandom::loadVariable(const std::string & variableGroup, const std::string & value, const Variables & variables, si32 defaultValue)
 	{
 		if (value.empty() || value[0] != '@')
@@ -483,7 +498,10 @@ VCMI_LIB_NAMESPACE_BEGIN
 		if (!potentialPicks.empty())
 			pickedCreature = *RandomGeneratorUtil::nextItem(potentialPicks, rng);
 		else
-			logMod->warn("Failed to select suitable random creature!");
+			throw JsonRandomizationException("No potential creatures to pick!", value);
+
+		if (!pickedCreature.hasValue())
+			throw JsonRandomizationException("Invalid creature picked!", value);
 
 		stack.setType(pickedCreature.toCreature());
 		stack.count = loadValue(value, rng, variables);

+ 7 - 0
lib/json/JsonRandom.h

@@ -27,6 +27,13 @@ struct Bonus;
 struct Component;
 class CStackBasicDescriptor;
 
+class JsonRandomizationException : public std::runtime_error
+{
+	std::string cleanupJson(const JsonNode & value);
+public:
+	JsonRandomizationException(const std::string & message, const JsonNode & input);
+};
+
 class JsonRandom : public GameCallbackHolder
 {
 public:

+ 9 - 1
lib/mapObjectConstructors/CRewardableConstructor.cpp

@@ -11,6 +11,7 @@
 #include "CRewardableConstructor.h"
 
 #include "../json/JsonUtils.h"
+#include "../json/JsonRandom.h"
 #include "../mapObjects/CRewardableObject.h"
 #include "../texts/CGeneralTextHandler.h"
 #include "../IGameCallback.h"
@@ -49,7 +50,14 @@ Rewardable::Configuration CRewardableConstructor::generateConfiguration(IGameCal
 {
 	Rewardable::Configuration result;
 	result.variables.preset = presetVariables;
-	objectInfo.configureObject(result, rand, cb);
+
+	try {
+		objectInfo.configureObject(result, rand, cb);
+	}
+	catch (const JsonRandomizationException & e)
+	{
+		throw std::runtime_error("Failed to generate configuration for object '" + getJsonKey() + "'! Reason: " + e.what());
+	}
 
 	for(auto & rewardInfo : result.info)
 	{

+ 2 - 2
lib/mapObjects/CArmedInstance.cpp

@@ -46,7 +46,7 @@ CArmedInstance::CArmedInstance(IGameCallback *cb)
 CArmedInstance::CArmedInstance(IGameCallback *cb, bool isHypothetic):
 	CGObjectInstance(cb),
 	CBonusSystemNode(isHypothetic),
-	nonEvilAlignmentMix(this, BonusType::NONEVIL_ALIGNMENT_MIX), // Take Angelic Alliance troop-mixing freedom of non-evil units into account.
+	nonEvilAlignmentMix(this, Selector::type()(BonusType::NONEVIL_ALIGNMENT_MIX)), // Take Angelic Alliance troop-mixing freedom of non-evil units into account.
 	battle(nullptr)
 {
 }
@@ -86,7 +86,7 @@ void CArmedInstance::updateMoraleBonusFromArmy()
 
 	size_t factionsInArmy = factions.size(); //town garrison seems to take both sets into account
 
-	if (nonEvilAlignmentMix.getHasBonus())
+	if (nonEvilAlignmentMix.hasBonus())
 	{
 		size_t mixableFactions = 0;
 

+ 2 - 3
lib/mapObjects/CArmedInstance.h

@@ -11,8 +11,8 @@
 
 #include "CGObjectInstance.h"
 #include "../CCreatureSet.h"
-#include "../bonuses/CBonusProxy.h"
 #include "../bonuses/CBonusSystemNode.h"
+#include "../bonuses/BonusCache.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -23,8 +23,7 @@ class JsonSerializeFormat;
 class DLL_LINKAGE CArmedInstance: public CGObjectInstance, public CBonusSystemNode, public CCreatureSet, public IConstBonusProvider
 {
 private:
-	CCheckProxy nonEvilAlignmentMix;
-	static CSelector nonEvilAlignmentMixSelector;
+	BonusValueCache nonEvilAlignmentMix;
 
 public:
 	BattleInfo *battle; //set to the current battle, if engaged

+ 41 - 84
lib/mapObjects/CGHeroInstance.cpp

@@ -73,31 +73,6 @@ void CGHeroPlaceholder::serializeJsonOptions(JsonSerializeFormat & handler)
 		handler.serializeInt("powerRank", powerRank.value());
 }
 
-static int lowestSpeed(const CGHeroInstance * chi)
-{
-	static const CSelector selectorSTACKS_SPEED = Selector::type()(BonusType::STACKS_SPEED);
-	static const std::string keySTACKS_SPEED = "type_" + std::to_string(static_cast<si32>(BonusType::STACKS_SPEED));
-
-	if(!chi->stacksCount())
-	{
-		if(chi->commander && chi->commander->alive)
-		{
-			return chi->commander->valOfBonuses(selectorSTACKS_SPEED, keySTACKS_SPEED);
-		}
-
-		logGlobal->error("Hero %d (%s) has no army!", chi->id.getNum(), chi->getNameTranslated());
-		return 20;
-	}
-
-	auto i = chi->Slots().begin();
-	//TODO? should speed modifiers (eg from artifacts) affect hero movement?
-
-	int ret = (i++)->second->valOfBonuses(selectorSTACKS_SPEED, keySTACKS_SPEED);
-	for(; i != chi->Slots().end(); i++)
-		ret = std::min(ret, i->second->valOfBonuses(selectorSTACKS_SPEED, keySTACKS_SPEED));
-	return ret;
-}
-
 ui32 CGHeroInstance::getTileMovementCost(const TerrainTile & dest, const TerrainTile & from, const TurnInfo * ti) const
 {
 	int64_t ret = GameConstants::BASE_MOVEMENT_COST;
@@ -107,13 +82,10 @@ ui32 CGHeroInstance::getTileMovementCost(const TerrainTile & dest, const Terrain
 	{
 		ret = from.getRoad()->movementCost;
 	}
-	else if(ti->nativeTerrain != from.getTerrainID() &&//the terrain is not native
-			ti->nativeTerrain != ETerrainId::ANY_TERRAIN && //no special creature bonus
-			!ti->hasBonusOfType(BonusType::NO_TERRAIN_PENALTY, BonusSubtypeID(from.getTerrainID()))) //no special movement bonus
+	else if(!ti->hasNoTerrainPenalty(from.getTerrainID())) //no special movement bonus
 	{
-
 		ret = VLC->terrainTypeHandler->getById(from.getTerrainID())->moveCost;
-		ret -= ti->valOfBonuses(BonusType::ROUGH_TERRAIN_DISCOUNT);
+		ret -= ti->getRoughTerrainDiscountValue();
 		if(ret < GameConstants::BASE_MOVEMENT_COST)
 			ret = GameConstants::BASE_MOVEMENT_COST;
 	}
@@ -257,30 +229,41 @@ void CGHeroInstance::setMovementPoints(int points)
 
 int CGHeroInstance::movementPointsLimit(bool onLand) const
 {
-	return valOfBonuses(BonusType::MOVEMENT, onLand ? BonusCustomSubtype::heroMovementLand : BonusCustomSubtype::heroMovementSea);
+	auto ti = getTurnInfo(0);
+	return onLand ? ti->getMovePointsLimitLand() : ti->getMovePointsLimitWater();
 }
 
 int CGHeroInstance::getLowestCreatureSpeed() const
 {
-	return lowestCreatureSpeed;
-}
+	if(stacksCount() != 0)
+	{
+		int minimalSpeed = std::numeric_limits<int>::max();
+		//TODO? should speed modifiers (eg from artifacts) affect hero movement?
+		for(const auto & slot : Slots())
+			minimalSpeed = std::min(minimalSpeed, slot.second->getInitiative());
 
-void CGHeroInstance::updateArmyMovementBonus(bool onLand, const TurnInfo * ti) const
-{
-	auto realLowestSpeed = lowestSpeed(this);
-	if(lowestCreatureSpeed != realLowestSpeed)
+		return minimalSpeed;
+	}
+	else
 	{
-		lowestCreatureSpeed = realLowestSpeed;
-		//Let updaters run again
-		treeHasChanged();
-		ti->updateHeroBonuses(BonusType::MOVEMENT);
+		if(commander && commander->alive)
+			return commander->getInitiative();
 	}
+
+	return 10;
+}
+
+std::unique_ptr<TurnInfo> CGHeroInstance::getTurnInfo(int days) const
+{
+	return std::make_unique<TurnInfo>(turnInfoCache.get(), this, days);
 }
 
 int CGHeroInstance::movementPointsLimitCached(bool onLand, const TurnInfo * ti) const
 {
-	updateArmyMovementBonus(onLand, ti);
-	return ti->valOfBonuses(BonusType::MOVEMENT, onLand ? BonusCustomSubtype::heroMovementLand : BonusCustomSubtype::heroMovementSea);
+	if (onLand)
+		return ti->getMovePointsLimitLand();
+	else
+		return ti->getMovePointsLimitWater();
 }
 
 CGHeroInstance::CGHeroInstance(IGameCallback * cb)
@@ -293,7 +276,10 @@ CGHeroInstance::CGHeroInstance(IGameCallback * cb)
 	level(1),
 	exp(UNINITIALIZED_EXPERIENCE),
 	gender(EHeroGender::DEFAULT),
-	lowestCreatureSpeed(0)
+	primarySkills(this),
+	magicSchoolMastery(this),
+	turnInfoCache(std::make_unique<TurnInfoCache>(this)),
+	manaPerKnowledgeCached(this, Selector::type()(BonusType::MANA_PER_KNOWLEDGE_PERCENTAGE))
 {
 	setNodeType(HERO);
 	ID = Obj::HERO;
@@ -704,40 +690,20 @@ void CGHeroInstance::setPropertyDer(ObjProperty what, ObjPropertyID identifier)
 		setStackCount(SlotID(0), identifier.getNum());
 }
 
-std::array<int, 4> CGHeroInstance::getPrimarySkills() const
+int CGHeroInstance::getPrimSkillLevel(PrimarySkill id) const
 {
-	std::array<int, 4> result;
-
-	auto allSkills = getBonusBearer()->getBonusesOfType(BonusType::PRIMARY_SKILL);
-	for (auto skill : PrimarySkill::ALL_SKILLS())
-	{
-		int ret = allSkills->valOfBonuses(Selector::subtype()(BonusSubtypeID(skill)));
-		int minSkillValue = VLC->engineSettings()->getVectorValue(EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS, skill.getNum());
-		result[skill] = std::max(ret, minSkillValue); //otherwise, some artifacts may cause negative skill value effect
-	}
-
-	return result;
+	return primarySkills.getSkills()[id];
 }
 
 double CGHeroInstance::getFightingStrength() const
 {
-	const auto & primarySkills = getPrimarySkills();
-	return getFightingStrengthImpl(primarySkills);
-}
-
-double CGHeroInstance::getFightingStrengthImpl(const std::array<int, 4> & primarySkills) const
-{
-	return sqrt((1.0 + 0.05*primarySkills[PrimarySkill::ATTACK]) * (1.0 + 0.05*primarySkills[PrimarySkill::DEFENSE]));
+	const auto & skillValues = primarySkills.getSkills();
+	return sqrt((1.0 + 0.05*skillValues[PrimarySkill::ATTACK]) * (1.0 + 0.05*skillValues[PrimarySkill::DEFENSE]));
 }
 
 double CGHeroInstance::getMagicStrength() const
 {
-	const auto & primarySkills = getPrimarySkills();
-	return getMagicStrengthImpl(primarySkills);
-}
-
-double CGHeroInstance::getMagicStrengthImpl(const std::array<int, 4> & primarySkills) const
-{
+	const auto & skillValues = primarySkills.getSkills();
 	if (!hasSpellbook())
 		return 1;
 	bool atLeastOneCombatSpell = false;
@@ -751,13 +717,12 @@ double CGHeroInstance::getMagicStrengthImpl(const std::array<int, 4> & primarySk
 	}
 	if (!atLeastOneCombatSpell)
 		return 1;
-	return sqrt((1.0 + 0.05*primarySkills[PrimarySkill::KNOWLEDGE] * mana / manaLimit()) * (1.0 + 0.05*primarySkills[PrimarySkill::SPELL_POWER] * mana / manaLimit()));
+	return sqrt((1.0 + 0.05*skillValues[PrimarySkill::KNOWLEDGE] * mana / manaLimit()) * (1.0 + 0.05*skillValues[PrimarySkill::SPELL_POWER] * mana / manaLimit()));
 }
 
 double CGHeroInstance::getHeroStrength() const
 {
-	const auto & primarySkills = getPrimarySkills();
-	return getFightingStrengthImpl(primarySkills) * getMagicStrengthImpl(primarySkills);
+	return getFightingStrength() * getMagicStrength();
 }
 
 uint64_t CGHeroInstance::getValueForDiplomacy() const
@@ -809,7 +774,7 @@ int32_t CGHeroInstance::getSpellSchoolLevel(const spells::Spell * spell, SpellSc
 
 	spell->forEachSchool([&, this](const SpellSchool & cnf, bool & stop)
 	{
-		int32_t thisSchool = valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, BonusSubtypeID(cnf)); //FIXME: Bonus shouldn't be additive (Witchking Artifacts : Crown of Skies)
+		int32_t thisSchool = magicSchoolMastery.getMastery(cnf); //FIXME: Bonus shouldn't be additive (Witchking Artifacts : Crown of Skies)
 		if(thisSchool > skill)
 		{
 			skill = thisSchool;
@@ -818,7 +783,7 @@ int32_t CGHeroInstance::getSpellSchoolLevel(const spells::Spell * spell, SpellSc
 		}
 	});
 
-	vstd::amax(skill, valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, BonusSubtypeID(SpellSchool::ANY))); //any school bonus
+	vstd::amax(skill, magicSchoolMastery.getMastery(SpellSchool::ANY)); //any school bonus
 	vstd::amax(skill, valOfBonuses(BonusType::SPELL, BonusSubtypeID(spell->getId()))); //given by artifact or other effect
 
 	vstd::amax(skill, 0); //in case we don't know any school
@@ -1207,8 +1172,7 @@ std::string CGHeroInstance::nodeName() const
 
 si32 CGHeroInstance::manaLimit() const
 {
-	return si32(getPrimSkillLevel(PrimarySkill::KNOWLEDGE)
-		* (valOfBonuses(BonusType::MANA_PER_KNOWLEDGE_PERCENTAGE))) / 100;
+	return getPrimSkillLevel(PrimarySkill::KNOWLEDGE) * manaPerKnowledgeCached.getValue() / 100;
 }
 
 HeroTypeID CGHeroInstance::getPortraitSource() const
@@ -1381,14 +1345,7 @@ CBonusSystemNode & CGHeroInstance::whereShouldBeAttached(CGameState * gs)
 
 int CGHeroInstance::movementPointsAfterEmbark(int MPsBefore, int basicCost, bool disembark, const TurnInfo * ti) const
 {
-	std::unique_ptr<TurnInfo> turnInfoLocal;
-	if(!ti)
-	{
-		turnInfoLocal = std::make_unique<TurnInfo>(this);
-		ti = turnInfoLocal.get();
-	}
-
-	if(!ti->hasBonusOfType(BonusType::FREE_SHIP_BOARDING))
+	if(!ti->hasFreeShipBoarding())
 		return 0; // take all MPs by default
 	
 	auto boatLayer = boat ? boat->layer : EPathfindingLayer::SAIL;

+ 13 - 9
lib/mapObjects/CGHeroInstance.h

@@ -14,6 +14,7 @@
 #include "CArmedInstance.h"
 #include "IOwnableObject.h"
 
+#include "../bonuses/BonusCache.h"
 #include "../entities/hero/EHeroGender.h"
 #include "../CArtHandler.h" // For CArtifactSet
 
@@ -24,8 +25,10 @@ class CGBoat;
 class CGTownInstance;
 class CMap;
 class UpgradeInfo;
+class TurnInfo;
+
 struct TerrainTile;
-struct TurnInfo;
+struct TurnInfoCache;
 
 class DLL_LINKAGE CGHeroPlaceholder : public CGObjectInstance
 {
@@ -58,13 +61,14 @@ class DLL_LINKAGE CGHeroInstance : public CArmedInstance, public IBoatGenerator,
 	friend class CMapFormatJson;
 
 private:
+	PrimarySkillsCache primarySkills;
+	MagicSchoolMasteryCache magicSchoolMastery;
+	BonusValueCache manaPerKnowledgeCached;
+	std::unique_ptr<TurnInfoCache> turnInfoCache;
+
 	std::set<SpellID> spells; //known spells (spell IDs)
-	mutable int lowestCreatureSpeed;
 	ui32 movement; //remaining movement points
 
-	double getFightingStrengthImpl(const std::array<int, 4> & primarySkills) const;
-	double getMagicStrengthImpl(const std::array<int, 4> & primarySkills) const;
-
 public:
 
 	//////////////////////////////////////////////////////////////////////////
@@ -204,7 +208,7 @@ public:
 	std::vector<SecondarySkill> getLevelUpProposedSecondarySkills(vstd::RNG & rand) const;
 
 	ui8 getSecSkillLevel(const SecondarySkill & skill) const; //0 - no skill
-	std::array<int, 4> getPrimarySkills() const;
+	int getPrimSkillLevel(PrimarySkill id) const;
 
 	/// Returns true if hero has free secondary skill slot.
 	bool canLearnSkill() const;
@@ -222,10 +226,10 @@ public:
 	int movementPointsLimit(bool onLand) const;
 	//cached version is much faster, TurnInfo construction is costly
 	int movementPointsLimitCached(bool onLand, const TurnInfo * ti) const;
-	//update army movement bonus
-	void updateArmyMovementBonus(bool onLand, const TurnInfo * ti) const;
 
-	int movementPointsAfterEmbark(int MPsBefore, int basicCost, bool disembark = false, const TurnInfo * ti = nullptr) const;
+	int movementPointsAfterEmbark(int MPsBefore, int basicCost, bool disembark, const TurnInfo * ti) const;
+
+	std::unique_ptr<TurnInfo> getTurnInfo(int days) const;
 
 	double getFightingStrength() const; // takes attack / defense skill into account
 	double getMagicStrength() const; // takes knowledge / spell power skill but also current mana, whether the hero owns a spell-book and whether that books contains anything into account

+ 1 - 0
lib/mapObjects/CGTownInstance.cpp

@@ -1134,6 +1134,7 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler)
 		eventsHandler.syncSize(events, JsonNode::JsonType::DATA_VECTOR);
 		eventsHandler.serializeStruct(events);
 	}
+	handler.serializeId("alignmentToPlayer", alignmentToPlayer, PlayerColor::NEUTRAL);
 }
 
 const CFaction * CGTownInstance::getFaction() const

+ 5 - 0
lib/mapObjects/CQuest.cpp

@@ -803,6 +803,11 @@ std::string CGKeys::getObjectName() const
 	return VLC->generaltexth->tentColors[subID.getNum()] + " " + CGObjectInstance::getObjectName();
 }
 
+std::string CGKeys::getObjectDescription(PlayerColor player) const
+{
+	return visitedTxt(wasMyColorVisited(player));
+}
+
 bool CGKeymasterTent::wasVisited (PlayerColor player) const
 {
 	return wasMyColorVisited (player);

+ 2 - 1
lib/mapObjects/CQuest.h

@@ -199,7 +199,8 @@ public:
 
 	bool wasMyColorVisited(const PlayerColor & player) const;
 
-	std::string getObjectName() const override; //depending on color
+	std::string getObjectName() const override;
+	std::string getObjectDescription(PlayerColor player) const;
 	std::string getHoverText(PlayerColor player) const override;
 
 	template <typename Handler> void serialize(Handler &h)

+ 5 - 0
lib/mapObjects/MiscObjects.cpp

@@ -1289,6 +1289,11 @@ std::string CGObelisk::getHoverText(PlayerColor player) const
 	return getObjectName() + " " + visitedTxt(wasVisited(player));
 }
 
+std::string CGObelisk::getObjectDescription(PlayerColor player) const
+{
+	return visitedTxt(wasVisited(player));
+}
+
 void CGObelisk::setPropertyDer(ObjProperty what, ObjPropertyID identifier)
 {
 	switch(what)

+ 1 - 0
lib/mapObjects/MiscObjects.h

@@ -406,6 +406,7 @@ public:
 	void onHeroVisit(const CGHeroInstance * h) const override;
 	void initObj(vstd::RNG & rand) override;
 	std::string getHoverText(PlayerColor player) const override;
+	std::string getObjectDescription(PlayerColor player) const;
 
 	template <typename Handler> void serialize(Handler &h)
 	{

+ 1 - 94
lib/mapping/CMap.cpp

@@ -143,17 +143,6 @@ TerrainTile::TerrainTile():
 {
 }
 
-bool TerrainTile::entrableTerrain(const TerrainTile * from) const
-{
-	return entrableTerrain(from ? from->isLand() : true, from ? from->isWater() : true);
-}
-
-bool TerrainTile::entrableTerrain(bool allowLand, bool allowSea) const
-{
-	return getTerrain()->isPassable()
-			&& ((allowSea && isWater())  ||  (allowLand && isLand()));
-}
-
 bool TerrainTile::isClear(const TerrainTile * from) const
 {
 	return entrableTerrain(from) && !blocked();
@@ -187,72 +176,6 @@ EDiggingStatus TerrainTile::getDiggingStatus(const bool excludeTop) const
 		return EDiggingStatus::CAN_DIG;
 }
 
-bool TerrainTile::hasFavorableWinds() const
-{
-	return extTileFlags & 128;
-}
-
-bool TerrainTile::isWater() const
-{
-	return getTerrain()->isWater();
-}
-
-bool TerrainTile::isLand() const
-{
-	return getTerrain()->isLand();
-}
-
-bool TerrainTile::visitable() const
-{
-	return !visitableObjects.empty();
-}
-
-bool TerrainTile::blocked() const
-{
-	return !blockingObjects.empty();
-}
-
-bool TerrainTile::hasRiver() const
-{
-	return getRiverID() != RiverId::NO_RIVER;
-}
-
-bool TerrainTile::hasRoad() const
-{
-	return getRoadID() != RoadId::NO_ROAD;
-}
-
-const TerrainType * TerrainTile::getTerrain() const
-{
-	return terrainType.toEntity(VLC);
-}
-
-const RiverType * TerrainTile::getRiver() const
-{
-	return riverType.toEntity(VLC);
-}
-
-const RoadType * TerrainTile::getRoad() const
-{
-	return roadType.toEntity(VLC);
-}
-
-TerrainId TerrainTile::getTerrainID() const
-{
-	return terrainType;
-}
-
-RiverId TerrainTile::getRiverID() const
-{
-	return riverType;
-}
-
-RoadId TerrainTile::getRoadID() const
-{
-	return roadType;
-}
-
-
 CMap::CMap(IGameCallback * cb)
 	: GameCallbackHolder(cb)
 	, checksum(0)
@@ -365,7 +288,7 @@ bool CMap::isCoastalTile(const int3 & pos) const
 		return false;
 	}
 
-	if(isWaterTile(pos))
+	if(getTile(pos).isWater())
 		return false;
 
 	for(const auto & dir : dirs)
@@ -382,22 +305,6 @@ bool CMap::isCoastalTile(const int3 & pos) const
 	return false;
 }
 
-TerrainTile & CMap::getTile(const int3 & tile)
-{
-	assert(isInTheMap(tile));
-	return terrain[tile.z][tile.x][tile.y];
-}
-
-const TerrainTile & CMap::getTile(const int3 & tile) const
-{
-	assert(isInTheMap(tile));
-	return terrain[tile.z][tile.x][tile.y];
-}
-
-bool CMap::isWaterTile(const int3 &pos) const
-{
-	return isInTheMap(pos) && getTile(pos).isWater();
-}
 bool CMap::canMoveBetween(const int3 &src, const int3 &dst) const
 {
 	const TerrainTile * dstTile = &getTile(dst);

+ 24 - 11
lib/mapping/CMap.h

@@ -86,18 +86,10 @@ public:
 	void initTerrain();
 
 	CMapEditManager * getEditManager();
-	TerrainTile & getTile(const int3 & tile);
-	const TerrainTile & getTile(const int3 & tile) const;
+	inline TerrainTile & getTile(const int3 & tile);
+	inline const TerrainTile & getTile(const int3 & tile) const;
 	bool isCoastalTile(const int3 & pos) const;
-	bool isWaterTile(const int3 & pos) const;
-	inline bool isInTheMap(const int3 & pos) const
-	{
-		// Check whether coord < 0 is done implicitly. Negative signed int overflows to unsigned number larger than all signed ints.
-		return
-			static_cast<uint32_t>(pos.x) < static_cast<uint32_t>(width) &&
-			static_cast<uint32_t>(pos.y) < static_cast<uint32_t>(height) &&
-			static_cast<uint32_t>(pos.z) <= (twoLevel ? 1 : 0);
-	}
+	inline bool isInTheMap(const int3 & pos) const;
 
 	bool canMoveBetween(const int3 &src, const int3 &dst) const;
 	bool checkForVisitableDir(const int3 & src, const TerrainTile * pom, const int3 & dst) const;
@@ -250,4 +242,25 @@ public:
 	}
 };
 
+inline bool CMap::isInTheMap(const int3 & pos) const
+{
+	// Check whether coord < 0 is done implicitly. Negative signed int overflows to unsigned number larger than all signed ints.
+	return
+		static_cast<uint32_t>(pos.x) < static_cast<uint32_t>(width) &&
+		static_cast<uint32_t>(pos.y) < static_cast<uint32_t>(height) &&
+		static_cast<uint32_t>(pos.z) <= (twoLevel ? 1 : 0);
+}
+
+inline TerrainTile & CMap::getTile(const int3 & tile)
+{
+	assert(isInTheMap(tile));
+	return terrain[tile.z][tile.x][tile.y];
+}
+
+inline const TerrainTile & CMap::getTile(const int3 & tile) const
+{
+	assert(isInTheMap(tile));
+	return terrain[tile.z][tile.x][tile.y];
+}
+
 VCMI_LIB_NAMESPACE_END

+ 100 - 16
lib/mapping/CMapDefines.h

@@ -12,7 +12,8 @@
 
 #include "../ResourceSet.h"
 #include "../texts/MetaString.h"
-#include "../int3.h"
+#include "../VCMI_Lib.h"
+#include "../TerrainHandler.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -103,31 +104,32 @@ struct DLL_LINKAGE TerrainTile
 	TerrainTile();
 
 	/// Gets true if the terrain is not a rock. If from is water/land, same type is also required.
-	bool entrableTerrain(const TerrainTile * from = nullptr) const;
-	bool entrableTerrain(bool allowLand, bool allowSea) const;
+	inline bool entrableTerrain() const;
+	inline bool entrableTerrain(const TerrainTile * from) const;
+	inline bool entrableTerrain(bool allowLand, bool allowSea) const;
 	/// Checks for blocking objects and terraint type (water / land).
 	bool isClear(const TerrainTile * from = nullptr) const;
 	/// Gets the ID of the top visitable object or -1 if there is none.
 	Obj topVisitableId(bool excludeTop = false) const;
 	CGObjectInstance * topVisitableObj(bool excludeTop = false) const;
-	bool isWater() const;
-	bool isLand() const;
+	inline bool isWater() const;
+	inline bool isLand() const;
 	EDiggingStatus getDiggingStatus(bool excludeTop = true) const;
-	bool hasFavorableWinds() const;
+	inline bool hasFavorableWinds() const;
 
-	bool visitable() const;
-	bool blocked() const;
+	inline bool visitable() const;
+	inline bool blocked() const;
 
-	const TerrainType * getTerrain() const;
-	const RiverType * getRiver() const;
-	const RoadType * getRoad() const;
+	inline const TerrainType * getTerrain() const;
+	inline const RiverType * getRiver() const;
+	inline const RoadType * getRoad() const;
 
-	TerrainId getTerrainID() const;
-	RiverId getRiverID() const;
-	RoadId getRoadID() const;
+	inline TerrainId getTerrainID() const;
+	inline RiverId getRiverID() const;
+	inline RoadId getRoadID() const;
 
-	bool hasRiver() const;
-	bool hasRoad() const;
+	inline bool hasRiver() const;
+	inline bool hasRoad() const;
 
 	TerrainId terrainType;
 	RiverId riverType;
@@ -193,4 +195,86 @@ struct DLL_LINKAGE TerrainTile
 	}
 };
 
+inline bool TerrainTile::hasFavorableWinds() const
+{
+	return extTileFlags & 128;
+}
+
+inline bool TerrainTile::isWater() const
+{
+	return getTerrain()->isWater();
+}
+
+inline bool TerrainTile::isLand() const
+{
+	return getTerrain()->isLand();
+}
+
+inline bool TerrainTile::visitable() const
+{
+	return !visitableObjects.empty();
+}
+
+inline bool TerrainTile::blocked() const
+{
+	return !blockingObjects.empty();
+}
+
+inline bool TerrainTile::hasRiver() const
+{
+	return getRiverID() != RiverId::NO_RIVER;
+}
+
+inline bool TerrainTile::hasRoad() const
+{
+	return getRoadID() != RoadId::NO_ROAD;
+}
+
+inline const TerrainType * TerrainTile::getTerrain() const
+{
+	return terrainType.toEntity(VLC);
+}
+
+inline const RiverType * TerrainTile::getRiver() const
+{
+	return riverType.toEntity(VLC);
+}
+
+inline const RoadType * TerrainTile::getRoad() const
+{
+	return roadType.toEntity(VLC);
+}
+
+inline TerrainId TerrainTile::getTerrainID() const
+{
+	return terrainType;
+}
+
+inline RiverId TerrainTile::getRiverID() const
+{
+	return riverType;
+}
+
+inline RoadId TerrainTile::getRoadID() const
+{
+	return roadType;
+}
+
+inline bool TerrainTile::entrableTerrain() const
+{
+	return entrableTerrain(true, true);
+}
+
+inline bool TerrainTile::entrableTerrain(const TerrainTile * from) const
+{
+	const TerrainType * terrainFrom = from->getTerrain();
+	return entrableTerrain(terrainFrom->isLand(), terrainFrom->isWater());
+}
+
+inline bool TerrainTile::entrableTerrain(bool allowLand, bool allowSea) const
+{
+	const TerrainType * terrain = getTerrain();
+	return terrain->isPassable() && ((allowSea && terrain->isWater()) || (allowLand && terrain->isLand()));
+}
+
 VCMI_LIB_NAMESPACE_END

+ 3 - 1
lib/modding/CModHandler.cpp

@@ -144,7 +144,7 @@ TModID CModHandler::findResourceOrigin(const ResourcePath & name) const
 			return "core";
 
 		if(CResourceHandler::get("mapEditor")->existsResource(name))
-			return "core"; // Workaround for loading maps via map editor
+			return "mapEditor"; // Workaround for loading maps via map editor
 	}
 	catch( const std::out_of_range & e)
 	{
@@ -189,6 +189,8 @@ std::string CModHandler::getModLanguage(const TModID& modId) const
 		return VLC->generaltexth->getInstalledLanguage();
 	if(modId == "map")
 		return VLC->generaltexth->getPreferredLanguage();
+	if(modId == "mapEditor")
+		return VLC->generaltexth->getPreferredLanguage();
 	return getModInfo(modId).getBaseLanguage();
 }
 

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä