Browse Source

Merge remote-tracking branch 'origin/develop' into terrain-rewrite

# Conflicts:
#	lib/Terrain.cpp
#	lib/Terrain.h
Tomasz Zieliński 3 years ago
parent
commit
a5077245a8
96 changed files with 925 additions and 366 deletions
  1. 56 5
      .github/workflows/github.yml
  2. 6 0
      AI/BattleAI/StackWithBonuses.cpp
  3. 4 0
      AI/BattleAI/StackWithBonuses.h
  4. 1 2
      AI/Nullkiller/Analyzers/ArmyManager.cpp
  5. 1 0
      AI/Nullkiller/Analyzers/ArmyManager.h
  6. 1 3
      AI/Nullkiller/Analyzers/BuildAnalyzer.cpp
  7. 4 4
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp
  8. 4 3
      AI/Nullkiller/Analyzers/HeroManager.h
  9. 1 1
      AI/Nullkiller/Analyzers/ObjectClusterizer.cpp
  10. 0 2
      AI/Nullkiller/Behaviors/BuildingBehavior.cpp
  11. 6 6
      AI/Nullkiller/Behaviors/DefenceBehavior.cpp
  12. 4 4
      AI/Nullkiller/Behaviors/StartupBehavior.cpp
  13. 2 10
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  14. 1 0
      AI/Nullkiller/Engine/PriorityEvaluator.h
  15. 1 1
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  16. 1 4
      AI/Nullkiller/Pathfinding/Actions/BoatActions.h
  17. 3 1
      AI/Nullkiller/Pathfinding/Actions/SpecialAction.h
  18. 1 12
      AI/Nullkiller/Pathfinding/Actors.cpp
  19. 3 2
      AI/Nullkiller/Pathfinding/Actors.h
  20. 0 3
      AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp
  21. 1 5
      AI/VCAI/AIhelper.cpp
  22. 0 1
      AI/VCAI/AIhelper.h
  23. 1 0
      AI/VCAI/ArmyManager.h
  24. 3 8
      AI/VCAI/ResourceManager.cpp
  25. 2 0
      CCallback.cpp
  26. 4 0
      CCallback.h
  27. 12 2
      CMakeLists.txt
  28. 1 5
      CMakePresets.json
  29. 19 1
      ChangeLog
  30. 2 0
      client/CGameInfo.cpp
  31. 2 0
      client/CGameInfo.h
  32. 2 0
      client/CMT.cpp
  33. 15 0
      client/CServerHandler.cpp
  34. 10 0
      client/Client.cpp
  35. 7 0
      client/Client.h
  36. 1 2
      client/battle/CBattleInterface.cpp
  37. 3 2
      client/gui/CAnimation.cpp
  38. 1 0
      client/gui/CIntObject.h
  39. 1 0
      client/gui/SDL_Extensions.h
  40. 2 1
      client/mainmenu/CCampaignScreen.cpp
  41. 2 0
      client/mainmenu/CCampaignScreen.h
  42. 3 2
      client/mainmenu/CreditsScreen.cpp
  43. 1 2
      client/widgets/AdventureMapClasses.cpp
  44. 3 2
      client/widgets/CComponent.cpp
  45. 3 2
      client/widgets/CGarrisonInt.cpp
  46. 5 5
      client/windows/CAdvmapInterface.cpp
  47. 14 5
      client/windows/CCastleInterface.cpp
  48. 1 1
      client/windows/CCastleInterface.h
  49. 1 1
      client/windows/CKingdomInterface.cpp
  50. 1 1
      client/windows/CSpellWindow.cpp
  51. 3 2
      client/windows/CWindowObject.cpp
  52. 24 3
      client/windows/CreaturePurchaseCard.cpp
  53. 20 1
      client/windows/CreaturePurchaseCard.h
  54. 5 5
      client/windows/GUIClasses.cpp
  55. 1 1
      client/windows/GUIClasses.h
  56. 2 1
      client/windows/InfoWindows.cpp
  57. 4 4
      client/windows/QuickRecruitmentWindow.cpp
  58. 2 2
      client/windows/QuickRecruitmentWindow.h
  59. 24 12
      config/translate.json
  60. 2 0
      include/vcmi/ServerCallback.h
  61. 4 0
      include/vcmi/Services.h
  62. 2 0
      include/vcmi/scripting/Service.h
  63. 79 27
      launcher/modManager/cmodlist.cpp
  64. 4 0
      launcher/modManager/cmodlist.h
  65. 1 0
      launcher/modManager/cmodlistmodel_moc.cpp
  66. 9 9
      launcher/modManager/cmodlistview_moc.ui
  67. 4 0
      launcher/modManager/cmodmanager.cpp
  68. 2 0
      lib/CGameInterface.cpp
  69. 6 0
      lib/CGameInterface.h
  70. 63 0
      lib/CModHandler.cpp
  71. 69 14
      lib/CModHandler.h
  72. 4 0
      lib/CPathfinder.h
  73. 2 0
      lib/CScriptingModule.cpp
  74. 2 0
      lib/CScriptingModule.h
  75. 10 2
      lib/GameConstants.cpp
  76. 3 0
      lib/GameConstants.h
  77. 5 2
      lib/IGameCallback.h
  78. 2 0
      lib/ScriptHandler.cpp
  79. 2 0
      lib/ScriptHandler.h
  80. 10 0
      lib/VCMI_Lib.cpp
  81. 11 3
      lib/VCMI_Lib.h
  82. 2 0
      lib/battle/BattleInfo.cpp
  83. 2 0
      lib/battle/BattleInfo.h
  84. 0 7
      lib/battle/CBattleInfoCallback.h
  85. 4 0
      lib/battle/IBattleInfoCallback.h
  86. 1 3
      lib/events/ApplyDamage.cpp
  87. 0 4
      lib/events/ApplyDamage.h
  88. 1 1
      lib/filesystem/Filesystem.cpp
  89. 2 3
      lib/mapObjects/CRewardableObject.cpp
  90. 180 107
      lib/rmg/ObstaclePlacer.cpp
  91. 51 1
      lib/rmg/ObstaclePlacer.h
  92. 1 5
      lib/rmg/Zone.cpp
  93. 2 0
      lib/spells/ISpellMechanics.cpp
  94. 6 0
      lib/spells/ISpellMechanics.h
  95. 59 35
      server/CGameHandler.cpp
  96. 10 1
      server/CGameHandler.h

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

@@ -1,17 +1,68 @@
 name: VCMI
 
 on:
-    push:
-      branches:
-        - features/*
-        - develop
-    pull_request:
+  push:
+    branches:
+      - features/*
+  pull_request:
+  schedule:
+    - cron: '0 2 * * *'
+  workflow_dispatch:
+
 env:
   # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.)
   BUILD_TYPE: Release
 
 jobs:
+  check_last_build:
+    if: github.event.schedule != ''
+    runs-on: ubuntu-latest
+    outputs:
+      skip_build: ${{ steps.check_if_built.outputs.skip_build }}
+    defaults:
+      run:
+        shell: bash
+    steps:
+      - name: Get repo name
+        id: get_repo_name
+        run: echo "::set-output name=value::${GITHUB_REPOSITORY#*/}"
+      - name: Get last successful build for ${{ github.sha }}
+        uses: octokit/[email protected]
+        id: get_last_scheduled_run
+        with:
+          route: GET /repos/{owner}/{repo}/actions/runs
+          owner: ${{ github.repository_owner }}
+          repo: ${{ steps.get_repo_name.outputs.value }}
+          status: success
+          per_page: 1
+          head_sha: ${{ github.sha }}
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      - name: Check if successful build of the current commit exists
+        id: check_if_built
+        run: |
+          if [ ${{ fromJson(steps.get_last_scheduled_run.outputs.data).total_count }} -gt 0 ]; then
+            echo '::set-output name=skip_build::1'
+          else
+            echo '::set-output name=skip_build::0'
+          fi
+      - name: Cancel current run
+        if: steps.check_if_built.outputs.skip_build == 1
+        uses: octokit/[email protected]
+        with:
+          route: POST /repos/{owner}/{repo}/actions/runs/{run_id}/cancel
+          owner: ${{ github.repository_owner }}
+          repo: ${{ steps.get_repo_name.outputs.value }}
+          run_id: ${{ github.run_id }}
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      - name: Wait for the run to be cancelled
+        if: steps.check_if_built.outputs.skip_build == 1
+        run: sleep 60
+
   build:
+    needs: check_last_build
+    if: always() && needs.check_last_build.skip_build != 1
     strategy:
       matrix:
         include:

+ 6 - 0
AI/BattleAI/StackWithBonuses.cpp

@@ -16,7 +16,9 @@
 #include "../../lib/CStack.h"
 #include "../../lib/ScriptHandler.h"
 
+#if SCRIPTING_ENABLED
 using scripting::Pool;
+#endif
 
 void actualizeEffect(TBonusListPtr target, const Bonus & ef)
 {
@@ -217,7 +219,9 @@ HypotheticBattle::HypotheticBattle(const Environment * ENV, Subject realBattle)
 	localEnvironment.reset(new HypotheticEnvironment(this, env));
 	serverCallback.reset(new HypotheticServerCallback(this));
 
+#if SCRIPTING_ENABLED
 	pool.reset(new scripting::PoolImpl(localEnvironment.get(), serverCallback.get()));
+#endif
 }
 
 bool HypotheticBattle::unitHasAmmoCart(const battle::Unit * unit) const
@@ -420,10 +424,12 @@ int64_t HypotheticBattle::getTreeVersion() const
 	return getBattleNode()->getTreeVersion() + bonusTreeVersion;
 }
 
+#if SCRIPTING_ENABLED
 Pool * HypotheticBattle::getContextPool() const
 {
 	return pool.get();
 }
+#endif
 
 ServerCallback * HypotheticBattle::getServerCallback()
 {

+ 4 - 0
AI/BattleAI/StackWithBonuses.h

@@ -136,7 +136,9 @@ public:
 
 	int64_t getTreeVersion() const;
 
+#if SCRIPTING_ENABLED
 	scripting::Pool * getContextPool() const override;
+#endif
 
 	ServerCallback * getServerCallback();
 
@@ -189,6 +191,8 @@ private:
 	std::unique_ptr<HypotheticServerCallback> serverCallback;
 	std::unique_ptr<HypotheticEnvironment> localEnvironment;
 
+#if SCRIPTING_ENABLED
 	mutable std::shared_ptr<scripting::Pool> pool;
+#endif
 	mutable std::shared_ptr<events::EventBus> eventBus;
 };

+ 1 - 2
AI/Nullkiller/Analyzers/ArmyManager.cpp

@@ -204,7 +204,6 @@ std::shared_ptr<CCreatureSet> ArmyManager::getArmyAvailableToBuyAsCCreatureSet(
 	TResources availableRes) const
 {
 	std::vector<creInfo> creaturesInDwellings;
-	int freeHeroSlots = GameConstants::ARMY_SIZE;
 	auto army = std::make_shared<TemporaryArmy>();
 
 	for(int i = dwelling->creatures.size() - 1; i >= 0; i--)
@@ -497,4 +496,4 @@ ArmyUpgradeInfo ArmyManager::calculateCreaturesUpgrade(
 	}
 
 	return result;
-}
+}

+ 1 - 0
AI/Nullkiller/Analyzers/ArmyManager.h

@@ -41,6 +41,7 @@ struct ArmyUpgradeInfo
 class DLL_EXPORT IArmyManager //: public: IAbstractManager
 {
 public:
+	virtual ~IArmyManager() = default;
 	virtual void update() = 0;
 	virtual ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const = 0;
 	virtual	ui64 howManyReinforcementsCanBuy(

+ 1 - 3
AI/Nullkiller/Analyzers/BuildAnalyzer.cpp

@@ -129,8 +129,6 @@ void BuildAnalyzer::update()
 	{
 		logAi->trace("Checking town %s", town->name);
 
-		auto townInfo = town->town;
-
 		developmentInfos.push_back(TownDevelopmentInfo(town));
 		TownDevelopmentInfo & developmentInfo = developmentInfos.back();
 
@@ -399,4 +397,4 @@ std::string BuildingInfo::toString() const
 		+ ", creature: " + std::to_string(creatureGrows) + " x " + std::to_string(creatureLevel)
 		+ " x " + creatureCost.toString()
 		+ ", daily: " + dailyIncome.toString();
-}
+}

+ 4 - 4
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp

@@ -63,7 +63,7 @@ void DangerHitMapAnalyzer::updateHitMap()
 				auto & node = hitMap[pos.x][pos.y][pos.z];
 
 				if(tileDanger > node.maximumDanger.danger
-					|| tileDanger == node.maximumDanger.danger && node.maximumDanger.turn > turn)
+					|| (tileDanger == node.maximumDanger.danger && node.maximumDanger.turn > turn))
 				{
 					node.maximumDanger.danger = tileDanger;
 					node.maximumDanger.turn = turn;
@@ -71,7 +71,7 @@ void DangerHitMapAnalyzer::updateHitMap()
 				}
 
 				if(turn < node.fastestDanger.turn
-					|| turn == node.fastestDanger.turn && node.fastestDanger.danger < tileDanger)
+					|| (turn == node.fastestDanger.turn && node.fastestDanger.danger < tileDanger))
 				{
 					node.fastestDanger.danger = tileDanger;
 					node.fastestDanger.turn = turn;
@@ -101,8 +101,8 @@ uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath &
 	int turn = path.turn();
 	const HitMapNode & info = hitMap[tile.x][tile.y][tile.z];
 
-	return info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger)
-		|| info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger);
+	return (info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger))
+		|| (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger));
 }
 
 const HitMapNode & DangerHitMapAnalyzer::getObjectTreat(const CGObjectInstance * obj) const

+ 4 - 3
AI/Nullkiller/Analyzers/HeroManager.h

@@ -20,6 +20,7 @@
 class DLL_EXPORT IHeroManager //: public: IAbstractManager
 {
 public:
+	virtual ~IHeroManager() = default;
 	virtual const std::map<HeroPtr, HeroRole> & getHeroRoles() const = 0;
 	virtual int selectBestSkill(const HeroPtr & hero, const std::vector<SecondarySkill> & skills) const = 0;
 	virtual HeroRole getHeroRole(const HeroPtr & hero) const = 0;
@@ -31,6 +32,7 @@ public:
 class DLL_EXPORT ISecondarySkillRule
 {
 public:
+	virtual ~ISecondarySkillRule() = default;
 	virtual void evaluateScore(const CGHeroInstance * hero, SecondarySkill skill, float & score) const = 0;
 };
 
@@ -52,11 +54,10 @@ private:
 	static SecondarySkillEvaluator scountSkillsScores;
 
 	CCallback * cb; //this is enough, but we downcast from CCallback
-	const Nullkiller * ai;
 	std::map<HeroPtr, HeroRole> heroRoles;
 
 public:
-	HeroManager(CCallback * CB, const Nullkiller * ai) : cb(CB), ai(ai) {}
+	HeroManager(CCallback * CB, const Nullkiller * ai) : cb(CB) {}
 	const std::map<HeroPtr, HeroRole> & getHeroRoles() const override;
 	HeroRole getHeroRole(const HeroPtr & hero) const override;
 	int selectBestSkill(const HeroPtr & hero, const std::vector<SecondarySkill> & skills) const override;
@@ -102,4 +103,4 @@ private:
 
 public:
 	void evaluateScore(const CGHeroInstance * hero, SecondarySkill skill, float & score) const override;
-};
+};

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

@@ -149,7 +149,7 @@ bool ObjectClusterizer::shouldVisitObject(const CGObjectInstance * obj) const
 
 	const int3 pos = obj->visitablePos();
 
-	if(obj->ID != Obj::CREATURE_GENERATOR1 && vstd::contains(ai->memory->alreadyVisited, obj)
+	if((obj->ID != Obj::CREATURE_GENERATOR1 && vstd::contains(ai->memory->alreadyVisited, obj))
 		|| obj->wasVisited(ai->playerID))
 	{
 		return false;

+ 0 - 2
AI/Nullkiller/Behaviors/BuildingBehavior.cpp

@@ -53,8 +53,6 @@ Goals::TGoalVec BuildingBehavior::decompose() const
 
 	for(auto & developmentInfo : developmentInfos)
 	{
-		auto town = developmentInfo.town;
-
 		for(auto & buildingInfo : developmentInfo.toBuild)
 		{
 			if(goldPreasure < MAX_GOLD_PEASURE || buildingInfo.dailyIncome[Res::GOLD] > 0)

+ 6 - 6
AI/Nullkiller/Behaviors/DefenceBehavior.cpp

@@ -106,10 +106,10 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 		{
 			if(path.getHeroStrength() > treat.danger)
 			{
-				if(path.turn() <= treat.turn && dayOfWeek + treat.turn < 6 && isSafeToVisit(path.targetHero, path.heroArmy, treat.danger)
-					|| path.exchangeCount == 1 && path.turn() < treat.turn
+				if((path.turn() <= treat.turn && dayOfWeek + treat.turn < 6 && isSafeToVisit(path.targetHero, path.heroArmy, treat.danger))
+					|| (path.exchangeCount == 1 && path.turn() < treat.turn)
 					|| path.turn() < treat.turn - 1
-					|| path.turn() < treat.turn && treat.turn >= 2)
+					|| (path.turn() < treat.turn && treat.turn >= 2))
 				{
 					logAi->debug(
 						"Hero %s can eliminate danger for town %s using path %s.",
@@ -217,7 +217,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 				// dismiss creatures we are not able to pick to be able to hide in garrison
 				if(town->garrisonHero
 					|| town->getUpperArmy()->stacksCount() == 0
-					|| town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL)
+					|| (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL))
 				{
 					tasks.push_back(
 						Goals::sptr(Composition()
@@ -228,7 +228,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 				continue;
 			}
 				
-			if(treat.turn == 0 || path.turn() <= treat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= treat.danger)
+			if(treat.turn == 0 || (path.turn() <= treat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= treat.danger))
 			{
 				if(ai->nullkiller->arePathHeroesLocked(path))
 				{
@@ -294,4 +294,4 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 	}
 
 	logAi->debug("Found %d tasks", tasks.size());
-}
+}

+ 4 - 4
AI/Nullkiller/Behaviors/StartupBehavior.cpp

@@ -55,7 +55,7 @@ const CGHeroInstance * getNearestHero(const CGTownInstance * town)
 	if(shortestPath.nodes.size() > 1
 		|| shortestPath.turn() != 0
 		|| shortestPath.targetHero->visitablePos().dist2dSQ(town->visitablePos()) > 4
-		|| town->garrisonHero && shortestPath.targetHero == town->garrisonHero.get())
+		|| (town->garrisonHero && shortestPath.targetHero == town->garrisonHero.get()))
 		return nullptr;
 
 	return shortestPath.targetHero;
@@ -76,13 +76,13 @@ bool needToRecruitHero(const CGTownInstance * startupTown)
 
 	for(auto obj : ai->nullkiller->objectClusterizer->getNearbyObjects())
 	{
-		if(obj->ID == Obj::RESOURCE && obj->subID == Res::GOLD
+		if((obj->ID == Obj::RESOURCE && obj->subID == Res::GOLD)
 			|| obj->ID == Obj::TREASURE_CHEST
 			|| obj->ID == Obj::CAMPFIRE
 			|| obj->ID == Obj::WATER_WHEEL)
 		{
 			auto path = paths->getPathInfo(obj->visitablePos());
-			if((path->accessible == CGPathNode::BLOCKVIS || path->accessible == CGPathNode::VISIT) 
+			if((path->accessible == CGPathNode::BLOCKVIS || path->accessible == CGPathNode::VISITABLE) 
 				&& path->reachable())
 			{
 				treasureSourcesCount++;
@@ -162,7 +162,7 @@ Goals::TGoalVec StartupBehavior::decompose() const
 				auto garrisonHeroScore = ai->nullkiller->heroManager->evaluateHero(garrisonHero);
 
 				if(visitingHeroScore > garrisonHeroScore
-					|| ai->nullkiller->heroManager->getHeroRole(garrisonHero) == HeroRole::SCOUT && ai->nullkiller->heroManager->getHeroRole(visitingHero) == HeroRole::MAIN)
+					|| (ai->nullkiller->heroManager->getHeroRole(garrisonHero) == HeroRole::SCOUT && ai->nullkiller->heroManager->getHeroRole(visitingHero) == HeroRole::MAIN))
 				{
 					if(canRecruitHero || ai->nullkiller->armyManager->howManyReinforcementsCanGet(visitingHero, garrisonHero) > 200)
 					{

+ 2 - 10
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -122,7 +122,7 @@ uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHero
 	{
 		//No free slot, we might discard our weakest stack
 		weakestStackPower = std::numeric_limits<ui64>().max();
-		for (const auto stack : slots)
+		for (const auto & stack : slots)
 		{
 			vstd::amin(weakestStackPower, stack.second->getPower());
 		}
@@ -645,7 +645,6 @@ public:
 		}
 
 		auto heroPtr = task->hero;
-		auto day = ai->cb->getDate(Date::DAY);
 		auto hero = heroPtr.get(ai->cb.get());
 		bool checkGold = evaluationContext.danger == 0;
 		auto army = path.heroArmy;
@@ -670,11 +669,8 @@ public:
 
 class ClusterEvaluationContextBuilder : public IEvaluationContextBuilder
 {
-private:
-	const Nullkiller * ai;
-
 public:
-	ClusterEvaluationContextBuilder(const Nullkiller * ai) : ai(ai) {}
+	ClusterEvaluationContextBuilder(const Nullkiller * ai) {}
 
 	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
 	{
@@ -699,7 +695,6 @@ public:
 		for(auto objInfo : objects)
 		{
 			auto target = objInfo.first;
-			auto day = ai->cb->getDate(Date::DAY);
 			bool checkGold = objInfo.second.danger == 0;
 			auto army = hero;
 
@@ -718,9 +713,6 @@ public:
 			if(boost > 8)
 				break;
 		}
-
-		const AIPath & pathToCenter = clusterGoal.getPathToCenter();
-
 	}
 };
 

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

@@ -61,6 +61,7 @@ struct DLL_EXPORT EvaluationContext
 class IEvaluationContextBuilder
 {
 public:
+	virtual ~IEvaluationContextBuilder() = default;
 	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal goal) const = 0;
 };
 

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

@@ -558,7 +558,7 @@ bool AINodeStorage::selectNextActor()
 	for(auto actor = actors.begin(); actor != actors.end(); actor++)
 	{
 		if(actor->get()->armyValue > currentActor->get()->armyValue
-			|| actor->get()->armyValue == currentActor->get()->armyValue && actor <= currentActor)
+			|| (actor->get()->armyValue == currentActor->get()->armyValue && actor <= currentActor))
 		{
 			continue;
 		}

+ 1 - 4
AI/Nullkiller/Pathfinding/Actions/BoatActions.h

@@ -24,9 +24,6 @@ namespace AIPathfinding
 	
 	class SummonBoatAction : public VirtualBoatAction
 	{
-	private:
-		const CGHeroInstance * hero;
-
 	public:
 		virtual void execute(const CGHeroInstance * hero) const override;
 
@@ -71,4 +68,4 @@ namespace AIPathfinding
 
 		virtual const CGObjectInstance * targetObject() const override;
 	};
-}
+}

+ 3 - 1
AI/Nullkiller/Pathfinding/Actions/SpecialAction.h

@@ -18,6 +18,8 @@ struct AIPathNode;
 class SpecialAction
 {
 public:
+	virtual ~SpecialAction() = default;
+
 	virtual bool canAct(const AIPathNode * source) const
 	{
 		return true;
@@ -39,4 +41,4 @@ public:
 	virtual std::string toString() const = 0;
 
 	virtual const CGObjectInstance * targetObject() const { return nullptr; }
-};
+};

+ 1 - 12
AI/Nullkiller/Pathfinding/Actors.cpp

@@ -269,8 +269,6 @@ ExchangeResult HeroExchangeMap::tryExchangeNoLock(const ChainActor * other)
 			return result; // already inserted
 		}
 
-		auto position = inserted.first;
-
 		auto differentMasks = (actor->chainMask & other->chainMask) == 0;
 
 		if(!differentMasks) return result;
@@ -461,15 +459,6 @@ CCreatureSet * DwellingActor::getDwellingCreatures(const CGDwelling * dwelling,
 			continue;
 
 		auto creature = creatureInfo.second.back().toCreature();
-		auto count = creatureInfo.first;
-
-		if(waitForGrowth)
-		{
-			const CGTownInstance * town = dynamic_cast<const CGTownInstance *>(dwelling);
-
-			count += town ? town->creatureGrowth(creature->level) : creature->growth;
-		}
-
 		dwellingCreatures->addToSlot(
 			dwellingCreatures->getSlotFor(creature),
 			creature->idNumber,
@@ -487,4 +476,4 @@ TownGarrisonActor::TownGarrisonActor(const CGTownInstance * town, uint64_t chain
 std::string TownGarrisonActor::toString() const
 {
 	return town->name;
-}
+}

+ 3 - 2
AI/Nullkiller/Pathfinding/Actors.h

@@ -75,7 +75,8 @@ public:
 	TResources armyCost;
 	std::shared_ptr<TurnInfo> tiCache;
 
-	ChainActor(){}
+	ChainActor() = default;
+	virtual ~ChainActor() = default;
 
 	virtual std::string toString() const;
 	ExchangeResult tryExchangeNoLock(const ChainActor * other) const { return tryExchangeNoLock(this, other); }
@@ -168,4 +169,4 @@ private:
 public:
 	TownGarrisonActor(const CGTownInstance * town, uint64_t chainMask);
 	virtual std::string toString() const override;
-};
+};

+ 0 - 3
AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp

@@ -126,7 +126,6 @@ namespace AIPathfinding
 		const AIPathNode * destinationNode = nodeStorage->getAINode(destination.node);
 		auto questObj = dynamic_cast<const IQuestObject *>(destination.nodeObject);
 		auto questInfo = QuestInfo(questObj->quest, destination.nodeObject, destination.coord);
-		auto nodeHero = pathfinderHelper->hero;
 		QuestAction questAction(questInfo);
 
 		if(destination.nodeObject->ID == Obj::QUEST_GUARD && questObj->quest->missionType == CQuest::MISSION_NONE)
@@ -157,8 +156,6 @@ namespace AIPathfinding
 
 			nodeStorage->updateAINode(destination.node, [&](AIPathNode * node)
 			{
-				auto questInfo = QuestInfo(questObj->quest, destination.nodeObject, destination.coord);
-
 				node->specialAction.reset(new QuestAction(questAction));
 			});
 		}

+ 1 - 5
AI/VCAI/AIhelper.cpp

@@ -19,10 +19,6 @@ AIhelper::AIhelper()
 	armyManager.reset(new ArmyManager());
 }
 
-AIhelper::~AIhelper()
-{
-}
-
 bool AIhelper::notifyGoalCompleted(Goals::TSubgoal goal)
 {
 	return resourceManager->notifyGoalCompleted(goal);
@@ -182,4 +178,4 @@ std::vector<SlotInfo>::iterator AIhelper::getWeakestCreature(std::vector<SlotInf
 std::vector<SlotInfo> AIhelper::getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const
 {
 	return armyManager->getSortedSlots(target, source);
-}
+}

+ 0 - 1
AI/VCAI/AIhelper.h

@@ -36,7 +36,6 @@ class DLL_EXPORT AIhelper : public IResourceManager, public IBuildingManager, pu
 	//TODO: vector<IAbstractManager>
 public:
 	AIhelper();
-	~AIhelper();
 
 	bool canAfford(const TResources & cost) const;
 	TResources reservedResources() const override;

+ 1 - 0
AI/VCAI/ArmyManager.h

@@ -28,6 +28,7 @@ struct SlotInfo
 class DLL_EXPORT IArmyManager //: public: IAbstractManager
 {
 public:
+	virtual ~IArmyManager() = default;
 	virtual void init(CPlayerSpecificInfoCallback * CB) = 0;
 	virtual void setAI(VCAI * AI) = 0;
 	virtual bool canGetArmy(const CArmedInstance * target, const CArmedInstance * source) const = 0;

+ 3 - 8
AI/VCAI/ResourceManager.cpp

@@ -120,14 +120,12 @@ Goals::TSubgoal ResourceManager::collectResourcesForOurGoal(ResourceObjective &o
 		return o.goal;
 	}
 
-	float goalPriority = 10; //arbitrary, will be divided
-	for (const resPair & p : missingResources)
+	for (const resPair p : missingResources)
 	{
 		if (!income[p.first]) //prioritize resources with 0 income
 		{
 			resourceType = p.first;
 			amountToCollect = p.second;
-			goalPriority /= amountToCollect; //need more resources -> lower priority
 			break;
 		}
 	}
@@ -138,7 +136,7 @@ Goals::TSubgoal ResourceManager::collectResourcesForOurGoal(ResourceObjective &o
 		std::map<Res::ERes, float> daysToEarn;
 		for (auto it : missingResources)
 			daysToEarn[it.first] = (float)missingResources[it.first] / income[it.first];
-		auto incomeComparer = [&income](const timePair & lhs, const timePair & rhs) -> bool
+		auto incomeComparer = [](const timePair & lhs, const timePair & rhs) -> bool
 		{
 			//theoretically income can be negative, but that falls into this comparison
 			return lhs.second < rhs.second;
@@ -146,12 +144,9 @@ Goals::TSubgoal ResourceManager::collectResourcesForOurGoal(ResourceObjective &o
 
 		resourceType = boost::max_element(daysToEarn, incomeComparer)->first;
 		amountToCollect = missingResources[resourceType];
-		goalPriority /= daysToEarn[resourceType]; //more days - lower priority
 	}
-	if (resourceType == Res::GOLD)
-		goalPriority *= 1000;
 
-	//this is abstract goal and might take soem time to complete
+	//this is abstract goal and might take some time to complete
 	return Goals::sptr(Goals::CollectRes(resourceType, amountToCollect).setisAbstract(true));
 }
 

+ 2 - 0
CCallback.cpp

@@ -366,10 +366,12 @@ void CCallback::unregisterBattleInterface(std::shared_ptr<IBattleEventsReceiver>
 	cl->additionalBattleInts[*player] -= battleEvents;
 }
 
+#if SCRIPTING_ENABLED
 scripting::Pool * CBattleCallback::getContextPool() const
 {
 	return cl->getGlobalContextPool();
 }
+#endif
 
 CBattleCallback::CBattleCallback(boost::optional<PlayerColor> Player, CClient *C )
 {

+ 4 - 0
CCallback.h

@@ -35,6 +35,8 @@ struct ArtifactLocation;
 class IBattleCallback
 {
 public:
+	virtual ~IBattleCallback() = default;
+
 	bool waitTillRealize; //if true, request functions will return after they are realized by server
 	bool unlockGsWhenWaiting;//if true after sending each request, gs mutex will be unlocked so the changes can be applied; NOTICE caller must have gs mx locked prior to any call to actiob callback!
 	//battle
@@ -99,7 +101,9 @@ public:
 	int battleMakeAction(const BattleAction * action) override;//for casting spells by hero - DO NOT use it for moving active stack
 	bool battleMakeTacticAction(BattleAction * action) override; // performs tactic phase actions
 
+#if SCRIPTING_ENABLED
 	scripting::Pool * getContextPool() const override;
+#endif
 
 	friend class CCallback;
 	friend class CClient;

+ 12 - 2
CMakeLists.txt

@@ -41,8 +41,8 @@ set(VCMI_VERSION_MAJOR 1)
 set(VCMI_VERSION_MINOR 0)
 set(VCMI_VERSION_PATCH 0)
 
-option(ENABLE_ERM "Enable compilation of ERM scripting module" ON)
-option(ENABLE_LUA "Enable compilation of LUA scripting module" ON)
+option(ENABLE_ERM "Enable compilation of ERM scripting module" OFF)
+option(ENABLE_LUA "Enable compilation of LUA scripting module" OFF)
 option(ENABLE_LAUNCHER "Enable compilation of launcher" ON)
 option(ENABLE_TEST "Enable compilation of unit tests" ON)
 if(NOT ${CMAKE_VERSION} VERSION_LESS "3.16.0")
@@ -59,6 +59,11 @@ option(ENABLE_MONOLITHIC_INSTALL "Install everything in single directory on Linu
 set(PACKAGE_NAME_SUFFIX "" CACHE STRING "Suffix for CPack package name")
 set(PACKAGE_FILE_NAME "" CACHE STRING "Override for CPack package filename")
 
+# ERM depends on LUA implicitly
+if(ENABLE_ERM AND NOT ENABLE_LUA)
+	set(ENABLE_LUA ON)
+endif()
+
 ############################################
 #        Miscellaneous options             #
 ############################################
@@ -192,6 +197,7 @@ if(CMAKE_COMPILER_IS_GNUCXX OR NOT WIN32) #so far all *nix compilers support suc
 	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-strict-aliasing -Wno-switch -Wno-sign-compare -Wno-unused-local-typedefs")
 	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-parameter -Wno-overloaded-virtual -Wno-type-limits -Wno-unknown-pragmas")
 	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-reorder")
+	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-varargs") # fuzzylite - Operation.h
 
 	if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
 		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-mismatched-tags -Wno-unknown-warning-option -Wno-missing-braces")
@@ -214,6 +220,10 @@ if(NOT WIN32)
 	endif()
 endif()
 
+if(ENABLE_LUA)
+	add_compile_definitions(SCRIPTING_ENABLED=1)
+endif()
+
 ############################################
 #        Finding packages                  #
 ############################################

+ 1 - 5
CMakePresets.json

@@ -79,11 +79,7 @@
             "name": "macos-arm-conan-ninja-release",
             "displayName": "Ninja+Conan arm64 release",
             "description": "VCMI MacOS-arm64 Ninja using Conan",
-            "inherits": "macos-conan-ninja-release",
-            "cacheVariables": {
-                "ENABLE_ERM": "OFF",
-                "ENABLE_LUA": "OFF"
-            }
+            "inherits": "macos-conan-ninja-release"
         },
         {
             "name": "macos-xcode-release",

+ 19 - 1
ChangeLog

@@ -1,4 +1,22 @@
-0.99 -> 1.0
+1.0.0 -> 1.1.0
+
+GENERAL:
+* Mods and their versions and serialized into save files. Game checks mod compatibility before loading
+* Logs are stored in system default logs directory
+* LUA/ERM libs are not compiled by default
+* FFMpeg dependency is optional now
+
+MODS:
+* Supported rewardable objects customization
+* Battleground obstacles are extendable now with VLC mechanism
+* Introduced "compatibility" section into mods settings
+
+LAUNCHER:
+* Fixed problem with duplicated mods in the list
+* Launcher shows compatible mods only
+* Uninstall button was moved to the left of layout
+
+0.99 -> 1.0.0
 
 GENERAL:
 * Spectator mode was implemented through command-line options

+ 2 - 0
client/CGameInfo.cpp

@@ -72,10 +72,12 @@ const HeroTypeService * CGameInfo::heroTypes() const
 	return globalServices->heroTypes();
 }
 
+#if SCRIPTING_ENABLED
 const scripting::Service * CGameInfo::scripts()  const
 {
 	return globalServices->scripts();
 }
+#endif
 
 const spells::Service * CGameInfo::spells()  const
 {

+ 2 - 0
client/CGameInfo.h

@@ -60,7 +60,9 @@ public:
 	const FactionService * factions() const override;
 	const HeroClassService * heroClasses() const override;
 	const HeroTypeService * heroTypes() const override;
+#if SCRIPTING_ENABLED
 	const scripting::Service * scripts() const override;
+#endif
 	const spells::Service * spells() const override;
 	const SkillService * skills() const override;
 	const BattleFieldService * battlefields() const override;

+ 2 - 0
client/CMT.cpp

@@ -686,6 +686,7 @@ void processCommand(const std::string &message)
 		std::cout << "\rExtracting done :)\n";
 		std::cout << " Extracted files can be found in " << outPath << " directory\n";
 	}
+#if SCRIPTING_ENABLED
 	else if(message=="get scripts")
 	{
 		std::cout << "Command accepted.\t";
@@ -708,6 +709,7 @@ void processCommand(const std::string &message)
 		std::cout << "\rExtracting done :)\n";
 		std::cout << " Extracted files can be found in " << outPath << " directory\n";
 	}
+#endif
 	else if(message=="get txt")
 	{
 		std::cout << "Command accepted.\t";

+ 15 - 0
client/CServerHandler.cpp

@@ -17,6 +17,7 @@
 
 #include "lobby/CSelectionBase.h"
 #include "lobby/CLobbyScreen.h"
+#include "windows/InfoWindows.h"
 
 #include "mainmenu/CMainMenu.h"
 
@@ -161,6 +162,20 @@ void CServerHandler::startLocalServerAndConnect()
 		threadRunLocalServer->join();
 
 	th->update();
+	
+	auto errorMsg = CGI->generaltexth->localizedTexts["server"]["errors"]["existingProcess"].String();
+	try
+	{
+		CConnection testConnection(settings["server"]["server"].String(), getDefaultPort(), NAME, uuid);
+		logNetwork->error("Port is busy, check if another instance of vcmiserver is working");
+		CInfoWindow::showInfoDialog(errorMsg, {});
+		return;
+	}
+	catch(...)
+	{
+		//no connection means that port is not busy and we can start local server
+	}
+	
 #ifdef VCMI_ANDROID
 	{
 		CAndroidVMHelper envHelper;

+ 10 - 0
client/Client.cpp

@@ -263,12 +263,14 @@ void CClient::serialize(BinarySerializer & h, const int version)
 		i->second->saveGame(h, version);
 	}
 
+#if SCRIPTING_ENABLED
 	if(version >= 800)
 	{
 		JsonNode scriptsState;
 		clientScripts->serializeState(h.saving, scriptsState);
 		h & scriptsState;
 	}
+#endif
 }
 
 void CClient::serialize(BinaryDeserializer & h, const int version)
@@ -329,11 +331,13 @@ void CClient::serialize(BinaryDeserializer & h, const int version)
 		LOCPLINT = prevInt;
 	}
 
+#if SCRIPTING_ENABLED
 	{
 		JsonNode scriptsState;
 		h & scriptsState;
 		clientScripts->serializeState(h.saving, scriptsState);
 	}
+#endif
 
 	logNetwork->trace("Loaded client part of save %d ms", CSH->th->getDiff());
 }
@@ -352,7 +356,9 @@ void CClient::save(const std::string & fname)
 
 void CClient::endGame()
 {
+#if SCRIPTING_ENABLED
 	clientScripts.reset();
+#endif
 
 	//suggest interfaces to finish their stuff (AI should interrupt any bg working threads)
 	for(auto & i : playerint)
@@ -732,6 +738,7 @@ PlayerColor CClient::getLocalPlayer() const
 	return getCurrentPlayer();
 }
 
+#if SCRIPTING_ENABLED
 scripting::Pool * CClient::getGlobalContextPool() const
 {
 	return clientScripts.get();
@@ -741,11 +748,14 @@ scripting::Pool * CClient::getContextPool() const
 {
 	return clientScripts.get();
 }
+#endif
 
 void CClient::reinitScripting()
 {
 	clientEventBus = make_unique<events::EventBus>();
+#if SCRIPTING_ENABLED
 	clientScripts.reset(new scripting::PoolImpl(this));
+#endif
 }
 
 

+ 7 - 0
client/Client.h

@@ -39,10 +39,12 @@ namespace boost { class thread; }
 template<typename T> class CApplier;
 class CBaseForCLApply;
 
+#if SCRIPTING_ENABLED
 namespace scripting
 {
 	class PoolImpl;
 }
+#endif
 
 namespace events
 {
@@ -233,13 +235,18 @@ public:
 	void showInfoDialog(InfoWindow * iw) override {};
 	void showInfoDialog(const std::string & msg, PlayerColor player) override {};
 
+#if SCRIPTING_ENABLED
 	scripting::Pool * getGlobalContextPool() const override;
 	scripting::Pool * getContextPool() const override;
+#endif
+
 private:
 	std::map<PlayerColor, std::shared_ptr<CBattleCallback>> battleCallbacks; //callbacks given to player interfaces
 	std::map<PlayerColor, std::shared_ptr<CPlayerEnvironment>> playerEnvironments;
 
+#if SCRIPTING_ENABLED
 	std::shared_ptr<scripting::PoolImpl> clientScripts;
+#endif
 	std::unique_ptr<events::EventBus> clientEventBus;
 
 	std::shared_ptr<CApplier<CBaseForCLApply>> applier;

+ 1 - 2
client/battle/CBattleInterface.cpp

@@ -1083,11 +1083,10 @@ void CBattleInterface::stacksAreAttacked(std::vector<StackAttackedInfo> attacked
 
 	std::array<int, 2> killedBySide = {0, 0};
 
-	int targets = 0, damage = 0;
+	int targets = 0;
 	for(const StackAttackedInfo & attackedInfo : attackedInfos)
 	{
 		++targets;
-		damage += (int)attackedInfo.dmg;
 
 		ui8 side = attackedInfo.defender->side;
 		killedBySide.at(side) += attackedInfo.amountKilled;

+ 3 - 2
client/gui/CAnimation.cpp

@@ -10,10 +10,11 @@
 #include "StdInc.h"
 #include "CAnimation.h"
 
+#include "SDL_Extensions.h"
+#include "SDL_Pixels.h"
+
 #include "../CBitmapHandler.h"
 #include "../Graphics.h"
-#include "../gui/SDL_Extensions.h"
-#include "../gui/SDL_Pixels.h"
 
 #include "../lib/filesystem/Filesystem.h"
 #include "../lib/filesystem/ISimpleResourceLoader.h"

+ 1 - 0
client/gui/CIntObject.h

@@ -140,6 +140,7 @@ public:
 	//double click
 	virtual void onDoubleClick(){}
 
+	// These are the arguments that can be used to determine what kind of input the CIntObject will receive
 	enum {LCLICK=1, RCLICK=2, HOVER=4, MOVE=8, KEYBOARD=16, TIME=32, GENERAL=64, WHEEL=128, DOUBLECLICK=256, TEXTINPUT=512, MCLICK=1024, ALL=0xffff};
 	const ui16 & active;
 	void addUsedEvents(ui16 newActions);

+ 1 - 0
client/gui/SDL_Extensions.h

@@ -155,6 +155,7 @@ typedef void (*BlitterWithRotationVal)(SDL_Surface *src,SDL_Rect srcRect, SDL_Su
 class ColorShifter
 {
 public:
+	virtual ~ColorShifter() = default;
 	virtual SDL_Color shiftColor(SDL_Color clr) const = 0;
 };
 

+ 2 - 1
client/mainmenu/CCampaignScreen.cpp

@@ -9,9 +9,10 @@
  */
 
 #include "StdInc.h"
-#include "../mainmenu/CMainMenu.h"
 #include "CCampaignScreen.h"
 
+#include "CMainMenu.h"
+
 #include "../CGameInfo.h"
 #include "../CMessage.h"
 #include "../CBitmapHandler.h"

+ 2 - 0
client/mainmenu/CCampaignScreen.h

@@ -9,6 +9,8 @@
  */
 #pragma once
 
+#include "../windows/CWindowObject.h"
+
 class CLabel;
 class CPicture;
 class CButton;

+ 3 - 2
client/mainmenu/CreditsScreen.cpp

@@ -9,9 +9,10 @@
  */
 
 #include "StdInc.h"
-
 #include "CreditsScreen.h"
-#include "../mainmenu/CMainMenu.h"
+
+#include "CMainMenu.h"
+
 #include "../gui/CGuiHandler.h"
 #include "../widgets/TextControls.h"
 #include "../widgets/ObjectLists.h"

+ 1 - 2
client/widgets/AdventureMapClasses.cpp

@@ -14,6 +14,7 @@
 
 #include "MiscWidgets.h"
 #include "CComponent.h"
+#include "Images.h"
 
 #include "../CGameInfo.h"
 #include "../CMusicHandler.h"
@@ -26,8 +27,6 @@
 #include "../gui/SDL_Pixels.h"
 #include "../gui/SDL_Compat.h"
 
-#include "../widgets/Images.h"
-
 #include "../windows/InfoWindows.h"
 #include "../windows/CAdvmapInterface.h"
 #include "../windows/GUIClasses.h"

+ 3 - 2
client/widgets/CComponent.cpp

@@ -10,6 +10,9 @@
 #include "StdInc.h"
 #include "CComponent.h"
 
+#include "CArtifactHolder.h"
+#include "Images.h"
+
 #include <vcmi/spells/Service.h>
 #include <vcmi/spells/Spell.h>
 
@@ -18,8 +21,6 @@
 
 #include "../CMessage.h"
 #include "../CGameInfo.h"
-#include "../widgets/Images.h"
-#include "../widgets/CArtifactHolder.h"
 #include "../windows/CAdvmapInterface.h"
 
 #include "../../lib/CArtHandler.h"

+ 3 - 2
client/widgets/CGarrisonInt.cpp

@@ -10,12 +10,13 @@
 #include "StdInc.h"
 #include "CGarrisonInt.h"
 
+#include "Buttons.h"
+#include "TextControls.h"
+
 #include "../gui/CGuiHandler.h"
 
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
-#include "../widgets/Buttons.h"
-#include "../widgets/TextControls.h"
 #include "../windows/CCreatureWindow.h"
 #include "../windows/GUIClasses.h"
 

+ 5 - 5
client/windows/CAdvmapInterface.cpp

@@ -14,8 +14,9 @@
 #include "CHeroWindow.h"
 #include "CKingdomInterface.h"
 #include "CSpellWindow.h"
-#include "GUIClasses.h"
 #include "CTradeWindow.h"
+#include "GUIClasses.h"
+#include "InfoWindows.h"
 
 #include "../CBitmapHandler.h"
 #include "../CGameInfo.h"
@@ -35,7 +36,6 @@
 #include "../gui/CGuiHandler.h"
 #include "../gui/SDL_Extensions.h"
 #include "../widgets/MiscWidgets.h"
-#include "../windows/InfoWindows.h"
 
 #include "../../CCallback.h"
 
@@ -1217,7 +1217,7 @@ void CAdvMapInt::keyPressed(const SDL_KeyboardEvent & key)
 			if(itr != LOCPLINT->towns.end())
 				LOCPLINT->showThievesGuildWindow(*itr);
 			else
-				LOCPLINT->showInfoDialog("No available town with tavern!");
+				LOCPLINT->showInfoDialog(CGI->generaltexth->localizedTexts["adventureMap"]["noTownWithTavern"].String());
 		}
 		return;
 	case SDLK_i:
@@ -1249,7 +1249,7 @@ void CAdvMapInt::keyPressed(const SDL_KeyboardEvent & key)
 	case SDLK_r:
 		if(isActive() && LOCPLINT->ctrlPressed())
 		{
-			LOCPLINT->showYesNoDialog("Are you sure you want to restart game?",
+			LOCPLINT->showYesNoDialog(CGI->generaltexth->localizedTexts["adventureMap"]["confirmRestartGame"].String(),
 				[](){ LOCPLINT->sendCustomEvent(EUserEvent::RESTART_GAME); }, nullptr);
 		}
 		return;
@@ -1308,7 +1308,7 @@ void CAdvMapInt::keyPressed(const SDL_KeyboardEvent & key)
 				if(townWithMarket) //if any town has marketplace, open window
 					GH.pushIntT<CMarketplaceWindow>(townWithMarket);
 				else //if not - complain
-					LOCPLINT->showInfoDialog("No available marketplace!");
+					LOCPLINT->showInfoDialog(CGI->generaltexth->localizedTexts["adventureMap"]["noTownWithMarket"].String());
 			}
 			else if(isActive()) //no ctrl, advmapint is on the top => switch to town
 			{

+ 14 - 5
client/windows/CCastleInterface.cpp

@@ -13,6 +13,7 @@
 #include "CAdvmapInterface.h"
 #include "CHeroWindow.h"
 #include "CTradeWindow.h"
+#include "InfoWindows.h"
 #include "GUIClasses.h"
 #include "QuickRecruitmentWindow.h"
 
@@ -24,7 +25,6 @@
 #include "../Graphics.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/SDL_Extensions.h"
-#include "../windows/InfoWindows.h"
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/CComponent.h"
 
@@ -842,7 +842,16 @@ void CCastleBuildings::enterDwelling(int level)
 
 void CCastleBuildings::enterToTheQuickRecruitmentWindow()
 {
-	GH.pushIntT<QuickRecruitmentWindow>(town, pos);
+	const auto beginIt = town->creatures.cbegin();
+	const auto afterLastIt = town->creatures.size() > GameConstants::CREATURES_PER_TOWN
+		? std::next(beginIt, GameConstants::CREATURES_PER_TOWN)
+		: town->creatures.cend();
+	const auto hasSomeoneToRecruit = std::any_of(beginIt, afterLastIt,
+		[](const auto & creatureInfo) { return creatureInfo.first > 0; });
+	if(hasSomeoneToRecruit)
+		GH.pushIntT<QuickRecruitmentWindow>(town, pos);
+	else
+		CInfoWindow::showInfoDialog(CGI->generaltexth->localizedTexts["townHall"]["noCreaturesToRecruit"].String(), {});
 }
 
 void CCastleBuildings::enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID::EBuildingID upgrades)
@@ -1235,9 +1244,9 @@ void CCastleInterface::recreateIcons()
 	hall = std::make_shared<CTownInfo>(80, 413, town, true);
 	fort = std::make_shared<CTownInfo>(122, 413, town, false);
 
-	fastArmyPurhase = std::make_shared<CButton>(Point(122, 413), "itmcl.def", CButton::tooltip(), [&](){builds->enterToTheQuickRecruitmentWindow();});
-	fastArmyPurhase->setImageOrder(town->fortLevel()-1, town->fortLevel()-1, town->fortLevel()-1, town->fortLevel()-1);
-	fastArmyPurhase->setAnimateLonelyFrame(true);
+	fastArmyPurchase = std::make_shared<CButton>(Point(122, 413), "itmcl.def", CButton::tooltip(), [&](){ builds->enterToTheQuickRecruitmentWindow(); });
+	fastArmyPurchase->setImageOrder(town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1);
+	fastArmyPurchase->setAnimateLonelyFrame(true);
 
 	creainfo.clear();
 

+ 1 - 1
client/windows/CCastleInterface.h

@@ -209,7 +209,7 @@ class CCastleInterface : public CStatusbarWindow, public CGarrisonHolder
 
 	std::shared_ptr<CButton> exit;
 	std::shared_ptr<CButton> split;
-	std::shared_ptr<CButton> fastArmyPurhase;
+	std::shared_ptr<CButton> fastArmyPurchase;
 
 	std::vector<std::shared_ptr<CCreaInfo>> creainfo;//small icons of creatures (bottom-left corner);
 

+ 1 - 1
client/windows/CKingdomInterface.cpp

@@ -12,6 +12,7 @@
 
 #include "CAdvmapInterface.h"
 #include "CCastleInterface.h"
+#include "InfoWindows.h"
 
 #include "../CGameInfo.h"
 #include "../CMT.h"
@@ -19,7 +20,6 @@
 #include "../gui/CGuiHandler.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/MiscWidgets.h"
-#include "../windows/InfoWindows.h"
 
 #include "../../CCallback.h"
 

+ 1 - 1
client/windows/CSpellWindow.cpp

@@ -562,7 +562,7 @@ void CSpellWindow::SpellArea::clickLeft(tribool down, bool previousState)
 				if(!texts.empty())
 					owner->myInt->showInfoDialog(texts.front());
 				else
-					owner->myInt->showInfoDialog("Unknown problem with this spell, no more information available.");
+					owner->myInt->showInfoDialog(CGI->generaltexth->localizedTexts["adventureMap"]["spellUnknownProblem"].String());
 			}
 		}
 		else //adventure spell

+ 3 - 2
client/windows/CWindowObject.cpp

@@ -10,6 +10,8 @@
 #include "StdInc.h"
 #include "CWindowObject.h"
 
+#include "CAdvmapInterface.h"
+
 #include "../widgets/MiscWidgets.h"
 
 #include "../gui/SDL_Pixels.h"
@@ -26,7 +28,6 @@
 #include "../CPlayerInterface.h"
 #include "../CMessage.h"
 #include "../CMusicHandler.h"
-#include "../windows/CAdvmapInterface.h"
 
 #include "../../CCallback.h"
 
@@ -251,4 +252,4 @@ void CStatusbarWindow::activate()
 {
 	CIntObject::activate();
 	GH.statusbar = statusbar;
-}
+}

+ 24 - 3
client/windows/CreaturePurchaseCard.cpp

@@ -17,6 +17,7 @@
 #include "QuickRecruitmentWindow.h"
 #include "../gui/CGuiHandler.h"
 #include "../../lib/CCreatureHandler.h"
+#include "CCreatureWindow.h"
 
 void CreaturePurchaseCard::initButtons()
 {
@@ -46,6 +47,7 @@ void CreaturePurchaseCard::switchCreatureLevel()
 	auto index = vstd::find_pos(upgradesID, creatureOnTheCard->idNumber);
 	auto nextCreatureId = vstd::circularAt(upgradesID, ++index);
 	creatureOnTheCard = nextCreatureId.toCreature();
+	creatureClickArea = std::make_shared<CCreatureClickArea>(Point(pos.x + CCreatureClickArea::CREATURE_X_POS, pos.y + CCreatureClickArea::CREATURE_Y_POS), picture, creatureOnTheCard);
 	picture = std::make_shared<CCreaturePic>(parent->pos.x, parent->pos.y, creatureOnTheCard);
 	parent->updateAllSliders();
 	cost->set(creatureOnTheCard->cost * slider->getValue());
@@ -54,14 +56,14 @@ void CreaturePurchaseCard::switchCreatureLevel()
 void CreaturePurchaseCard::initAmountInfo()
 {
 	availableAmount = std::make_shared<CLabel>(pos.x + 25, pos.y + 146, FONT_SMALL, CENTER, Colors::YELLOW);
-	purhaseAmount = std::make_shared<CLabel>(pos.x + 76, pos.y + 146, FONT_SMALL, CENTER, Colors::WHITE);
+	purchaseAmount = std::make_shared<CLabel>(pos.x + 76, pos.y + 146, FONT_SMALL, CENTER, Colors::WHITE);
 	updateAmountInfo(0);
 }
 
 void CreaturePurchaseCard::updateAmountInfo(int value)
 {
 	availableAmount->setText(boost::lexical_cast<std::string>(maxAmount-value));
-	purhaseAmount->setText(boost::lexical_cast<std::string>(value));
+	purchaseAmount->setText(boost::lexical_cast<std::string>(value));
 }
 
 void CreaturePurchaseCard::initSlider()
@@ -96,8 +98,27 @@ void CreaturePurchaseCard::initView()
 {
 	picture = std::make_shared<CCreaturePic>(pos.x, pos.y, creatureOnTheCard);
 	background = std::make_shared<CPicture>("QuickRecruitmentWindow/CreaturePurchaseCard.png", pos.x-4, pos.y-50);
+	initButtons();
+
+	creatureClickArea = std::make_shared<CCreatureClickArea>(Point(pos.x + CCreatureClickArea::CREATURE_X_POS, pos.y + CCreatureClickArea::CREATURE_Y_POS), picture, creatureOnTheCard);
+
 	initAmountInfo();
 	initSlider();
-	initButtons();
 	initCostBox();
 }
+
+CreaturePurchaseCard::CCreatureClickArea::CCreatureClickArea(const Point & position, const std::shared_ptr<CCreaturePic> creaturePic, const CCreature * creatureOnTheCard)
+	: CIntObject(RCLICK),
+	creatureOnTheCard(creatureOnTheCard)
+{
+	pos.x = position.x;
+	pos.y = position.y;
+	pos.w = CREATURE_WIDTH;
+	pos.h = CREATURE_HEIGHT;
+}
+
+void CreaturePurchaseCard::CCreatureClickArea::clickRight(tribool down, bool previousState)
+{
+	if (down)
+		GH.pushIntT<CStackWindow>(creatureOnTheCard, true);
+}

+ 20 - 1
client/windows/CreaturePurchaseCard.h

@@ -25,6 +25,7 @@ public:
 	QuickRecruitmentWindow * parent;
 	int maxAmount;
 	void sliderMoved(int to);
+
 	CreaturePurchaseCard(const std::vector<CreatureID> & creaturesID, Point position, int creaturesMaxAmount, QuickRecruitmentWindow * parents);
 private:
 	void initView();
@@ -42,10 +43,28 @@ private:
 
 	void initCostBox();
 
+	// This just wraps a clickeable area. There's a weird layout scheme in the file and
+	// it's easier to just add a separate invisble box on top
+	class CCreatureClickArea : public CIntObject
+	{
+	public:
+		CCreatureClickArea(const Point & pos, const std::shared_ptr<CCreaturePic> creaturePic, const CCreature * creatureOnTheCard);
+		void clickRight(tribool down, bool previousState) override;
+		const CCreature * creatureOnTheCard;
+
+		// These are obtained by guessing and checking. I'm not sure how the other numbers
+		// used to set positions were obtained; commit messages don't document it
+		static constexpr int CREATURE_WIDTH = 110;
+		static constexpr int CREATURE_HEIGHT = 132;
+		static constexpr int CREATURE_X_POS = 15;
+		static constexpr int CREATURE_Y_POS = 44;
+	};
+
 	std::shared_ptr<CButton> maxButton, minButton, creatureSwitcher;
-	std::shared_ptr<CLabel> availableAmount,  purhaseAmount;
+	std::shared_ptr<CLabel> availableAmount, purchaseAmount;
 	std::shared_ptr<CCreaturePic> picture;
 	std::shared_ptr<CreatureCostBox> cost;
 	std::vector<CreatureID> upgradesID;
 	std::shared_ptr<CPicture> background;
+	std::shared_ptr<CCreatureClickArea> creatureClickArea;
 };

+ 5 - 5
client/windows/GUIClasses.cpp

@@ -15,6 +15,7 @@
 #include "CCreatureWindow.h"
 #include "CHeroWindow.h"
 #include "CreatureCostBox.h"
+#include "InfoWindows.h"
 
 #include "../CBitmapHandler.h"
 #include "../CGameInfo.h"
@@ -36,7 +37,6 @@
 
 #include "../widgets/CComponent.h"
 #include "../widgets/MiscWidgets.h"
-#include "../windows/InfoWindows.h"
 
 #include "../lobby/CSavingScreen.h"
 
@@ -1286,7 +1286,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2,
 			int skill = hero->secSkills[g].first,
 				level = hero->secSkills[g].second; // <1, 3>
 			secSkillAreas[b].push_back(std::make_shared<LRClickableAreaWTextComp>());
-			secSkillAreas[b][g]->pos = genRect(32, 32, pos.x + 32 + g*36 + b*454 , pos.y + qeLayout ? 83 : 88);
+			secSkillAreas[b][g]->pos = genRect(32, 32, pos.x + 32 + g*36 + b*454 , pos.y + (qeLayout ? 83 : 88));
 			secSkillAreas[b][g]->baseType = 1;
 
 			secSkillAreas[b][g]->type = skill;
@@ -1301,12 +1301,12 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2,
 		heroAreas[b] = std::make_shared<CHeroArea>(257 + 228*b, 13, hero);
 
 		specialtyAreas[b] = std::make_shared<LRClickableAreaWText>();
-		specialtyAreas[b]->pos = genRect(32, 32, pos.x + 69 + 490*b, pos.y + qeLayout ? 41 : 45);
+		specialtyAreas[b]->pos = genRect(32, 32, pos.x + 69 + 490*b, pos.y + (qeLayout ? 41 : 45));
 		specialtyAreas[b]->hoverText = CGI->generaltexth->heroscrn[27];
 		specialtyAreas[b]->text = hero->type->specDescr;
 
 		experienceAreas[b] = std::make_shared<LRClickableAreaWText>();
-		experienceAreas[b]->pos = genRect(32, 32, pos.x + 105 + 490*b, pos.y + qeLayout ? 41 : 45);
+		experienceAreas[b]->pos = genRect(32, 32, pos.x + 105 + 490*b, pos.y + (qeLayout ? 41 : 45));
 		experienceAreas[b]->hoverText = CGI->generaltexth->heroscrn[9];
 		experienceAreas[b]->text = CGI->generaltexth->allTexts[2];
 		boost::algorithm::replace_first(experienceAreas[b]->text, "%d", boost::lexical_cast<std::string>(hero->level));
@@ -1314,7 +1314,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2,
 		boost::algorithm::replace_first(experienceAreas[b]->text, "%d", boost::lexical_cast<std::string>(hero->exp));
 
 		spellPointsAreas[b] = std::make_shared<LRClickableAreaWText>();
-		spellPointsAreas[b]->pos = genRect(32, 32, pos.x + 141 + 490*b, pos.y + qeLayout ? 41 : 45);
+		spellPointsAreas[b]->pos = genRect(32, 32, pos.x + 141 + 490*b, pos.y + (qeLayout ? 41 : 45));
 		spellPointsAreas[b]->hoverText = CGI->generaltexth->heroscrn[22];
 		spellPointsAreas[b]->text = CGI->generaltexth->allTexts[205];
 		boost::algorithm::replace_first(spellPointsAreas[b]->text, "%s", hero->name);

+ 1 - 1
client/windows/GUIClasses.h

@@ -9,13 +9,13 @@
  */
 #pragma once
 
+#include "CWindowObject.h"
 #include "../lib/GameConstants.h"
 #include "../lib/ResourceSet.h"
 #include "../lib/CConfigHandler.h"
 #include "../widgets/CArtifactHolder.h"
 #include "../widgets/CGarrisonInt.h"
 #include "../widgets/Images.h"
-#include "../windows/CWindowObject.h"
 
 class CGDwelling;
 class CreatureCostBox;

+ 2 - 1
client/windows/InfoWindows.cpp

@@ -10,6 +10,8 @@
 #include "StdInc.h"
 #include "InfoWindows.h"
 
+#include "CAdvmapInterface.h"
+
 #include "../CBitmapHandler.h"
 #include "../Graphics.h"
 #include "../CGameInfo.h"
@@ -17,7 +19,6 @@
 #include "../CMessage.h"
 #include "../CMusicHandler.h"
 
-#include "../windows/CAdvmapInterface.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/MiscWidgets.h"
 

+ 4 - 4
client/windows/QuickRecruitmentWindow.cpp

@@ -35,7 +35,7 @@ void QuickRecruitmentWindow::setCancelButton()
 
 void QuickRecruitmentWindow::setBuyButton()
 {
-	buyButton = std::make_shared<CButton>(Point((pos.w/2)-32, 418), "IBY6432.DEF", CButton::tooltip(), [&](){ purhaseUnits(); }, SDLK_RETURN);
+	buyButton = std::make_shared<CButton>(Point((pos.w / 2) - 32, 418), "IBY6432.DEF", CButton::tooltip(), [&](){ purchaseUnits(); }, SDLK_RETURN);
 	cancelButton->assignedKeys.insert(SDLK_ESCAPE);
 	buyButton->setImageOrder(0, 1, 2, 3);
 }
@@ -46,7 +46,7 @@ void QuickRecruitmentWindow::setMaxButton()
 	maxButton->setImageOrder(0, 1, 2, 3);
 }
 
-void QuickRecruitmentWindow::setCreaturePurhaseCards()
+void QuickRecruitmentWindow::setCreaturePurchaseCards()
 {
 	int availableAmount = getAvailableCreatures();
 	Point position = Point((pos.w - 100*availableAmount - 8*(availableAmount-1))/2,64);
@@ -99,7 +99,7 @@ void QuickRecruitmentWindow::maxAllCards(std::vector<std::shared_ptr<CreaturePur
 }
 
 
-void QuickRecruitmentWindow::purhaseUnits()
+void QuickRecruitmentWindow::purchaseUnits()
 {
 	for(auto selected : cards)
 	{
@@ -154,6 +154,6 @@ QuickRecruitmentWindow::QuickRecruitmentWindow(const CGTownInstance * townd, Rec
 
 	initWindow(startupPosition);
 	setButtons();
-	setCreaturePurhaseCards();
+	setCreaturePurchaseCards();
 	maxAllCards(cards);
 }

+ 2 - 2
client/windows/QuickRecruitmentWindow.h

@@ -31,11 +31,11 @@ private:
 	void setBuyButton();
 	void setMaxButton();
 
-	void setCreaturePurhaseCards();
+	void setCreaturePurchaseCards();
 
 	void maxAllCards(std::vector<std::shared_ptr<CreaturePurchaseCard>> cards);
 	void maxAllSlidersAmount(std::vector<std::shared_ptr<CreaturePurchaseCard>> cards);
-	void purhaseUnits();
+	void purchaseUnits();
 
 	const CGTownInstance * town;
 	std::shared_ptr<CButton> maxButton, buyButton, cancelButton;

+ 24 - 12
config/translate.json

@@ -21,8 +21,19 @@
 				"Impossible"
 			]
 		},
+		"confirmRestartGame" : "Are you sure you want to restart game?",
+		"noTownWithMarket": "No available marketplace!",
+		"noTownWithTavern": "No available town with tavern!",
+		"spellUnknownProblem": "Unknown problem with this spell, no more information available.",
 		"playerAttacked" : "Player has been attacked: %s"
 	},
+	"server" :
+	{
+		"errors" :
+		{
+			"existingProcess" : "Another vcmiserver process is running, please terminate it first"
+		}
+	},
 	"systemOptions" :
 	{
 		"fullscreenButton" :
@@ -44,6 +55,7 @@
 	"townHall" :
 	{
 		"missingBase" : "Base building %s must be built first",
+		"noCreaturesToRecruit" : "There are no creatures to recruit!",
 		"greetingManaVortex" : "As you near the %s your body is filled with new energy. You have doubled your normal spell points.",
 		"greetingKnowledge" : "You study the glyphs on the %s and gain insight into the workings of various magics (+1 Knowledge).",
 		"greetingSpellPower" : "The %s teaches you new ways to focus your magical powers (+1 Power).",
@@ -62,18 +74,18 @@
 		"allOf"  :  "All of the following:",
 		"noneOf" : "None of the following:"
 	},
-  "heroWindow": 
-  {
-    "openCommander": 
-    {
-      "label": "Open commander window",
-      "help": "Displays information about commander of this hero"
-    }
-  },
-  "commanderWindow": 
-  {
-    "artifactMessage": "Do you want to give this artifact back to hero?"
-  },
+	"heroWindow" :
+	{
+		"openCommander" :
+		{
+			"label" : "Open commander window",
+			"help" : "Displays information about commander of this hero"
+		}
+	},
+	"commanderWindow":
+	{
+		"artifactMessage": "Do you want to give this artifact back to hero?"
+	},
 	"creatureWindow" :
 	{
 		"showBonuses" :

+ 2 - 0
include/vcmi/ServerCallback.h

@@ -27,6 +27,8 @@ struct CatapultAttack;
 class DLL_LINKAGE ServerCallback
 {
 public:
+	virtual ~ServerCallback() = default;
+
 	virtual void complain(const std::string & problem) = 0;
 	virtual bool describeChanges() const = 0;
 

+ 4 - 0
include/vcmi/Services.h

@@ -32,10 +32,12 @@ namespace spells
 	}
 }
 
+#if SCRIPTING_ENABLED
 namespace scripting
 {
 	class Service;
 }
+#endif
 
 class DLL_LINKAGE Services
 {
@@ -47,7 +49,9 @@ public:
 	virtual const FactionService * factions() const = 0;
 	virtual const HeroClassService * heroClasses() const = 0;
 	virtual const HeroTypeService * heroTypes() const = 0;
+#if SCRIPTING_ENABLED
 	virtual const scripting::Service * scripts() const = 0;
+#endif
 	virtual const spells::Service * spells() const = 0;
 	virtual const SkillService * skills() const = 0;
 	virtual const BattleFieldService * battlefields() const = 0;

+ 2 - 0
include/vcmi/scripting/Service.h

@@ -10,6 +10,7 @@
 
 #pragma once
 
+#if SCRIPTING_ENABLED
 #include <vcmi/Environment.h>
 
 class Services;
@@ -78,3 +79,4 @@ public:
 
 
 }
+#endif

+ 79 - 27
launcher/modManager/cmodlist.cpp

@@ -12,29 +12,53 @@
 
 #include "../../lib/JsonNode.h"
 #include "../../lib/filesystem/CFileInputStream.h"
+#include "../../lib/GameConstants.h"
 
-bool CModEntry::compareVersions(QString lesser, QString greater)
+namespace
 {
-	static const int maxSections = 3; // versions consist from up to 3 sections, major.minor.patch
-
-	QStringList lesserList = lesser.split(".");
-	QStringList greaterList = greater.split(".");
-
-	assert(lesserList.size() <= maxSections);
-	assert(greaterList.size() <= maxSections);
+bool isCompatible(const QString & verMin, const QString & verMax)
+{
+	const int maxSections = 3; // versions consist from up to 3 sections, major.minor.patch
+	QVersionNumber vcmiVersion(GameConstants::VCMI_VERSION_MAJOR,
+							   GameConstants::VCMI_VERSION_MINOR,
+							   GameConstants::VCMI_VERSION_PATCH);
+	
+	auto versionMin = QVersionNumber::fromString(verMin);
+	auto versionMax = QVersionNumber::fromString(verMax);
+	
+	auto buildVersion = [maxSections](QVersionNumber & ver)
+	{
+		if(ver.segmentCount() < maxSections)
+		{
+			auto segments = ver.segments();
+			for(int i = segments.size() - 1; i < maxSections; ++i)
+				segments.append(0);
+			ver = QVersionNumber(segments);
+		}
+	};
 
-	for(int i = 0; i < maxSections; i++)
+	if(!versionMin.isNull())
 	{
-		if(greaterList.size() <= i) // 1.1.1 > 1.1
+		buildVersion(versionMin);
+		if(vcmiVersion < versionMin)
 			return false;
-
-		if(lesserList.size() <= i) // 1.1 < 1.1.1
-			return true;
-
-		if(lesserList[i].toInt() != greaterList[i].toInt())
-			return lesserList[i].toInt() < greaterList[i].toInt(); // 1.1 < 1.2
 	}
-	return false;
+	
+	if(!versionMax.isNull())
+	{
+		buildVersion(versionMax);
+		if(vcmiVersion > versionMax)
+			return false;
+	}
+	return true;
+}
+}
+
+bool CModEntry::compareVersions(QString lesser, QString greater)
+{
+	auto versionLesser = QVersionNumber::fromString(lesser);
+	auto versionGreater = QVersionNumber::fromString(greater);
+	return versionLesser < versionGreater;
 }
 
 QString CModEntry::sizeToString(double size)
@@ -92,6 +116,15 @@ bool CModEntry::isUpdateable() const
 	return false;
 }
 
+bool CModEntry::isCompatible() const
+{
+	if(!isInstalled())
+		return false;
+
+	auto compatibility = localData["compatibility"].toMap();
+	return ::isCompatible(compatibility["min"].toString(), compatibility["max"].toString());
+}
+
 bool CModEntry::isEssential() const
 {
 	return getValue("storedLocaly").toBool();
@@ -102,6 +135,11 @@ bool CModEntry::isInstalled() const
 	return !localData.isEmpty();
 }
 
+bool CModEntry::isValid() const
+{
+	return !localData.isEmpty() || !repository.isEmpty();
+}
+
 int CModEntry::getModStatus() const
 {
 	int status = 0;
@@ -193,7 +231,11 @@ static QVariant getValue(QVariant input, QString path)
 		QString remainder = "/" + path.section('/', 2, -1);
 
 		entryName.remove(0, 1);
-		return getValue(input.toMap().value(entryName), remainder);
+		QMap<QString, QString> keyNormalize;
+		for(auto & key : input.toMap().keys())
+			keyNormalize[key.toLower()] = key;
+
+		return getValue(input.toMap().value(keyNormalize[entryName]), remainder);
 	}
 	else
 	{
@@ -203,6 +245,7 @@ static QVariant getValue(QVariant input, QString path)
 
 CModEntry CModList::getMod(QString modname) const
 {
+	modname = modname.toLower();
 	QVariantMap repo;
 	QVariantMap local = localModList[modname].toMap();
 	QVariantMap settings;
@@ -241,19 +284,28 @@ CModEntry CModList::getMod(QString modname) const
 		}
 	}
 
+	if(settings.value("active").toBool())
+	{
+		auto compatibility = local.value("compatibility").toMap();
+		if(compatibility["min"].isValid() || compatibility["max"].isValid())
+			if(!isCompatible(compatibility["min"].toString(), compatibility["max"].toString()))
+				settings["active"] = false;
+	}
+
+
 	for(auto entry : repositories)
 	{
 		QVariant repoVal = getValue(entry, path);
 		if(repoVal.isValid())
 		{
-			if(repo.empty())
-			{
-				repo = repoVal.toMap();
-			}
-			else
+			auto repoValMap = repoVal.toMap();
+			auto compatibility = repoValMap["compatibility"].toMap();
+			if(isCompatible(compatibility["min"].toString(), compatibility["max"].toString()))
 			{
-				if(CModEntry::compareVersions(repo["version"].toString(), repoVal.toMap()["version"].toString()))
-					repo = repoVal.toMap();
+				if(repo.empty() || CModEntry::compareVersions(repo["version"].toString(), repoValMap["version"].toString()))
+				{
+					repo = repoValMap;
+				}
 			}
 		}
 	}
@@ -297,12 +349,12 @@ QVector<QString> CModList::getModList() const
 	{
 		for(auto it = repo.begin(); it != repo.end(); it++)
 		{
-			knownMods.insert(it.key());
+			knownMods.insert(it.key().toLower());
 		}
 	}
 	for(auto it = localModList.begin(); it != localModList.end(); it++)
 	{
-		knownMods.insert(it.key());
+		knownMods.insert(it.key().toLower());
 	}
 
 	for(auto entry : knownMods)

+ 4 - 0
launcher/modManager/cmodlist.h

@@ -51,6 +51,10 @@ public:
 	bool isInstalled() const;
 	// vcmi essential files
 	bool isEssential() const;
+	// checks if verison is compatible with vcmi
+	bool isCompatible() const;
+	// returns if has any data
+	bool isValid() const;
 
 	// see ModStatus enum
 	int getModStatus() const;

+ 1 - 0
launcher/modManager/cmodlistmodel_moc.cpp

@@ -245,6 +245,7 @@ bool CModFilterModel::filterMatchesThis(const QModelIndex & source) const
 {
 	CModEntry mod = base->getMod(source.data(ModRoles::ModNameRole).toString());
 	return (mod.getModStatus() & filterMask) == filteredType &&
+			mod.isValid() &&
 	       QSortFilterProxyModel::filterAcceptsRow(source.row(), source.parent());
 }
 

+ 9 - 9
launcher/modManager/cmodlistview_moc.ui

@@ -253,7 +253,7 @@
             </property>
             <property name="html">
              <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
-&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;meta charset=&quot;utf-8&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
 p, li { white-space: pre-wrap; }
 &lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Ubuntu'; font-size:10pt; font-weight:400; font-style:normal;&quot;&gt;
 &lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:11pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@@ -386,7 +386,7 @@ p, li { white-space: pre-wrap; }
       </spacer>
      </item>
      <item>
-      <widget class="QPushButton" name="enableButton">
+      <widget class="QPushButton" name="uninstallButton">
        <property name="sizePolicy">
         <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
          <horstretch>0</horstretch>
@@ -406,12 +406,12 @@ p, li { white-space: pre-wrap; }
         </size>
        </property>
        <property name="text">
-        <string>Enable</string>
+        <string>Uninstall</string>
        </property>
       </widget>
      </item>
      <item>
-      <widget class="QPushButton" name="disableButton">
+      <widget class="QPushButton" name="enableButton">
        <property name="sizePolicy">
         <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
          <horstretch>0</horstretch>
@@ -431,12 +431,12 @@ p, li { white-space: pre-wrap; }
         </size>
        </property>
        <property name="text">
-        <string>Disable</string>
+        <string>Enable</string>
        </property>
       </widget>
      </item>
      <item>
-      <widget class="QPushButton" name="updateButton">
+      <widget class="QPushButton" name="disableButton">
        <property name="sizePolicy">
         <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
          <horstretch>0</horstretch>
@@ -456,12 +456,12 @@ p, li { white-space: pre-wrap; }
         </size>
        </property>
        <property name="text">
-        <string>Update</string>
+        <string>Disable</string>
        </property>
       </widget>
      </item>
      <item>
-      <widget class="QPushButton" name="uninstallButton">
+      <widget class="QPushButton" name="updateButton">
        <property name="sizePolicy">
         <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
          <horstretch>0</horstretch>
@@ -481,7 +481,7 @@ p, li { white-space: pre-wrap; }
         </size>
        </property>
        <property name="text">
-        <string>Uninstall</string>
+        <string>Update</string>
        </property>
       </widget>
      </item>

+ 4 - 0
launcher/modManager/cmodmanager.cpp

@@ -169,6 +169,10 @@ bool CModManager::canEnableMod(QString modname)
 	if(!mod.isInstalled())
 		return addError(modname, "Mod must be installed first");
 
+	//check for compatibility
+	if(!mod.isCompatible())
+		return addError(modname, "Mod is not compatible, please update VCMI and checkout latest mod revisions");
+
 	for(auto modEntry : mod.getValue("depends").toStringList())
 	{
 		if(!modList->hasMod(modEntry)) // required mod is not available

+ 2 - 0
lib/CGameInterface.cpp

@@ -126,10 +126,12 @@ std::shared_ptr<CBattleGameInterface> CDynLibHandler::getNewBattleAI(std::string
 	return createAnyAI<CBattleGameInterface>(dllname, "GetNewBattleAI");
 }
 
+#if SCRIPTING_ENABLED
 std::shared_ptr<scripting::Module> CDynLibHandler::getNewScriptingModule(const boost::filesystem::path & dllname)
 {
 	return createAny<scripting::Module>(dllname, "GetNewModule");
 }
+#endif
 
 BattleAction CGlobalAI::activeStack(const CStack * stack)
 {

+ 6 - 0
lib/CGameInterface.h

@@ -56,10 +56,14 @@ class CSaveFile;
 class BinaryDeserializer;
 class BinarySerializer;
 struct ArtifactLocation;
+
+#if SCRIPTING_ENABLED
 namespace scripting
 {
 	class Module;
 }
+#endif
+
 
 class DLL_LINKAGE CBattleGameInterface : public IBattleEventsReceiver
 {
@@ -110,7 +114,9 @@ class DLL_LINKAGE CDynLibHandler
 public:
 	static std::shared_ptr<CGlobalAI> getNewAI(std::string dllname);
 	static std::shared_ptr<CBattleGameInterface> getNewBattleAI(std::string dllname);
+#if SCRIPTING_ENABLED
 	static std::shared_ptr<scripting::Module> getNewScriptingModule(const boost::filesystem::path & dllname);
+#endif
 };
 
 class DLL_LINKAGE CGlobalAI : public CGameInterface // AI class (to derivate)

+ 63 - 0
lib/CModHandler.cpp

@@ -434,7 +434,9 @@ void CContentHandler::init()
 	handlers.insert(std::make_pair("spells", ContentTypeHandler(VLC->spellh, "spell")));
 	handlers.insert(std::make_pair("skills", ContentTypeHandler(VLC->skillh, "skill")));
 	handlers.insert(std::make_pair("templates", ContentTypeHandler((IHandlerBase *)VLC->tplh, "template")));
+#if SCRIPTING_ENABLED
 	handlers.insert(std::make_pair("scripts", ContentTypeHandler(VLC->scriptHandler, "script")));
+#endif
 	handlers.insert(std::make_pair("battlefields", ContentTypeHandler(VLC->battlefieldsHandler, "battlefield")));
 	handlers.insert(std::make_pair("obstacles", ContentTypeHandler(VLC->obstacleHandler, "obstacle")));
 	//TODO: any other types of moddables?
@@ -532,6 +534,51 @@ JsonNode addMeta(JsonNode config, std::string meta)
 	return config;
 }
 
+CModInfo::Version CModInfo::Version::GameVersion()
+{
+	return Version(GameConstants::VCMI_VERSION_MAJOR, GameConstants::VCMI_VERSION_MINOR, GameConstants::VCMI_VERSION_PATCH);
+}
+
+CModInfo::Version CModInfo::Version::fromString(std::string from)
+{
+	int major = 0, minor = 0, patch = 0;
+	try
+	{
+		auto pointPos = from.find('.');
+		major = std::stoi(from.substr(0, pointPos));
+		if(pointPos != std::string::npos)
+		{
+			from = from.substr(pointPos + 1);
+			pointPos = from.find('.');
+			minor = std::stoi(from.substr(0, pointPos));
+			if(pointPos != std::string::npos)
+				patch = std::stoi(from.substr(pointPos + 1));
+		}
+	}
+	catch(const std::invalid_argument & e)
+	{
+		return Version();
+	}
+	return Version(major, minor, patch);
+}
+
+std::string CModInfo::Version::toString() const
+{
+	return std::to_string(major) + '.' + std::to_string(minor) + '.' + std::to_string(patch);
+}
+
+bool CModInfo::Version::compatible(const Version & other, bool checkMinor, bool checkPatch) const
+{
+	return  (major == other.major &&
+			(!checkMinor || minor >= other.minor) &&
+			(!checkPatch || minor > other.minor || (minor == other.minor && patch >= other.patch)));
+}
+
+bool CModInfo::Version::isNull() const
+{
+	return major == 0 && minor == 0 && patch == 0;
+}
+
 CModInfo::CModInfo():
 	checksum(0),
 	enabled(false),
@@ -551,6 +598,12 @@ CModInfo::CModInfo(std::string identifier,const JsonNode & local, const JsonNode
 	validation(PENDING),
 	config(addMeta(config, identifier))
 {
+	version = Version::fromString(config["version"].String());
+	if(!config["compatibility"].isNull())
+	{
+		vcmiCompatibleMin = Version::fromString(config["compatibility"]["min"].String());
+		vcmiCompatibleMax = Version::fromString(config["compatibility"]["max"].String());
+	}
 	loadLocalData(local);
 }
 
@@ -601,6 +654,14 @@ void CModInfo::loadLocalData(const JsonNode & data)
 		validated = data["validated"].Bool();
 		checksum  = strtol(data["checksum"].String().c_str(), nullptr, 16);
 	}
+	
+	//check compatibility
+	bool wasEnabled = enabled;
+	enabled = enabled && (vcmiCompatibleMin.isNull() || Version::GameVersion().compatible(vcmiCompatibleMin));
+	enabled = enabled && (vcmiCompatibleMax.isNull() || vcmiCompatibleMax.compatible(Version::GameVersion()));
+
+	if(wasEnabled && !enabled)
+		logGlobal->warn("Mod %s is incompatible with current version of VCMI and cannot be enabled", name);
 
 	if (enabled)
 		validation = validated ? PASSED : PENDING;
@@ -986,7 +1047,9 @@ void CModHandler::load()
 	for(const TModID & modName : activeMods)
 		content->load(allMods[modName]);
 
+#if SCRIPTING_ENABLED
 	VLC->scriptHandler->performRegistration(VLC);//todo: this should be done before any other handlers load
+#endif
 
 	content->loadCustom();
 

+ 69 - 14
lib/CModHandler.h

@@ -177,6 +177,30 @@ public:
 		FAILED,
 		PASSED
 	};
+	
+	struct Version
+	{
+		int major = 0;
+		int minor = 0;
+		int patch = 0;
+		
+		Version() = default;
+		Version(int mj, int mi, int p): major(mj), minor(mi), patch(p) {}
+		
+		static Version GameVersion();
+		static Version fromString(std::string from);
+		std::string toString() const;
+		
+		bool compatible(const Version & other, bool checkMinor = false, bool checkPatch = false) const;
+		bool isNull() const;
+		
+		template <typename Handler> void serialize(Handler &h, const int version)
+		{
+			h & major;
+			h & minor;
+			h & patch;
+		}
+	};
 
 	/// identifier, identical to name of folder with mod
 	std::string identifier;
@@ -184,6 +208,13 @@ public:
 	/// human-readable strings
 	std::string name;
 	std::string description;
+	
+	/// version of the mod
+	Version version;
+	
+	/// vcmi versions compatible with the mod
+
+	Version vcmiCompatibleMin, vcmiCompatibleMax;
 
 	/// list of mods that should be loaded before this one
 	std::set <TModID> dependencies;
@@ -210,18 +241,6 @@ public:
 	static std::string getModDir(std::string name);
 	static std::string getModFile(std::string name);
 
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & identifier;
-		h & description;
-		h & name;
-		h & dependencies;
-		h & conflicts;
-		h & config;
-		h & checksum;
-		h & validation;
-		h & enabled;
-	}
 private:
 	void loadLocalData(const JsonNode & data);
 };
@@ -256,6 +275,13 @@ class DLL_LINKAGE CModHandler
 	void loadMods(std::string path, std::string parent, const JsonNode & modSettings, bool enableMods);
 	void loadOneMod(std::string modName, std::string parent, const JsonNode & modSettings, bool enableMods);
 public:
+	
+	class Incompatibility: public std::logic_error
+	{
+	public:
+		Incompatibility(const std::string & w): std::logic_error(w)
+		{}
+	};
 
 	CIdentifierStorage identifiers;
 
@@ -336,8 +362,37 @@ public:
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
-		h & allMods;
-		h & activeMods;
+		if(h.saving)
+		{
+			h & activeMods;
+			for(const auto & m : activeMods)
+
+				h & allMods[m].version;
+		}
+		else
+		{
+			loadMods();
+			std::vector<TModID> newActiveMods;
+			h & newActiveMods;
+			for(auto & m : newActiveMods)
+			{
+				if(!allMods.count(m))
+					throw Incompatibility(m + " unkown mod");
+				
+				CModInfo::Version mver;
+				h & mver;
+				if(!allMods[m].version.isNull() && !mver.isNull() && !allMods[m].version.compatible(mver))
+				{
+					std::string err = allMods[m].name +
+					": version needed " + mver.toString() +
+					"but you have installed " + allMods[m].version.toString();
+					throw Incompatibility(err);
+				}
+				allMods[m].enabled = true;
+			}
+			std::swap(activeMods, newActiveMods);
+		}
+				
 		h & settings;
 		h & modules;
 		h & identifiers;

+ 4 - 0
lib/CPathfinder.h

@@ -386,6 +386,9 @@ class DLL_LINKAGE INodeStorage
 {
 public:
 	using ELayer = EPathfindingLayer;
+
+	virtual ~INodeStorage() = default;
+
 	virtual std::vector<CGPathNode *> getInitialNodes() = 0;
 
 	virtual std::vector<CGPathNode *> calculateNeighbours(
@@ -448,6 +451,7 @@ public:
 	PathfinderConfig(
 		std::shared_ptr<INodeStorage> nodeStorage,
 		std::vector<std::shared_ptr<IPathfindingRule>> rules);
+	virtual ~PathfinderConfig() = default;
 
 	virtual CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) = 0;
 };

+ 2 - 0
lib/CScriptingModule.cpp

@@ -11,6 +11,7 @@
 
 #include "CScriptingModule.h"
 
+#if SCRIPTING_ENABLED
 namespace scripting
 {
 
@@ -30,3 +31,4 @@ Module::Module()
 Module::~Module() = default;
 
 }
+#endif

+ 2 - 0
lib/CScriptingModule.h

@@ -9,6 +9,7 @@
  */
 #pragma once
 
+#if SCRIPTING_ENABLED
 #include <vcmi/scripting/Service.h>
 
 namespace spells
@@ -45,3 +46,4 @@ public:
 };
 
 }
+#endif

+ 10 - 2
lib/GameConstants.cpp

@@ -51,10 +51,18 @@ const TeamID TeamID::NO_TEAM = TeamID(255);
 
 namespace GameConstants
 {
+	const int VCMI_VERSION_MAJOR = 1;
+	const int VCMI_VERSION_MINOR = 1;
+	const int VCMI_VERSION_PATCH = 0;
+
+	const std::string VCMI_VERSION_STRING = std::to_string(VCMI_VERSION_MAJOR) + "." +
+											std::to_string(VCMI_VERSION_MINOR) + "." +
+											std::to_string(VCMI_VERSION_PATCH);
+
 #ifdef VCMI_NO_EXTRA_VERSION
-	const std::string VCMI_VERSION = std::string("VCMI 1.0.0");
+	const std::string VCMI_VERSION = std::string("VCMI ") + VCMI_VERSION_STRING;
 #else
-	const std::string VCMI_VERSION = std::string("VCMI 1.0.0.") + GIT_SHA1;
+	const std::string VCMI_VERSION = std::string("VCMI ") + VCMI_VERSION_STRING + "." + GIT_SHA1;
 #endif
 }
 

+ 3 - 0
lib/GameConstants.h

@@ -36,6 +36,9 @@ struct IdTag
 
 namespace GameConstants
 {
+	DLL_LINKAGE extern const int VCMI_VERSION_MAJOR;
+	DLL_LINKAGE extern const int VCMI_VERSION_MINOR;
+	DLL_LINKAGE extern const int VCMI_VERSION_PATCH;
 	DLL_LINKAGE extern const std::string VCMI_VERSION;
 
 	const int PUZZLE_MAP_PIECES = 48;

+ 5 - 2
lib/IGameCallback.h

@@ -27,12 +27,13 @@ class CStackBasicDescriptor;
 class CGCreature;
 struct ShashInt3;
 
+#if SCRIPTING_ENABLED
 namespace scripting
 {
-	class Context;
 	class Pool;
-	class Script;
 }
+#endif
+
 
 class DLL_LINKAGE CPrivilegedInfoCallback : public CGameInfoCallback
 {
@@ -132,7 +133,9 @@ class DLL_LINKAGE IGameCallback : public CPrivilegedInfoCallback, public IGameEv
 public:
 	virtual ~IGameCallback(){};
 
+#if SCRIPTING_ENABLED
 	virtual scripting::Pool * getGlobalContextPool() const = 0;
+#endif
 
 	//get info
 	virtual bool isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero);

+ 2 - 0
lib/ScriptHandler.cpp

@@ -11,6 +11,7 @@
 
 #include "ScriptHandler.h"
 
+#if SCRIPTING_ENABLED
 #include <vcmi/Services.h>
 #include <vcmi/Environment.h>
 
@@ -311,3 +312,4 @@ void ScriptHandler::saveState(JsonNode & state)
 
 
 }
+#endif

+ 2 - 0
lib/ScriptHandler.h

@@ -10,6 +10,7 @@
 
 #pragma once
 
+#if SCRIPTING_ENABLED
 #include <vcmi/scripting/Service.h>
 #include "IHandlerBase.h"
 #include "JsonNode.h"
@@ -131,3 +132,4 @@ private:
 };
 
 }
+#endif

+ 10 - 0
lib/VCMI_Lib.cpp

@@ -82,10 +82,12 @@ const HeroTypeService * LibClasses::heroTypes() const
 	return heroh;
 }
 
+#if SCRIPTING_ENABLED
 const scripting::Service * LibClasses::scripts() const
 {
 	return scriptHandler;
 }
+#endif
 
 const spells::Service * LibClasses::spells() const
 {
@@ -217,7 +219,9 @@ void LibClasses::init(bool onlyEssential)
 
 	createHandler(tplh, "Template", pomtime); //templates need already resolved identifiers (refactor?)
 
+#if SCRIPTING_ENABLED
 	createHandler(scriptHandler, "Script", pomtime);
+#endif
 
 	createHandler(battlefieldsHandler, "Battlefields", pomtime);
 	
@@ -248,7 +252,9 @@ void LibClasses::clear()
 	delete bth;
 	delete tplh;
 	delete terviewh;
+#if SCRIPTING_ENABLED
 	delete scriptHandler;
+#endif
 	delete battlefieldsHandler;
 	makeNull();
 }
@@ -268,7 +274,9 @@ void LibClasses::makeNull()
 	bth = nullptr;
 	tplh = nullptr;
 	terviewh = nullptr;
+#if SCRIPTING_ENABLED
 	scriptHandler = nullptr;
+#endif
 	battlefieldsHandler = nullptr;
 }
 
@@ -289,10 +297,12 @@ void LibClasses::callWhenDeserializing()
 	//modh->loadConfigFromFile ("defaultMods"); //TODO: remember last saved config
 }
 
+#if SCRIPTING_ENABLED
 void LibClasses::scriptsLoaded()
 {
 	scriptHandler->performRegistration(this);
 }
+#endif
 
 LibClasses::~LibClasses()
 {

+ 11 - 3
lib/VCMI_Lib.h

@@ -33,10 +33,13 @@ class CTerrainViewPatternConfig;
 class CRmgTemplateStorage;
 class IHandlerBase;
 
+#if SCRIPTING_ENABLED
 namespace scripting
 {
 	class ScriptHandler;
 }
+#endif
+
 
 /// Loads and constructs several handlers
 class DLL_LINKAGE LibClasses : public Services
@@ -56,7 +59,9 @@ public:
 	const FactionService * factions() const override;
 	const HeroClassService * heroClasses() const override;
 	const HeroTypeService * heroTypes() const override;
+#if SCRIPTING_ENABLED
 	const scripting::Service * scripts() const override;
+#endif
 	const spells::Service * spells() const override;
 	const SkillService * skills() const override;
 	const BattleFieldService * battlefields() const override;
@@ -84,7 +89,9 @@ public:
 	CRmgTemplateStorage * tplh;
 	BattleFieldHandler * battlefieldsHandler;
 	ObstacleHandler * obstacleHandler;
+#if SCRIPTING_ENABLED
 	scripting::ScriptHandler * scriptHandler;
+#endif
 
 	LibClasses(); //c-tor, loads .lods and NULLs handlers
 	~LibClasses();
@@ -94,15 +101,19 @@ public:
 
 	void loadFilesystem(bool onlyEssential);// basic initialization. should be called before init()
 
+#if SCRIPTING_ENABLED
 	void scriptsLoaded();
+#endif
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
+#if SCRIPTING_ENABLED
 		h & scriptHandler;//must be first (or second after modh), it can modify factories other handlers depends on
 		if(!h.saving)
 		{
 			scriptsLoaded();
 		}
+#endif
 
 		h & heroh;
 		h & arth;
@@ -134,9 +145,6 @@ public:
 			callWhenDeserializing();
 		}
 	}
-
-private:
-	void update800();
 };
 
 extern DLL_LINKAGE LibClasses * VLC;

+ 2 - 0
lib/battle/BattleInfo.cpp

@@ -965,12 +965,14 @@ CGHeroInstance * BattleInfo::battleGetFightingHero(ui8 side) const
 	return const_cast<CGHeroInstance*>(CBattleInfoEssentials::battleGetFightingHero(side));
 }
 
+#if SCRIPTING_ENABLED
 scripting::Pool * BattleInfo::getContextPool() const
 {
 	//this is real battle, use global scripting context pool
 	//TODO: make this line not ugly
 	return IObjectInterface::cb->getGlobalContextPool();
 }
+#endif
 
 bool CMP_stack::operator()(const battle::Unit * a, const battle::Unit * b)
 {

+ 2 - 0
lib/battle/BattleInfo.h

@@ -143,7 +143,9 @@ public:
 	ui8 whatSide(PlayerColor player) const;
 
 protected:
+#if SCRIPTING_ENABLED
 	scripting::Pool * getContextPool() const override;
+#endif
 };
 
 

+ 0 - 7
lib/battle/CBattleInfoCallback.h

@@ -23,13 +23,6 @@ struct CObstacleInstance;
 class IBonusBearer;
 class CRandomGenerator;
 
-namespace scripting
-{
-	class Context;
-	class Pool;
-	class Script;
-}
-
 namespace spells
 {
 	class Caster;

+ 4 - 0
lib/battle/IBattleInfoCallback.h

@@ -24,15 +24,19 @@ namespace battle
 	using UnitFilter = std::function<bool(const Unit *)>;
 }
 
+#if SCRIPTING_ENABLED
 namespace scripting
 {
 	class Pool;
 }
+#endif
 
 class DLL_LINKAGE IBattleInfoCallback
 {
 public:
+#if SCRIPTING_ENABLED
 	virtual scripting::Pool * getContextPool() const = 0;
+#endif
 
 	virtual TTerrain battleTerrainType() const = 0;
 	virtual BattleField battleGetBattlefieldType() const = 0;

+ 1 - 3
lib/events/ApplyDamage.cpp

@@ -25,10 +25,8 @@ SubscriptionRegistry<ApplyDamage> * ApplyDamage::getRegistry()
 }
 
 CApplyDamage::CApplyDamage(const Environment * env_, BattleStackAttacked * pack_, std::shared_ptr<battle::Unit> target_)
-	: env(env_),
-	pack(pack_),
+	: pack(pack_),
 	target(target_)
-
 {
 	initalDamage = pack->damageAmount;
 }

+ 0 - 4
lib/events/ApplyDamage.h

@@ -28,12 +28,8 @@ public:
 private:
 	int64_t initalDamage;
 
-	const Environment * env;
 	BattleStackAttacked * pack;
 	std::shared_ptr<battle::Unit> target;
 };
 
 }
-
-
-

+ 1 - 1
lib/filesystem/Filesystem.cpp

@@ -156,7 +156,7 @@ ISimpleResourceLoader * CResourceHandler::createInitial()
 
 void CResourceHandler::initialize()
 {
-	// Create tree-loke structure that looks like this:
+	// Create tree-like structure that looks like this:
 	// root
 	// |
 	// |- initial

+ 2 - 3
lib/mapObjects/CRewardableObject.cpp

@@ -850,17 +850,16 @@ void CGOnceVisitable::initObj(CRandomGenerator & rand)
 	case Obj::WARRIORS_TOMB:
 		{
 			onSelect.addTxt(MetaString::ADVOB_TXT, 161);
+			onVisited.addTxt(MetaString::ADVOB_TXT, 163);
 
-			info.resize(2);
+			info.resize(1);
 			loadRandomArtifact(rand, info[0], 30, 50, 25, 5);
 
 			Bonus bonus(Bonus::ONE_BATTLE, Bonus::MORALE, Bonus::OBJECT, -3, ID);
 			info[0].reward.bonuses.push_back(bonus);
-			info[1].reward.bonuses.push_back(bonus);
 			info[0].limiter.numOfGrants = 1;
 			info[0].message.addTxt(MetaString::ADVOB_TXT, 162);
 			info[0].message.addReplacement(VLC->arth->objects[info[0].reward.artifacts.back()]->getName());
-			info[1].message.addTxt(MetaString::ADVOB_TXT, 163);
 		}
 		break;
 	case Obj::WAGON:

+ 180 - 107
lib/rmg/ObstaclePlacer.cpp

@@ -22,22 +22,10 @@
 #include "CMapGenerator.h"
 #include "../CRandomGenerator.h"
 #include "Functions.h"
+#include "../mapping/CMapEditManager.h"
 
-void ObstaclePlacer::process()
+void ObstacleProxy::collectPossibleObstacles(const Terrain & terrain)
 {
-	auto * manager = zone.getModificator<ObjectManager>();
-	if(!manager)
-		return;
-	
-	auto * riverManager = zone.getModificator<RiverPlacer>();
-	
-	typedef std::vector<std::shared_ptr<const ObjectTemplate>> ObstacleVector;
-	//obstacleVector possibleObstacles;
-	
-	std::map<int, ObstacleVector> obstaclesBySize;
-	typedef std::pair<int, ObstacleVector> ObstaclePair;
-	std::vector<ObstaclePair> possibleObstacles;
-	
 	//get all possible obstacles for this terrain
 	for(auto primaryID : VLC->objtypeh->knownObjects())
 	{
@@ -48,7 +36,7 @@ void ObstaclePlacer::process()
 			{
 				for(auto temp : handler->getTemplates())
 				{
-					if(temp->canBePlacedAt(zone.getTerrainType()) && temp->getBlockMapOffset().valid())
+					if(temp->canBePlacedAt(terrain) && temp->getBlockMapOffset().valid())
 						obstaclesBySize[temp->getBlockedOffsets().size()].push_back(temp);
 				}
 			}
@@ -62,122 +50,169 @@ void ObstaclePlacer::process()
 	{
 		return p1.first > p2.first; //bigger obstacles first
 	});
-	
-	auto blockedArea = zone.area().getSubarea([this](const int3 & t)
+}
+
+int ObstacleProxy::getWeightedObjects(const int3 & tile, const CMap * map, CRandomGenerator & rand, std::list<rmg::Object> & allObjects, std::vector<std::pair<rmg::Object*, int3>> & weightedObjects)
+{
+	int maxWeight = std::numeric_limits<int>::min();
+	for(int i = 0; i < possibleObstacles.size(); ++i)
 	{
-		return map.shouldBeBlocked(t);
-	});
-	blockedArea.subtract(zone.areaUsed());
-	zone.areaPossible().subtract(blockedArea);
-	
-	
-	auto prohibitedArea = zone.freePaths() + zone.areaUsed() + manager->getVisitableArea();
-	
+		if(!possibleObstacles[i].first)
+			continue;
+
+		auto shuffledObstacles = possibleObstacles[i].second;
+		RandomGeneratorUtil::randomShuffle(shuffledObstacles, rand);
+
+		for(auto temp : shuffledObstacles)
+		{
+			auto handler = VLC->objtypeh->getHandlerFor(temp->id, temp->subid);
+			auto obj = handler->create(temp);
+			allObjects.emplace_back(*obj);
+			rmg::Object * rmgObject = &allObjects.back();
+			for(auto & offset : obj->getBlockedOffsets())
+			{
+				rmgObject->setPosition(tile - offset);
+				if(!map->isInTheMap(rmgObject->getPosition()))
+					continue;
+
+				if(!rmgObject->getArea().getSubarea([map](const int3 & t)
+				{
+					return !map->isInTheMap(t);
+				}).empty())
+					continue;
+
+				if(isProhibited(rmgObject->getArea()))
+					continue;
+
+				int coverageBlocked = 0;
+				int coveragePossible = 0;
+				//do not use area intersection in optimization purposes
+				for(auto & t : rmgObject->getArea().getTilesVector())
+				{
+					auto coverage = verifyCoverage(t);
+					if(coverage.first)
+						++coverageBlocked;
+					if(coverage.second)
+						++coveragePossible;
+				}
+
+				int coverageOverlap = possibleObstacles[i].first - coverageBlocked - coveragePossible;
+				int weight = possibleObstacles[i].first + coverageBlocked - coverageOverlap * possibleObstacles[i].first;
+				assert(coverageOverlap >= 0);
+
+				if(weight > maxWeight)
+				{
+					weightedObjects.clear();
+					maxWeight = weight;
+					weightedObjects.emplace_back(rmgObject, rmgObject->getPosition());
+					if(weight > 0)
+						break;
+				}
+				else if(weight == maxWeight)
+					weightedObjects.emplace_back(rmgObject, rmgObject->getPosition());
+
+			}
+		}
+
+		if(maxWeight > 0)
+			break;
+	}
+
+	return maxWeight;
+}
+
+void ObstacleProxy::placeObstacles(CMap * map, CRandomGenerator & rand)
+{
 	//reverse order, since obstacles begin in bottom-right corner, while the map coordinates begin in top-left
 	auto blockedTiles = blockedArea.getTilesVector();
 	int tilePos = 0;
+	std::set<CGObjectInstance*> objs;
+
 	while(!blockedArea.empty() && tilePos < blockedArea.getTilesVector().size())
 	{
 		auto tile = blockedArea.getTilesVector()[tilePos];
-		
+
 		std::list<rmg::Object> allObjects;
-		std::vector<std::pair<rmg::Object*, int3>> weightedObjects; //obj + position
-		int maxWeight = std::numeric_limits<int>::min();
-		for(int i = 0; i < possibleObstacles.size(); ++i)
-		{
-			if(!possibleObstacles[i].first)
-				continue;
-			
-			auto shuffledObstacles = possibleObstacles[i].second;
-			RandomGeneratorUtil::randomShuffle(shuffledObstacles, generator.rand);
-			
-			for(auto & temp : shuffledObstacles)
-			{
-				auto handler = VLC->objtypeh->getHandlerFor(temp->id, temp->subid);
-				auto obj = handler->create(temp);
-				allObjects.emplace_back(*obj);
-				rmg::Object * rmgObject = &allObjects.back();
-				for(auto & offset : obj->getBlockedOffsets())
-				{
-					rmgObject->setPosition(tile - offset);
-					if(!map.isOnMap(rmgObject->getPosition()))
-						continue;
-					
-					if(!rmgObject->getArea().getSubarea([this](const int3 & t)
-					{
-						return !map.isOnMap(t);
-					}).empty())
-						continue;
-					
-					if(prohibitedArea.overlap(rmgObject->getArea()))
-						continue;
-					
-					if(!zone.area().contains(rmgObject->getArea()))
-						continue;
-					
-					int coverageBlocked = 0;
-					int coveragePossible = 0;
-					//do not use area intersection in optimization purposes
-					for(auto & t : rmgObject->getArea().getTilesVector())
-					{
-						if(map.shouldBeBlocked(t))
-							++coverageBlocked;
-						if(zone.areaPossible().contains(t))
-							++coveragePossible;
-					}
-					
-					int coverageOverlap = possibleObstacles[i].first - coverageBlocked - coveragePossible;
-					int weight = possibleObstacles[i].first + coverageBlocked - coverageOverlap * possibleObstacles[i].first;
-					assert(coverageOverlap >= 0);
-					
-					if(weight > maxWeight)
-					{
-						weightedObjects.clear();
-						maxWeight = weight;
-						weightedObjects.emplace_back(rmgObject, rmgObject->getPosition());
-						if(weight > 0)
-							break;
-					}
-					else if(weight == maxWeight)
-						weightedObjects.emplace_back(rmgObject, rmgObject->getPosition());
-					
-				}
-			}
-			
-			if(maxWeight > 0)
-				break;
-		}
-		
+		std::vector<std::pair<rmg::Object*, int3>> weightedObjects;
+		int maxWeight = getWeightedObjects(tile, map, rand, allObjects, weightedObjects);
+
 		if(weightedObjects.empty())
 		{
 			tilePos += 1;
 			continue;
 		}
-		
-		auto objIter = RandomGeneratorUtil::nextItem(weightedObjects, generator.rand);
+
+		auto objIter = RandomGeneratorUtil::nextItem(weightedObjects, rand);
 		objIter->first->setPosition(objIter->second);
-		manager->placeObject(*objIter->first, false, false);
+		placeObject(*objIter->first, objs);
+
 		blockedArea.subtract(objIter->first->getArea());
 		tilePos = 0;
-		
-		//river processing
-		if(riverManager)
-		{
-			if(objIter->first->instances().front()->object().typeName == "mountain")
-				riverManager->riverSource().unite(objIter->first->getArea());
-			if(objIter->first->instances().front()->object().typeName == "lake")
-				riverManager->riverSink().unite(objIter->first->getArea());
-		}
-		
+
+		postProcess(*objIter->first);
+
 		if(maxWeight < 0)
 			logGlobal->warn("Placed obstacle with negative weight at %s", objIter->second.toString());
-		
+
 		for(auto & o : allObjects)
 		{
 			if(&o != objIter->first)
 				o.clear();
 		}
 	}
+
+	finalInsertion(map->getEditManager(), objs);
+}
+
+void ObstacleProxy::finalInsertion(CMapEditManager * manager, std::set<CGObjectInstance*> & instances)
+{
+	manager->insertObjects(instances); //insert as one operation - for undo purposes
+}
+
+std::pair<bool, bool> ObstacleProxy::verifyCoverage(const int3 & t) const
+{
+	return {blockedArea.contains(t), false};
+}
+
+void ObstacleProxy::placeObject(rmg::Object & object, std::set<CGObjectInstance*> & instances)
+{
+	for (auto * instance : object.instances())
+	{
+		instances.insert(&instance->object());
+	}
+}
+
+void ObstacleProxy::postProcess(const rmg::Object & object)
+{
+}
+
+bool ObstacleProxy::isProhibited(const rmg::Area & objArea) const
+{
+	return false;
+}
+
+
+
+void ObstaclePlacer::process()
+{
+	manager = zone.getModificator<ObjectManager>();
+	if(!manager)
+		return;
+	
+	riverManager = zone.getModificator<RiverPlacer>();
+	
+	collectPossibleObstacles(zone.getTerrainType());
+	
+	blockedArea = zone.area().getSubarea([this](const int3 & t)
+	{
+		return map.shouldBeBlocked(t);
+	});
+	blockedArea.subtract(zone.areaUsed());
+	zone.areaPossible().subtract(blockedArea);
+	
+	prohibitedArea = zone.freePaths() + zone.areaUsed() + manager->getVisitableArea();
+		
+	placeObstacles(&map.map(), generator.rand);
 }
 
 void ObstaclePlacer::init()
@@ -189,3 +224,41 @@ void ObstaclePlacer::init()
 	DEPENDENCY(RoadPlacer);
 	DEPENDENCY_ALL(RockPlacer);
 }
+
+std::pair<bool, bool> ObstaclePlacer::verifyCoverage(const int3 & t) const
+{
+	return {map.shouldBeBlocked(t), zone.areaPossible().contains(t)};
+}
+
+void ObstaclePlacer::placeObject(rmg::Object & object, std::set<CGObjectInstance*> &)
+{
+	manager->placeObject(object, false, false);
+}
+
+void ObstaclePlacer::postProcess(const rmg::Object & object)
+{
+	//river processing
+	if(riverManager)
+	{
+		const auto objTypeName = object.instances().front()->object().typeName;
+		if(objTypeName == "mountain")
+			riverManager->riverSource().unite(object.getArea());
+		else if(objTypeName == "lake")
+			riverManager->riverSink().unite(object.getArea());
+	}
+}
+
+bool ObstaclePlacer::isProhibited(const rmg::Area & objArea) const
+{
+	if(prohibitedArea.overlap(objArea))
+		return true;
+	 
+	if(!zone.area().contains(objArea))
+		return true;
+	
+	return false;
+}
+
+void ObstaclePlacer::finalInsertion(CMapEditManager *, std::set<CGObjectInstance*> &)
+{
+}

+ 51 - 1
lib/rmg/ObstaclePlacer.h

@@ -11,11 +11,61 @@
 #pragma once
 #include "Zone.h"
 
-class ObstaclePlacer: public Modificator
+class CMap;
+class CMapEditManager;
+class RiverPlacer;
+class ObjectManager;
+class DLL_LINKAGE ObstacleProxy
+{
+public:
+	ObstacleProxy() = default;
+	virtual ~ObstacleProxy() = default;
+
+	rmg::Area blockedArea;
+
+	void collectPossibleObstacles(const Terrain & terrain);
+
+	void placeObstacles(CMap * map, CRandomGenerator & rand);
+
+	virtual std::pair<bool, bool> verifyCoverage(const int3 & t) const;
+
+	virtual void placeObject(rmg::Object & object, std::set<CGObjectInstance*> & instances);
+
+	virtual void postProcess(const rmg::Object & object);
+
+	virtual bool isProhibited(const rmg::Area & objArea) const;
+	
+	virtual void finalInsertion(CMapEditManager * manager, std::set<CGObjectInstance*> & instances);
+
+protected:
+	int getWeightedObjects(const int3 & tile, const CMap * map, CRandomGenerator & rand, std::list<rmg::Object> & allObjects, std::vector<std::pair<rmg::Object*, int3>> & weightedObjects);
+
+	typedef std::vector<std::shared_ptr<const ObjectTemplate>> ObstacleVector;
+	std::map<int, ObstacleVector> obstaclesBySize;
+	typedef std::pair<int, ObstacleVector> ObstaclePair;
+	std::vector<ObstaclePair> possibleObstacles;
+};
+
+class ObstaclePlacer: public Modificator, public ObstacleProxy
 {
 public:
 	MODIFICATOR(ObstaclePlacer);
 	
 	void process() override;
 	void init() override;
+	
+	std::pair<bool, bool> verifyCoverage(const int3 & t) const override;
+	
+	void placeObject(rmg::Object & object, std::set<CGObjectInstance*> & instances) override;
+	
+	void postProcess(const rmg::Object & object) override;
+	
+	bool isProhibited(const rmg::Area & objArea) const override;
+	
+	void finalInsertion(CMapEditManager * manager, std::set<CGObjectInstance*> & instances) override;
+	
+private:
+	rmg::Area prohibitedArea;
+	RiverPlacer * riverManager;
+	ObjectManager * manager;
 };

+ 1 - 5
lib/rmg/Zone.cpp

@@ -193,11 +193,7 @@ void Zone::fractalize()
 	rmg::Area clearedTiles(dAreaFree);
 	rmg::Area possibleTiles(dAreaPossible);
 	rmg::Area tilesToIgnore; //will be erased in this iteration
-	
-	//the more treasure density, the greater distance between paths. Scaling is experimental.
-	int totalDensity = 0;
-	for(auto ti : treasureInfo)
-		totalDensity += ti.density;
+
 	const float minDistance = 10 * 10; //squared
 	
 	if(type != ETemplateZoneType::JUNCTION)

+ 2 - 0
lib/spells/ISpellMechanics.cpp

@@ -720,10 +720,12 @@ const CreatureService * BaseMechanics::creatures() const
 	return VLC->creatures(); //todo: redirect
 }
 
+#if SCRIPTING_ENABLED
 const scripting::Service * BaseMechanics::scripts() const
 {
 	return VLC->scripts(); //todo: redirect
 }
+#endif
 
 const Service * BaseMechanics::spells() const
 {

+ 6 - 0
lib/spells/ISpellMechanics.h

@@ -35,10 +35,12 @@ namespace vstd
 	class RNG;
 }
 
+#if SCRIPTING_ENABLED
 namespace scripting
 {
 	class Service;
 }
+#endif
 
 
 ///callback to be provided by server
@@ -238,7 +240,9 @@ public:
 
 	//Global environment facade
 	virtual const CreatureService * creatures() const = 0;
+#if SCRIPTING_ENABLED
 	virtual const scripting::Service * scripts() const = 0;
+#endif
 	virtual const Service * spells() const = 0;
 
 	virtual const IGameInfoCallback * game() const = 0;
@@ -296,7 +300,9 @@ public:
 	std::vector<AimType> getTargetTypes() const override;
 
 	const CreatureService * creatures() const override;
+#if SCRIPTING_ENABLED
 	const scripting::Service * scripts() const override;
+#endif
 	const Service * spells() const override;
 
 	const IGameInfoCallback * game() const override;

+ 59 - 35
server/CGameHandler.cpp

@@ -1048,17 +1048,19 @@ void CGameHandler::makeAttack(const CStack * attacker, const CStack * defender,
 			bat.flags |= BattleAttack::BALLISTA_DOUBLE_DMG;
 		}
 	}
+
+	int64_t drainedLife = 0;
+
 	// only primary target
 	if(defender->alive())
-		applyBattleEffects(bat, blm, attackerState, fireShield, defender, distance, false);
+		drainedLife += applyBattleEffects(bat, attackerState, fireShield, defender, distance, false);
 
 	//multiple-hex normal attack
 	std::set<const CStack*> attackedCreatures = gs->curB->getAttackedCreatures(attacker, targetHex, bat.shot()); //creatures other than primary target
-
 	for(const CStack * stack : attackedCreatures)
 	{
 		if(stack != defender && stack->alive()) //do not hit same stack twice
-			applyBattleEffects(bat, blm, attackerState, fireShield, stack, distance, true);
+			drainedLife += applyBattleEffects(bat, attackerState, fireShield, stack, distance, true);
 	}
 
 	std::shared_ptr<const Bonus> bonus = attacker->getBonusLocalFirst(Selector::type()(Bonus::SPELL_LIKE_ATTACK));
@@ -1086,7 +1088,7 @@ void CGameHandler::makeAttack(const CStack * attacker, const CStack * defender,
 		{
 			if(stack != defender && stack->alive()) //do not hit same stack twice
 			{
-				applyBattleEffects(bat, blm, attackerState, fireShield, stack, distance, true);
+				drainedLife += applyBattleEffects(bat, attackerState, fireShield, stack, distance, true);
 			}
 		}
 
@@ -1134,7 +1136,28 @@ void CGameHandler::makeAttack(const CStack * attacker, const CStack * defender,
 
 		addGenericKilledLog(blm, defender, totalKills, multipleTargets);
 	}
-	sendAndApply(&blm);
+
+	// drain life effect (as well as log entry) must be applied after the attack
+	if(drainedLife > 0)
+	{
+		BattleAttack bat;
+		bat.stackAttacking = attacker->unitId();
+		{
+			CustomEffectInfo customEffect;
+			customEffect.sound = soundBase::DRAINLIF;
+			customEffect.effect = 52;
+			customEffect.stack = attackerState->unitId();
+			bat.customEffects.push_back(std::move(customEffect));
+		}
+		sendAndApply(&bat);
+
+		MetaString text;
+		attackerState->addText(text, MetaString::GENERAL_TXT, 361);
+		attackerState->addNameReplacement(text, false);
+		text.addReplacement(drainedLife);
+		defender->addNameReplacement(text, true);
+		blm.lines.push_back(std::move(text));
+	}
 
 	if(!fireShield.empty())
 	{
@@ -1174,12 +1197,24 @@ void CGameHandler::makeAttack(const CStack * attacker, const CStack * defender,
 		StacksInjured pack;
 		pack.stacks.push_back(bsa);
 		sendAndApply(&pack);
-		sendGenericKilledLog(attacker, bsa.killedAmount, false);
+
+		// TODO: this is already implemented in Damage::describeEffect()
+		{
+			MetaString text;
+			text.addTxt(MetaString::GENERAL_TXT, 376);
+			text.addReplacement(MetaString::SPELL_NAME, SpellID::FIRE_SHIELD);
+			text.addReplacement(totalDamage);
+			blm.lines.push_back(std::move(text));
+		}
+		addGenericKilledLog(blm, attacker, bsa.killedAmount, false);
 	}
 
+	sendAndApply(&blm);
+
 	handleAfterAttackCasting(ranged, attacker, defender);
 }
-void CGameHandler::applyBattleEffects(BattleAttack & bat, BattleLogMessage & blm, std::shared_ptr<battle::CUnitState> attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary)
+
+int64_t CGameHandler::applyBattleEffects(BattleAttack & bat, std::shared_ptr<battle::CUnitState> attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary)
 {
 	BattleStackAttacked bsa;
 	if(secondary)
@@ -1208,34 +1243,14 @@ void CGameHandler::applyBattleEffects(BattleAttack & bat, BattleLogMessage & blm
 		CStack::prepareAttacked(bsa, getRandomGenerator(), bai.defender->acquireState()); //calculate casualties
 	}
 
-	auto addLifeDrain = [&](int64_t & toHeal, EHealLevel level, EHealPower power)
-	{
-		attackerState->heal(toHeal, level, power);
-
-		if(toHeal > 0)
-		{
-			CustomEffectInfo customEffect;
-			customEffect.sound = soundBase::DRAINLIF;
-			customEffect.effect = 52;
-			customEffect.stack = attackerState->unitId();
-			bat.customEffects.push_back(customEffect);
-
-			MetaString text;
-			attackerState->addText(text, MetaString::GENERAL_TXT, 361);
-			attackerState->addNameReplacement(text, false);
-			text.addReplacement((int)toHeal);
-			def->addNameReplacement(text, true);
-			blm.lines.push_back(text);
-		}
-	};
+	int64_t drainedLife = 0;
 
 	//life drain handling
 	if(attackerState->hasBonusOfType(Bonus::LIFE_DRAIN) && def->isLiving())
 	{
 		int64_t toHeal = bsa.damageAmount * attackerState->valOfBonuses(Bonus::LIFE_DRAIN) / 100;
-
-		if(toHeal > 0)
-			addLifeDrain(toHeal, EHealLevel::RESURRECT, EHealPower::PERMANENT);
+		attackerState->heal(toHeal, EHealLevel::RESURRECT, EHealPower::PERMANENT);
+		drainedLife += toHeal;
 	}
 
 	//soul steal handling
@@ -1248,7 +1263,8 @@ void CGameHandler::applyBattleEffects(BattleAttack & bat, BattleLogMessage & blm
 			if(attackerState->hasBonusOfType(Bonus::SOUL_STEAL, subtype))
 			{
 				int64_t toHeal = bsa.killedAmount * attackerState->valOfBonuses(Bonus::SOUL_STEAL, subtype) * attackerState->MaxHealth();
-				addLifeDrain(toHeal, EHealLevel::OVERHEAL, ((subtype == 0) ? EHealPower::ONE_BATTLE : EHealPower::PERMANENT));
+				attackerState->heal(toHeal, EHealLevel::OVERHEAL, ((subtype == 0) ? EHealPower::ONE_BATTLE : EHealPower::PERMANENT));
+				drainedLife += toHeal;
 				break;
 			}
 		}
@@ -1263,6 +1279,8 @@ void CGameHandler::applyBattleEffects(BattleAttack & bat, BattleLogMessage & blm
 		auto fireShieldDamage = (std::min<int64_t>(def->getAvailableHealth(), bsa.damageAmount) * def->valOfBonuses(Bonus::FIRE_SHIELD)) / 100;
 		fireShield.push_back(std::make_pair(def, fireShieldDamage));
 	}
+
+	return drainedLife;
 }
 
 void CGameHandler::sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple)
@@ -1297,8 +1315,8 @@ void CGameHandler::addGenericKilledLog(BattleLogMessage & blm, const CStack * de
 			txt % (multiple ? VLC->generaltexth->allTexts[42] : defender->getCreature()->nameSing); // creature perishes
 		}
 		MetaString line;
-		line.addReplacement(txt.str());
-		blm.lines.push_back(line);
+		line << txt.str();
+		blm.lines.push_back(std::move(line));
 	}
 }
 
@@ -1652,7 +1670,9 @@ CGameHandler::~CGameHandler()
 void CGameHandler::reinitScripting()
 {
 	serverEventBus = make_unique<events::EventBus>();
+#if SCRIPTING_ENABLED
 	serverScripts.reset(new scripting::PoolImpl(this, spellEnv));
+#endif
 }
 
 void CGameHandler::init(StartInfo *si)
@@ -2112,7 +2132,9 @@ void CGameHandler::run(bool resume)
 		logGlobal->info(sbuffer.str());
 	}
 
+#if SCRIPTING_ENABLED
 	services()->scripts()->run(serverScripts);
+#endif
 
 	if(resume)
 		events::GameResumed::defaultExecute(serverEventBus.get());
@@ -5585,7 +5607,7 @@ bool CGameHandler::isAllowedExchange(ObjectInstanceID id1, ObjectInstanceID id2)
 			auto topArmy = dialog->exchangingArmies.at(0);
 			auto bottomArmy = dialog->exchangingArmies.at(1);
 
-			if (topArmy == o1 && bottomArmy == o2 || bottomArmy == o1 && topArmy == o2)
+			if ((topArmy == o1 && bottomArmy == o2) || (bottomArmy == o1 && topArmy == o2))
 				return true;
 		}
 	}
@@ -7319,15 +7341,17 @@ CRandomGenerator & CGameHandler::getRandomGenerator()
 	return CRandomGenerator::getDefault();
 }
 
+#if SCRIPTING_ENABLED
 scripting::Pool * CGameHandler::getGlobalContextPool() const
 {
 	return serverScripts.get();
 }
 
-scripting::Pool *  CGameHandler::getContextPool() const
+scripting::Pool * CGameHandler::getContextPool() const
 {
 	return serverScripts.get();
 }
+#endif
 
 const ObjectInstanceID CGameHandler::putNewObject(Obj ID, int subID, int3 pos)
 {

+ 10 - 1
server/CGameHandler.h

@@ -34,10 +34,12 @@ class IMarket;
 
 class SpellCastEnvironment;
 
+#if SCRIPTING_ENABLED
 namespace scripting
 {
 	class PoolImpl;
 }
+#endif
 
 
 template<typename T> class CApplier;
@@ -126,7 +128,8 @@ public:
 
 	void makeAttack(const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter);
 
-	void applyBattleEffects(BattleAttack & bat, BattleLogMessage & blm, std::shared_ptr<battle::CUnitState> attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary); //damage, drain life & fire shield
+	// damage, drain life & fire shield; returns amount of drained life
+	int64_t applyBattleEffects(BattleAttack & bat, std::shared_ptr<battle::CUnitState> attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary);
 
 	void sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple);
 	void addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple);
@@ -274,12 +277,14 @@ public:
 		h & finishingBattle;
 		h & getRandomGenerator();
 
+#if SCRIPTING_ENABLED
 		JsonNode scriptsState;
 		if(h.saving)
 			serverScripts->serializeState(h.saving, scriptsState);
 		h & scriptsState;
 		if(!h.saving)
 			serverScripts->serializeState(h.saving, scriptsState);
+#endif
 	}
 
 	void sendMessageToAll(const std::string &message);
@@ -326,13 +331,17 @@ public:
 
 	CRandomGenerator & getRandomGenerator();
 
+#if SCRIPTING_ENABLED
 	scripting::Pool * getGlobalContextPool() const override;
 	scripting::Pool * getContextPool() const override;
+#endif
 
 	friend class CVCMIServer;
 private:
 	std::unique_ptr<events::EventBus> serverEventBus;
+#if SCRIPTING_ENABLED
 	std::shared_ptr<scripting::PoolImpl> serverScripts;
+#endif
 
 	void reinitScripting();