浏览代码

Merge branch 'develop' into joystick_support

kdmcser 1 年之前
父节点
当前提交
92b1c8deb5
共有 100 个文件被更改,包括 1463 次插入384 次删除
  1. 4 0
      .gitmodules
  2. 0 4
      AI/BattleAI/BattleAI.cpp
  3. 12 0
      AI/BattleAI/BattleEvaluator.cpp
  4. 1 1
      AI/EmptyAI/CEmptyAI.cpp
  5. 1 1
      AI/EmptyAI/CEmptyAI.h
  6. 33 13
      AI/Nullkiller/AIGateway.cpp
  7. 1 1
      AI/Nullkiller/AIGateway.h
  8. 17 26
      AI/Nullkiller/AIUtility.cpp
  9. 2 1
      AI/Nullkiller/AIUtility.h
  10. 5 5
      AI/Nullkiller/Analyzers/BuildAnalyzer.cpp
  11. 3 3
      AI/Nullkiller/Analyzers/BuildAnalyzer.h
  12. 8 1
      AI/Nullkiller/Analyzers/HeroManager.cpp
  13. 118 24
      AI/Nullkiller/Analyzers/ObjectClusterizer.cpp
  14. 24 5
      AI/Nullkiller/Analyzers/ObjectClusterizer.h
  15. 2 2
      AI/Nullkiller/Behaviors/BuildingBehavior.cpp
  16. 1 1
      AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp
  17. 1 1
      AI/Nullkiller/Behaviors/ClusterBehavior.cpp
  18. 1 1
      AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp
  19. 1 1
      AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp
  20. 9 1
      AI/Nullkiller/Behaviors/StartupBehavior.cpp
  21. 2 6
      AI/Nullkiller/Engine/DeepDecomposer.cpp
  22. 1 1
      AI/Nullkiller/Engine/DeepDecomposer.h
  23. 13 2
      AI/Nullkiller/Engine/FuzzyHelper.cpp
  24. 179 85
      AI/Nullkiller/Engine/Nullkiller.cpp
  25. 22 3
      AI/Nullkiller/Engine/Nullkiller.h
  26. 22 21
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  27. 1 1
      AI/Nullkiller/Engine/PriorityEvaluator.h
  28. 14 27
      AI/Nullkiller/Engine/Settings.cpp
  29. 5 6
      AI/Nullkiller/Engine/Settings.h
  30. 5 3
      AI/Nullkiller/Goals/AbstractGoal.cpp
  31. 12 5
      AI/Nullkiller/Goals/AbstractGoal.h
  32. 3 3
      AI/Nullkiller/Goals/AdventureSpellCast.cpp
  33. 1 1
      AI/Nullkiller/Goals/AdventureSpellCast.h
  34. 29 2
      AI/Nullkiller/Goals/CGoal.h
  35. 1 1
      AI/Nullkiller/Goals/CompleteQuest.cpp
  36. 32 0
      AI/Nullkiller/Goals/Composition.cpp
  37. 3 0
      AI/Nullkiller/Goals/Composition.h
  38. 1 1
      AI/Nullkiller/Goals/DigAtTile.cpp
  39. 4 4
      AI/Nullkiller/Goals/DismissHero.cpp
  40. 5 1
      AI/Nullkiller/Goals/DismissHero.h
  41. 20 0
      AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp
  42. 3 0
      AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h
  43. 39 0
      AI/Nullkiller/Goals/ExecuteHeroChain.cpp
  44. 3 0
      AI/Nullkiller/Goals/ExecuteHeroChain.h
  45. 1 1
      AI/Nullkiller/Goals/StayAtTown.cpp
  46. 1 1
      AI/Nullkiller/Markers/ArmyUpgrade.cpp
  47. 1 1
      AI/Nullkiller/Markers/DefendTown.cpp
  48. 2 2
      AI/Nullkiller/Markers/HeroExchange.cpp
  49. 1 1
      AI/Nullkiller/Markers/UnlockCluster.h
  50. 4 0
      AI/Nullkiller/Pathfinding/AIPathfinder.cpp
  51. 4 1
      AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp
  52. 9 2
      AI/Nullkiller/Pathfinding/ObjectGraph.cpp
  53. 4 1
      AI/VCAI/Goals/CollectRes.cpp
  54. 1 1
      AI/VCAI/Goals/CompleteQuest.cpp
  55. 6 5
      AI/VCAI/VCAI.cpp
  56. 1 1
      AI/VCAI/VCAI.h
  57. 8 3
      CCallback.cpp
  58. 4 2
      CCallback.h
  59. 1 1
      CI/ios/before_install.sh
  60. 1 1
      CI/linux-qt6/before_install.sh
  61. 1 1
      CI/linux/before_install.sh
  62. 1 1
      CI/mac/before_install.sh
  63. 1 1
      CI/mingw-32/before_install.sh
  64. 1 1
      CI/mingw/before_install.sh
  65. 8 1
      CMakeLists.txt
  66. 32 0
      ChangeLog.md
  67. 二进制
      Mods/vcmi/Data/lobby/iconEnter.png
  68. 81 2
      Mods/vcmi/config/vcmi/chinese.json
  69. 0 1
      Mods/vcmi/config/vcmi/czech.json
  70. 83 2
      Mods/vcmi/config/vcmi/english.json
  71. 0 1
      Mods/vcmi/config/vcmi/german.json
  72. 0 1
      Mods/vcmi/config/vcmi/polish.json
  73. 85 21
      Mods/vcmi/config/vcmi/portuguese.json
  74. 0 1
      Mods/vcmi/config/vcmi/spanish.json
  75. 35 6
      Mods/vcmi/config/vcmi/ukrainian.json
  76. 2 0
      client/CMT.cpp
  77. 6 4
      client/CMakeLists.txt
  78. 11 5
      client/CPlayerInterface.cpp
  79. 1 1
      client/CPlayerInterface.h
  80. 4 4
      client/Client.cpp
  81. 46 9
      client/HeroMovementController.cpp
  82. 3 2
      client/HeroMovementController.h
  83. 1 1
      client/NetPacksClient.cpp
  84. 6 4
      client/adventureMap/AdventureMapInterface.cpp
  85. 1 1
      client/adventureMap/AdventureMapShortcuts.cpp
  86. 1 0
      client/adventureMap/CInfoBar.cpp
  87. 1 1
      client/battle/BattleInterface.cpp
  88. 14 1
      client/globalLobby/GlobalLobbyClient.cpp
  89. 3 0
      client/globalLobby/GlobalLobbyClient.h
  90. 4 0
      client/globalLobby/GlobalLobbyDefines.h
  91. 200 0
      client/globalLobby/GlobalLobbyRoomWindow.cpp
  92. 89 0
      client/globalLobby/GlobalLobbyRoomWindow.h
  93. 4 2
      client/globalLobby/GlobalLobbyServerSetup.cpp
  94. 11 15
      client/globalLobby/GlobalLobbyWidget.cpp
  95. 4 0
      client/globalLobby/GlobalLobbyWidget.h
  96. 2 1
      client/gui/Shortcut.h
  97. 2 1
      client/gui/ShortcutHandler.cpp
  98. 1 1
      client/lobby/CBonusSelection.cpp
  99. 2 2
      client/lobby/CLobbyScreen.cpp
  100. 3 0
      client/lobby/SelectionTab.cpp

+ 4 - 0
.gitmodules

@@ -6,3 +6,7 @@
 	path = AI/FuzzyLite
 	url = https://github.com/fuzzylite/fuzzylite.git
 	branch = release
+[submodule "innoextract"]
+	path = launcher/lib/innoextract
+	url = https://github.com/vcmi/innoextract.git
+	branch = vcmi

+ 0 - 4
AI/BattleAI/BattleAI.cpp

@@ -166,10 +166,6 @@ void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
 	{
 		throw;
 	}
-	catch(std::exception &e)
-	{
-		logAi->error("Exception occurred in %s %s",__FUNCTION__, e.what());
-	}
 
 	if(result.actionType == EActionType::DEFEND)
 	{

+ 12 - 0
AI/BattleAI/BattleEvaluator.cpp

@@ -64,6 +64,18 @@ std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const
 		auto moatHex = wallHex.cloneInDirection(BattleHex::LEFT);
 
 		result.push_back(moatHex);
+
+		moatHex = moatHex.cloneInDirection(BattleHex::LEFT);
+		auto obstaclesSecondRow = cb->getBattle(battleID)->battleGetAllObstaclesOnPos(moatHex, false);
+
+		for(auto obstacle : obstaclesSecondRow)
+		{
+			if(obstacle->obstacleType == CObstacleInstance::EObstacleType::MOAT)
+			{
+				result.push_back(moatHex);
+				break;
+			}
+		}
 	}
 
 	return result;

+ 1 - 1
AI/EmptyAI/CEmptyAI.cpp

@@ -56,7 +56,7 @@ void CEmptyAI::commanderGotLevel(const CCommanderInstance * commander, std::vect
 	cb->selectionMade(CRandomGenerator::getDefault().nextInt((int)skills.size() - 1), queryID);
 }
 
-void CEmptyAI::showBlockingDialog(const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel)
+void CEmptyAI::showBlockingDialog(const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel, bool safeToAutoaccept)
 {
 	cb->selectionMade(0, askID);
 }

+ 1 - 1
AI/EmptyAI/CEmptyAI.h

@@ -28,7 +28,7 @@ public:
 	void activeStack(const BattleID & battleID, const CStack * stack) override;
 	void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector<SecondarySkill> &skills, QueryID queryID) override;
 	void commanderGotLevel (const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID) override;
-	void showBlockingDialog(const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel) override;
+	void showBlockingDialog(const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel, bool safeToAutoaccept) override;
 	void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override;
 	void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override;
 	void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects) override;

+ 33 - 13
AI/Nullkiller/AIGateway.cpp

@@ -11,6 +11,7 @@
 
 #include "../../lib/ArtifactUtils.h"
 #include "../../lib/UnlockGuard.h"
+#include "../../lib/StartInfo.h"
 #include "../../lib/mapObjects/MapObjects.h"
 #include "../../lib/mapObjects/ObjectTemplate.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
@@ -251,6 +252,7 @@ void AIGateway::heroVisit(const CGHeroInstance * visitor, const CGObjectInstance
 	if(start && visitedObj) //we can end visit with null object, anyway
 	{
 		nullkiller->memory->markObjectVisited(visitedObj);
+		nullkiller->objectClusterizer->invalidate(visitedObj->id);
 	}
 
 	status.heroVisit(visitedObj, start);
@@ -373,6 +375,7 @@ void AIGateway::objectRemoved(const CGObjectInstance * obj, const PlayerColor &
 		return;
 
 	nullkiller->memory->removeFromMemory(obj);
+	nullkiller->objectClusterizer->onObjectRemoved(obj->id);
 
 	if(nullkiller->baseGraph && nullkiller->settings->isObjectGraphAllowed())
 	{
@@ -542,6 +545,11 @@ std::optional<BattleAction> AIGateway::makeSurrenderRetreatDecision(const Battle
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;
 
+	if(battleState.ourHero && battleState.ourHero->patrol.patrolling)
+	{
+		return std::nullopt;
+	}
+
 	double ourStrength = battleState.getOurStrength();
 	double fightRatio = ourStrength / (double)battleState.getEnemyStrength();
 
@@ -614,9 +622,9 @@ void AIGateway::commanderGotLevel(const CCommanderInstance * commander, std::vec
 	requestActionASAP([=](){ answerQuery(queryID, 0); });
 }
 
-void AIGateway::showBlockingDialog(const std::string & text, const std::vector<Component> & components, QueryID askID, const int soundID, bool selection, bool cancel)
+void AIGateway::showBlockingDialog(const std::string & text, const std::vector<Component> & components, QueryID askID, const int soundID, bool selection, bool cancel, bool safeToAutoaccept)
 {
-	LOG_TRACE_PARAMS(logAi, "text '%s', askID '%i', soundID '%i', selection '%i', cancel '%i'", text % askID % soundID % selection % cancel);
+	LOG_TRACE_PARAMS(logAi, "text '%s', askID '%i', soundID '%i', selection '%i', cancel '%i', autoaccept '%i'", text % askID % soundID % selection % cancel % safeToAutoaccept);
 	NET_EVENT_HANDLER;
 	status.addQuery(askID, boost::str(boost::format("Blocking dialog query with %d components - %s")
 									  % components.size() % text));
@@ -682,7 +690,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
 					&& components.size() == 2
 					&& components.front().type == ComponentType::RESOURCE
 					&& (nullkiller->heroManager->getHeroRole(hero) != HeroRole::MAIN
-						|| nullkiller->buildAnalyzer->isGoldPreasureHigh()))
+						|| nullkiller->buildAnalyzer->isGoldPressureHigh()))
 				{
 					sel = 1;
 				}
@@ -748,7 +756,7 @@ void AIGateway::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstan
 	//you can't request action from action-response thread
 	requestActionASAP([=]()
 	{
-		if(removableUnits && up->tempOwner == down->tempOwner)
+		if(removableUnits && up->tempOwner == down->tempOwner && nullkiller->settings->isGarrisonTroopsUsageAllowed() && !cb->getStartInfo()->isSteadwickFallCampaignMission())
 		{
 			pickBestCreatures(down, up);
 		}
@@ -1109,7 +1117,24 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re
 
 		if(!recruiter->getSlotFor(creID).validSlot())
 		{
-			continue;
+			for(auto stack : recruiter->Slots())
+			{
+				if(!stack.second->type)
+					continue;
+				
+				auto duplicatingSlot = recruiter->getSlotFor(stack.second->type);
+
+				if(duplicatingSlot != stack.first)
+				{
+					cb->mergeStacks(recruiter, recruiter, stack.first, duplicatingSlot);
+					break;
+				}
+			}
+
+			if(!recruiter->getSlotFor(creID).validSlot())
+			{
+				continue;
+			}
 		}
 
 		vstd::amin(count, cb->getResourceAmount() / creID.toCreature()->getFullRecruitCost());
@@ -1152,11 +1177,6 @@ void AIGateway::retrieveVisitableObjs()
 	{
 		for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false))
 		{
-			if(!obj->appearance)
-			{
-				logAi->error("Bad!");
-			}
-
 			addVisitableObj(obj);
 		}
 	});
@@ -1216,7 +1236,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 	{
 		//FIXME: this assertion fails also if AI moves onto defeated guarded object
 		//assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object
-		cb->moveHero(*h, h->convertFromVisitablePos(dst));
+		cb->moveHero(*h, h->convertFromVisitablePos(dst), false);
 		afterMovementCheck(); // TODO: is it feasible to hero get killed there if game work properly?
 		// If revisiting, teleport probing is never done, and so the entries into the list would remain unused and uncleared
 		teleportChannelProbingList.clear();
@@ -1278,7 +1298,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 			destinationTeleport = exitId;
 			if(exitPos.valid())
 				destinationTeleportPos = h->convertFromVisitablePos(exitPos);
-			cb->moveHero(*h, h->pos);
+			cb->moveHero(*h, h->pos, false);
 			destinationTeleport = ObjectInstanceID();
 			destinationTeleportPos = int3(-1);
 			afterMovementCheck();
@@ -1401,7 +1421,7 @@ void AIGateway::tryRealize(Goals::DigAtTile & g)
 	assert(g.hero->visitablePos() == g.tile); //surely we want to crash here?
 	if(g.hero->diggingStatus() == EDiggingStatus::CAN_DIG)
 	{
-		cb->dig(g.hero.get());
+		cb->dig(g.hero);
 	}
 	else
 	{

+ 1 - 1
AI/Nullkiller/AIGateway.h

@@ -115,7 +115,7 @@ public:
 
 	void heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector<SecondarySkill> & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id
 	void commanderGotLevel(const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID) override; //TODO
-	void showBlockingDialog(const std::string & text, const std::vector<Component> & components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID.
+	void showBlockingDialog(const std::string & text, const std::vector<Component> & components, QueryID askID, const int soundID, bool selection, bool cancel, bool safeToAutoaccept) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID.
 	void showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) override; //all stacks operations between these objects become allowed, interface has to call onEnd when done
 	void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override;
 	void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects) override;

+ 17 - 26
AI/Nullkiller/AIUtility.cpp

@@ -91,33 +91,10 @@ bool HeroPtr::operator<(const HeroPtr & rhs) const
 
 const CGHeroInstance * HeroPtr::get(bool doWeExpectNull) const
 {
-	//TODO? check if these all assertions every time we get info about hero affect efficiency
-	//
-	//behave terribly when attempting unauthorized access to hero that is not ours (or was lost)
-	assert(doWeExpectNull || h);
-
-	if(h)
-	{
-		auto obj = cb->getObj(hid);
-		//const bool owned = obj && obj->tempOwner == ai->playerID;
-
-		if(doWeExpectNull && !obj)
-		{
-			return nullptr;
-		}
-		else
-		{
-			if (!obj)
-				logAi->error("Accessing no longer accessible hero %s!", h->getNameTranslated());
-			//assert(obj);
-			//assert(owned);
-		}
-	}
-
-	return h;
+	return get(cb, doWeExpectNull);
 }
 
-const CGHeroInstance * HeroPtr::get(CCallback * cb, bool doWeExpectNull) const
+const CGHeroInstance * HeroPtr::get(const CPlayerSpecificInfoCallback * cb, bool doWeExpectNull) const
 {
 	//TODO? check if these all assertions every time we get info about hero affect efficiency
 	//
@@ -323,6 +300,19 @@ uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock>
 	return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
 }
 
+int getDuplicatingSlots(const CArmedInstance * army)
+{
+	int duplicatingSlots = 0;
+
+	for(auto stack : army->Slots())
+	{
+		if(stack.second->type && army->getSlotFor(stack.second->type) != stack.first)
+			duplicatingSlots++;
+	}
+
+	return duplicatingSlots;
+}
+
 // todo: move to obj manager
 bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObjectInstance * obj)
 {
@@ -370,13 +360,14 @@ bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObject
 			return false;
 
 		const CGDwelling * d = dynamic_cast<const CGDwelling *>(obj);
+		auto duplicatingSlotsCount = getDuplicatingSlots(h);
 
 		for(auto level : d->creatures)
 		{
 			for(auto c : level.second)
 			{
 				if(level.first
-					&& h->getSlotFor(CreatureID(c)) != SlotID()
+					&& (h->getSlotFor(CreatureID(c)) != SlotID() || duplicatingSlotsCount > 0)
 					&& ai->cb->getResourceAmount().canAfford(c.toCreature()->getFullRecruitCost()))
 				{
 					return true;

+ 2 - 1
AI/Nullkiller/AIUtility.h

@@ -109,7 +109,7 @@ public:
 	}
 
 	const CGHeroInstance * get(bool doWeExpectNull = false) const;
-	const CGHeroInstance * get(CCallback * cb, bool doWeExpectNull = false) const;
+	const CGHeroInstance * get(const CPlayerSpecificInfoCallback * cb, bool doWeExpectNull = false) const;
 	bool validAndSet() const;
 
 
@@ -242,6 +242,7 @@ uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock>
 
 // todo: move to obj manager
 bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObjectInstance * obj);
+int getDuplicatingSlots(const CArmedInstance * army);
 
 template <class T>
 class SharedPool

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

@@ -120,9 +120,9 @@ TResources BuildAnalyzer::getTotalResourcesRequired() const
 	return result;
 }
 
-bool BuildAnalyzer::isGoldPreasureHigh() const
+bool BuildAnalyzer::isGoldPressureHigh() const
 {
-	return goldPreasure > ai->settings->getMaxGoldPreasure();
+	return goldPressure > ai->settings->getMaxGoldPressure();
 }
 
 void BuildAnalyzer::update()
@@ -167,15 +167,15 @@ void BuildAnalyzer::update()
 
 	if(ai->cb->getDate(Date::DAY) == 1)
 	{
-		goldPreasure = 1;
+		goldPressure = 1;
 	}
 	else
 	{
-		goldPreasure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f
+		goldPressure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f
 			+ (float)armyCost[EGameResID::GOLD] / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
 	}
 
-	logAi->trace("Gold preasure: %f", goldPreasure);
+	logAi->trace("Gold pressure: %f", goldPressure);
 }
 
 void BuildAnalyzer::reset()

+ 3 - 3
AI/Nullkiller/Analyzers/BuildAnalyzer.h

@@ -84,7 +84,7 @@ private:
 	std::vector<TownDevelopmentInfo> developmentInfos;
 	TResources armyCost;
 	TResources dailyIncome;
-	float goldPreasure;
+	float goldPressure;
 	Nullkiller * ai;
 
 public:
@@ -95,8 +95,8 @@ public:
 	TResources getTotalResourcesRequired() const;
 	const std::vector<TownDevelopmentInfo> & getDevelopmentInfo() const { return developmentInfos; }
 	TResources getDailyIncome() const { return dailyIncome; }
-	float getGoldPreasure() const { return goldPreasure; }
-	bool isGoldPreasureHigh() const;
+	float getGoldPressure() const { return goldPressure; }
+	bool isGoldPressureHigh() const;
 	bool hasAnyBuilding(int32_t alignment, BuildingID bid) const;
 
 private:

+ 8 - 1
AI/Nullkiller/Analyzers/HeroManager.cpp

@@ -130,7 +130,14 @@ void HeroManager::update()
 
 	for(auto hero : myHeroes)
 	{
-		heroRoles[hero] = (globalMainCount--) > 0 ? HeroRole::MAIN : HeroRole::SCOUT;
+		if(hero->patrol.patrolling)
+		{
+			heroRoles[hero] = HeroRole::MAIN;
+		}
+		else
+		{
+			heroRoles[hero] = (globalMainCount--) > 0 ? HeroRole::MAIN : HeroRole::SCOUT;
+		}
 	}
 
 	for(auto hero : myHeroes)

+ 118 - 24
AI/Nullkiller/Analyzers/ObjectClusterizer.cpp

@@ -20,7 +20,7 @@ void ObjectCluster::addObject(const CGObjectInstance * obj, const AIPath & path,
 {
 	ClusterObjects::accessor info;
 
-	objects.insert(info, ClusterObjects::value_type(obj, ClusterObjectInfo()));
+	objects.insert(info, ClusterObjects::value_type(obj->id, ClusterObjectInfo()));
 
 	if(info->second.priority < priority)
 	{
@@ -31,15 +31,14 @@ void ObjectCluster::addObject(const CGObjectInstance * obj, const AIPath & path,
 	}
 }
 
-const CGObjectInstance * ObjectCluster::calculateCenter() const
+const CGObjectInstance * ObjectCluster::calculateCenter(const CPlayerSpecificInfoCallback * cb) const
 {
-	auto v = getObjects();
 	auto tile = int3(0);
 	float priority = 0;
 
-	for(auto pair : objects)
+	for(auto & pair : objects)
 	{
-		auto newPoint = pair.first->visitablePos();
+		auto newPoint = cb->getObj(pair.first)->visitablePos();
 		float newPriority = std::pow(pair.second.priority, 4); // lets make high priority targets more weghtful
 		int3 direction = newPoint - tile;
 		float priorityRatio = newPriority / (priority + newPriority);
@@ -48,21 +47,21 @@ const CGObjectInstance * ObjectCluster::calculateCenter() const
 		priority += newPriority;
 	}
 
-	auto closestPair = *vstd::minElementByFun(objects, [&](const std::pair<const CGObjectInstance *, ClusterObjectInfo> & pair) -> int
+	auto closestPair = *vstd::minElementByFun(objects, [&](const std::pair<ObjectInstanceID, ClusterObjectInfo> & pair) -> int
 	{
-		return pair.first->visitablePos().dist2dSQ(tile);
+		return cb->getObj(pair.first)->visitablePos().dist2dSQ(tile);
 	});
 
-	return closestPair.first;
+	return cb->getObj(closestPair.first);
 }
 
-std::vector<const CGObjectInstance *> ObjectCluster::getObjects() const
+std::vector<const CGObjectInstance *> ObjectCluster::getObjects(const CPlayerSpecificInfoCallback * cb) const
 {
 	std::vector<const CGObjectInstance *> result;
 
-	for(auto pair : objects)
+	for(auto & pair : objects)
 	{
-		result.push_back(pair.first);
+		result.push_back(cb->getObj(pair.first));
 	}
 
 	return result;
@@ -70,19 +69,19 @@ std::vector<const CGObjectInstance *> ObjectCluster::getObjects() const
 
 std::vector<const CGObjectInstance *> ObjectClusterizer::getNearbyObjects() const
 {
-	return nearObjects.getObjects();
+	return nearObjects.getObjects(ai->cb.get());
 }
 
 std::vector<const CGObjectInstance *> ObjectClusterizer::getFarObjects() const
 {
-	return farObjects.getObjects();
+	return farObjects.getObjects(ai->cb.get());
 }
 
 std::vector<std::shared_ptr<ObjectCluster>> ObjectClusterizer::getLockedClusters() const
 {
 	std::vector<std::shared_ptr<ObjectCluster>> result;
 
-	for(auto pair : blockedObjects)
+	for(auto & pair : blockedObjects)
 	{
 		result.push_back(pair.second);
 	}
@@ -163,6 +162,69 @@ const CGObjectInstance * ObjectClusterizer::getBlocker(const AIPath & path) cons
 	return nullptr;
 }
 
+void ObjectClusterizer::invalidate(ObjectInstanceID id)
+{
+	nearObjects.objects.erase(id);
+	farObjects.objects.erase(id);
+	invalidated.push_back(id);
+
+	for(auto & c : blockedObjects)
+	{
+		c.second->objects.erase(id);
+	}
+}
+
+void ObjectClusterizer::validateObjects()
+{
+	std::vector<ObjectInstanceID> toRemove;
+
+	auto scanRemovedObjects = [this, &toRemove](const ObjectCluster & cluster)
+	{
+		for(auto & pair : cluster.objects)
+		{
+			if(!ai->cb->getObj(pair.first, false))
+				toRemove.push_back(pair.first);
+		}
+	};
+
+	scanRemovedObjects(nearObjects);
+	scanRemovedObjects(farObjects);
+
+	for(auto & pair : blockedObjects)
+	{
+		if(!ai->cb->getObj(pair.first, false) || pair.second->objects.empty())
+			toRemove.push_back(pair.first);
+		else
+			scanRemovedObjects(*pair.second);
+	}
+
+	vstd::removeDuplicates(toRemove);
+
+	for(auto id : toRemove)
+	{
+		onObjectRemoved(id);
+	}
+}
+
+void ObjectClusterizer::onObjectRemoved(ObjectInstanceID id)
+{
+	invalidate(id);
+
+	vstd::erase_if_present(invalidated, id);
+
+	NKAI::ClusterMap::accessor cluster;
+	
+	if(blockedObjects.find(cluster, id))
+	{
+		for(auto & unlocked : cluster->second->objects)
+		{
+			invalidated.push_back(unlocked.first);
+		}
+
+		blockedObjects.erase(cluster);
+	}
+}
+
 bool ObjectClusterizer::shouldVisitObject(const CGObjectInstance * obj) const
 {
 	if(isObjectRemovable(obj))
@@ -222,17 +284,45 @@ Obj ObjectClusterizer::IgnoredObjectTypes[] = {
 
 void ObjectClusterizer::clusterize()
 {
-	auto start = std::chrono::high_resolution_clock::now();
+	if(isUpToDate)
+	{
+		validateObjects();
+	}
 
-	nearObjects.reset();
-	farObjects.reset();
-	blockedObjects.clear();
+	if(isUpToDate && invalidated.empty())
+		return;
+		
+	auto start = std::chrono::high_resolution_clock::now();
 
 	logAi->debug("Begin object clusterization");
 
-	std::vector<const CGObjectInstance *> objs(
-		ai->memory->visitableObjs.begin(),
-		ai->memory->visitableObjs.end());
+	std::vector<const CGObjectInstance *> objs;
+	
+	if(isUpToDate)
+	{
+		for(auto id : invalidated)
+		{
+			auto obj = cb->getObj(id, false);
+
+			if(obj)
+			{
+				objs.push_back(obj);
+			}
+		}
+
+		invalidated.clear();
+	}
+	else
+	{
+		nearObjects.reset();
+		farObjects.reset();
+		blockedObjects.clear();
+		invalidated.clear();
+
+		objs = std::vector<const CGObjectInstance *>(
+			ai->memory->visitableObjs.begin(),
+			ai->memory->visitableObjs.end());
+	}
 
 #if NKAI_TRACE_LEVEL == 0
 	tbb::parallel_for(tbb::blocked_range<size_t>(0, objs.size()), [&](const tbb::blocked_range<size_t> & r) {
@@ -256,16 +346,20 @@ void ObjectClusterizer::clusterize()
 
 	for(auto pair : blockedObjects)
 	{
-		logAi->trace("Cluster %s %s count: %i", pair.first->getObjectName(), pair.first->visitablePos().toString(), pair.second->objects.size());
+		auto blocker = cb->getObj(pair.first);
+
+		logAi->trace("Cluster %s %s count: %i", blocker->getObjectName(), blocker->visitablePos().toString(), pair.second->objects.size());
 
 #if NKAI_TRACE_LEVEL >= 1
-		for(auto obj : pair.second->getObjects())
+		for(auto obj : pair.second->getObjects(ai->cb.get()))
 		{
 			logAi->trace("Object %s %s", obj->getObjectName(), obj->visitablePos().toString());
 		}
 #endif
 	}
 
+	isUpToDate = true;
+
 	logAi->trace("Clusterization complete in %ld", timeElapsed(start));
 }
 
@@ -381,7 +475,7 @@ void ObjectClusterizer::clusterizeObject(
 				ClusterMap::accessor cluster;
 				blockedObjects.insert(
 					cluster,
-					ClusterMap::value_type(blocker, std::make_shared<ObjectCluster>(blocker)));
+					ClusterMap::value_type(blocker->id, std::make_shared<ObjectCluster>(blocker)));
 
 				cluster->second->addObject(obj, path, priority);
 

+ 24 - 5
AI/Nullkiller/Analyzers/ObjectClusterizer.h

@@ -23,7 +23,15 @@ struct ClusterObjectInfo
 	uint8_t turn;
 };
 
-using ClusterObjects = tbb::concurrent_hash_map<const CGObjectInstance *, ClusterObjectInfo>;
+struct ObjectInstanceIDHash
+{
+	ObjectInstanceID::hash hash;
+	bool equal(ObjectInstanceID o1, ObjectInstanceID o2) const
+	{
+		return o1 == o2;
+	}
+};
+using ClusterObjects = tbb::concurrent_hash_map<ObjectInstanceID, ClusterObjectInfo, ObjectInstanceIDHash>;
 
 struct ObjectCluster
 {
@@ -44,11 +52,11 @@ public:
 	{
 	}
 
-	std::vector<const CGObjectInstance *> getObjects() const;
-	const CGObjectInstance * calculateCenter() const;
+	std::vector<const CGObjectInstance *> getObjects(const CPlayerSpecificInfoCallback * cb) const;
+	const CGObjectInstance * calculateCenter(const CPlayerSpecificInfoCallback * cb) const;
 };
 
-using ClusterMap = tbb::concurrent_hash_map<const CGObjectInstance *, std::shared_ptr<ObjectCluster>>;
+using ClusterMap = tbb::concurrent_hash_map<ObjectInstanceID, std::shared_ptr<ObjectCluster>, ObjectInstanceIDHash>;
 
 class ObjectClusterizer
 {
@@ -60,6 +68,8 @@ private:
 	ClusterMap blockedObjects;
 	const Nullkiller * ai;
 	RewardEvaluator valueEvaluator;
+	bool isUpToDate;
+	std::vector<ObjectInstanceID> invalidated;
 
 public:
 	void clusterize();
@@ -69,7 +79,16 @@ public:
 	const CGObjectInstance * getBlocker(const AIPath & path) const;
 	std::optional<const CGObjectInstance *> getBlocker(const AIPathNodeInfo & node) const;
 
-	ObjectClusterizer(const Nullkiller * ai): ai(ai), valueEvaluator(ai) {}
+	ObjectClusterizer(const Nullkiller * ai): ai(ai), valueEvaluator(ai), isUpToDate(false){}
+
+	void validateObjects();
+	void onObjectRemoved(ObjectInstanceID id);
+	void invalidate(ObjectInstanceID id);
+
+	void reset() {
+		isUpToDate = false;
+		invalidated.clear();
+	}
 
 private:
 	bool shouldVisitObject(const CGObjectInstance * obj) const;

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

@@ -47,13 +47,13 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const
 		totalDevelopmentCost.toString());
 
 	auto & developmentInfos = ai->buildAnalyzer->getDevelopmentInfo();
-	auto isGoldPreasureLow = !ai->buildAnalyzer->isGoldPreasureHigh();
+	auto isGoldPressureLow = !ai->buildAnalyzer->isGoldPressureHigh();
 
 	for(auto & developmentInfo : developmentInfos)
 	{
 		for(auto & buildingInfo : developmentInfo.toBuild)
 		{
-			if(isGoldPreasureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0)
+			if(isGoldPressureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0)
 			{
 				if(buildingInfo.notEnoughRes)
 				{

+ 1 - 1
AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp

@@ -46,7 +46,7 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
 
 		for(const CGHeroInstance * targetHero : heroes)
 		{
-			if(ai->buildAnalyzer->isGoldPreasureHigh()	&& !town->hasBuilt(BuildingID::CITY_HALL))
+			if(ai->buildAnalyzer->isGoldPressureHigh()	&& !town->hasBuilt(BuildingID::CITY_HALL))
 			{
 				continue;
 			}

+ 1 - 1
AI/Nullkiller/Behaviors/ClusterBehavior.cpp

@@ -41,7 +41,7 @@ Goals::TGoalVec ClusterBehavior::decompose(const Nullkiller * ai) const
 
 Goals::TGoalVec ClusterBehavior::decomposeCluster(const Nullkiller * ai, std::shared_ptr<ObjectCluster> cluster) const
 {
-	auto center = cluster->calculateCenter();
+	auto center = cluster->calculateCenter(ai->cb.get());
 	auto paths = ai->pathfinder->getPathInfo(center->visitablePos(), ai->settings->isObjectGraphAllowed());
 
 	auto blockerPos = cluster->blocker->visitablePos();

+ 1 - 1
AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp

@@ -344,7 +344,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
 
 					if(scoutReinforcement >= armyToGetOrBuy.upgradeValue
 						&& ai->getFreeGold() >20000
-						&& !ai->buildAnalyzer->isGoldPreasureHigh())
+						&& !ai->buildAnalyzer->isGoldPressureHigh())
 					{
 						Composition recruitHero;
 

+ 1 - 1
AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp

@@ -85,7 +85,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
 				continue;
 
 			if(ai->cb->getHeroesInfo().size() < ai->cb->getTownsInfo().size() + 1
-				|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPreasureHigh()))
+				|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh()))
 			{
 				tasks.push_back(Goals::sptr(Goals::RecruitHero(town).setpriority(3)));
 			}

+ 9 - 1
AI/Nullkiller/Behaviors/StartupBehavior.cpp

@@ -71,7 +71,15 @@ bool needToRecruitHero(const Nullkiller * ai, const CGTownInstance * startupTown
 
 	for(auto obj : ai->objectClusterizer->getNearbyObjects())
 	{
-		if((obj->ID == Obj::RESOURCE && dynamic_cast<const CGResource *>(obj)->resourceID() == EGameResID::GOLD)
+		auto armed = dynamic_cast<const CArmedInstance *>(obj);
+
+		if(armed && armed->getArmyStrength() > 0)
+			continue;
+
+		bool isGoldPile = dynamic_cast<const CGResource *>(obj)
+			&& dynamic_cast<const CGResource *>(obj)->resourceID() == EGameResID::GOLD;
+
+		if(isGoldPile
 			|| obj->ID == Obj::TREASURE_CHEST
 			|| obj->ID == Obj::CAMPFIRE
 			|| obj->ID == Obj::WATER_WHEEL)

+ 2 - 6
AI/Nullkiller/Engine/DeepDecomposer.cpp

@@ -37,10 +37,8 @@ void DeepDecomposer::reset()
 	goals.clear();
 }
 
-Goals::TGoalVec DeepDecomposer::decompose(TSubgoal behavior, int depthLimit)
+void DeepDecomposer::decompose(TGoalVec & result, TSubgoal behavior, int depthLimit)
 {
-	TGoalVec tasks;
-
 	goals.clear();
 	goals.resize(depthLimit);
 	decompositionCache.resize(depthLimit);
@@ -79,7 +77,7 @@ Goals::TGoalVec DeepDecomposer::decompose(TSubgoal behavior, int depthLimit)
 #endif
 				if(!isCompositionLoop(subgoal))
 				{
-					tasks.push_back(task);
+					result.push_back(task);
 
 					if(!fromCache)
 					{
@@ -121,8 +119,6 @@ Goals::TGoalVec DeepDecomposer::decompose(TSubgoal behavior, int depthLimit)
 			}
 		}
 	}
-
-	return tasks;
 }
 
 Goals::TSubgoal DeepDecomposer::aggregateGoals(int startDepth, TSubgoal last)

+ 1 - 1
AI/Nullkiller/Engine/DeepDecomposer.h

@@ -35,7 +35,7 @@ private:
 public:
 	DeepDecomposer(const Nullkiller * ai);
 	void reset();
-	Goals::TGoalVec decompose(Goals::TSubgoal behavior, int depthLimit);
+	void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int depthLimit);
 
 private:
 	Goals::TSubgoal aggregateGoals(int startDepth, Goals::TSubgoal last);

+ 13 - 2
AI/Nullkiller/Engine/FuzzyHelper.cpp

@@ -53,15 +53,26 @@ ui64 FuzzyHelper::evaluateDanger(const int3 & tile, const CGHeroInstance * visit
 	// in some scenarios hero happens to be "under" the object (eg town). Then we consider ONLY the hero.
 	if(vstd::contains_if(visitableObjects, objWithID<Obj::HERO>))
 	{
-		vstd::erase_if(visitableObjects, [](const CGObjectInstance * obj)
+		vstd::erase_if(visitableObjects, [](const CGObjectInstance * obj) -> bool
 		{
-			return !objWithID<Obj::HERO>(obj);
+				return !objWithID<Obj::HERO>(obj);
 		});
 	}
 
 	if(const CGObjectInstance * dangerousObject = vstd::backOrNull(visitableObjects))
 	{
 		objectDanger = evaluateDanger(dangerousObject); //unguarded objects can also be dangerous or unhandled
+
+		if(objWithID<Obj::HERO>(dangerousObject))
+		{
+			auto hero = dynamic_cast<const CGHeroInstance *>(dangerousObject);
+
+			if(hero->visitedTown && !hero->visitedTown->garrisonHero)
+			{
+				objectDanger += evaluateDanger(hero->visitedTown.get());
+			}
+		}
+
 		if(objectDanger)
 		{
 			//TODO: don't downcast objects AI shouldn't know about!

+ 179 - 85
AI/Nullkiller/Engine/Nullkiller.cpp

@@ -64,55 +64,119 @@ void Nullkiller::init(std::shared_ptr<CCallback> cb, AIGateway * gateway)
 	armyFormation.reset(new ArmyFormation(cb, this));
 }
 
-Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const
+TaskPlanItem::TaskPlanItem(TSubgoal task)
+	:task(task), affectedObjects(task->asTask()->getAffectedObjects())
 {
-	Goals::TTask bestTask = *vstd::maxElementByFun(tasks, [](Goals::TTask task) -> float{
-		return task->priority;
-	});
-
-	return bestTask;
 }
 
-Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior, int decompositionMaxDepth) const
+Goals::TTaskVec TaskPlan::getTasks() const
 {
-	boost::this_thread::interruption_point();
+	Goals::TTaskVec result;
 
-	logAi->debug("Checking behavior %s", behavior->toString());
+	for(auto & item : tasks)
+	{
+		result.push_back(taskptr(*item.task));
+	}
 
-	auto start = std::chrono::high_resolution_clock::now();
-	
-	Goals::TGoalVec elementarGoals = decomposer->decompose(behavior, decompositionMaxDepth);
-	Goals::TTaskVec tasks;
+	vstd::removeDuplicates(result);
 
-	boost::this_thread::interruption_point();
-	
-	for(auto goal : elementarGoals)
-	{
-		Goals::TTask task = Goals::taskptr(*goal);
+	return result;
+}
 
-		if(task->priority <= 0)
-			task->priority = priorityEvaluator->evaluate(goal);
+void TaskPlan::merge(TSubgoal task)
+{
+	TGoalVec blockers;
+
+	for(auto & item : tasks)
+	{
+		for(auto objid : item.affectedObjects)
+		{
+			if(task == item.task || task->asTask()->isObjectAffected(objid))
+			{
+				if(item.task->asTask()->priority >= task->asTask()->priority)
+					return;
 
-		tasks.push_back(task);
+				blockers.push_back(item.task);
+				break;
+			}
+		}
 	}
 
+	vstd::erase_if(tasks, [&](const TaskPlanItem & task)
+		{
+			return vstd::contains(blockers, task.task);
+		});
+
+	tasks.emplace_back(task);
+}
+
+Goals::TTask Nullkiller::choseBestTask(Goals::TGoalVec & tasks) const
+{
 	if(tasks.empty())
 	{
-		logAi->debug("Behavior %s found no tasks. Time taken %ld", behavior->toString(), timeElapsed(start));
+		return taskptr(Invalid());
+	}
+
+	for(TSubgoal & task : tasks)
+	{
+		if(task->asTask()->priority <= 0)
+			task->asTask()->priority = priorityEvaluator->evaluate(task);
+	}
+
+	auto bestTask = *vstd::maxElementByFun(tasks, [](Goals::TSubgoal task) -> float
+		{
+			return task->asTask()->priority;
+		});
+
+	return taskptr(*bestTask);
+}
+
+Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks) const
+{
+	TaskPlan taskPlan;
+
+	tbb::parallel_for(tbb::blocked_range<size_t>(0, tasks.size()), [this, &tasks](const tbb::blocked_range<size_t> & r)
+		{
+			auto evaluator = this->priorityEvaluators->acquire();
 
-		return Goals::taskptr(Goals::Invalid());
+			for(size_t i = r.begin(); i != r.end(); i++)
+			{
+				auto task = tasks[i];
+
+				if(task->asTask()->priority <= 0)
+					task->asTask()->priority = evaluator->evaluate(task);
+			}
+		});
+
+	std::sort(tasks.begin(), tasks.end(), [](TSubgoal g1, TSubgoal g2) -> bool
+		{
+			return g2->asTask()->priority < g1->asTask()->priority;
+		});
+
+	for(TSubgoal & task : tasks)
+	{
+		taskPlan.merge(task);
 	}
 
-	auto task = choseBestTask(tasks);
+	return taskPlan.getTasks();
+}
+
+void Nullkiller::decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const
+{
+	boost::this_thread::interruption_point();
+
+	logAi->debug("Checking behavior %s", behavior->toString());
+
+	auto start = std::chrono::high_resolution_clock::now();
+	
+	decomposer->decompose(result, behavior, decompositionMaxDepth);
+
+	boost::this_thread::interruption_point();
 
 	logAi->debug(
-		"Behavior %s returns %s, priority %f. Time taken %ld",
+		"Behavior %s. Time taken %ld",
 		behavior->toString(),
-		task->toString(),
-		task->priority,
 		timeElapsed(start));
-
-	return task;
 }
 
 void Nullkiller::resetAiState()
@@ -124,6 +188,7 @@ void Nullkiller::resetAiState()
 	lockedHeroes.clear();
 	dangerHitMap->reset();
 	useHeroChain = true;
+	objectClusterizer->reset();
 
 	if(!baseGraph && settings->isObjectGraphAllowed())
 	{
@@ -251,7 +316,9 @@ void Nullkiller::makeTurn()
 
 	resetAiState();
 
-	for(int i = 1; i <= settings->getMaxPass(); i++)
+	Goals::TGoalVec bestTasks;
+
+	for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++)
 	{
 		auto start = std::chrono::high_resolution_clock::now();
 		updateAiState(i);
@@ -260,16 +327,18 @@ void Nullkiller::makeTurn()
 
 		for(;i <= settings->getMaxPass(); i++)
 		{
-			Goals::TTaskVec fastTasks = {
-				choseBestTask(sptr(BuyArmyBehavior()), 1),
-				choseBestTask(sptr(BuildingBehavior()), 1)
-			};
+			bestTasks.clear();
+
+			decompose(bestTasks, sptr(BuyArmyBehavior()), 1);
+			decompose(bestTasks, sptr(BuildingBehavior()), 1);
 
-			bestTask = choseBestTask(fastTasks);
+			bestTask = choseBestTask(bestTasks);
 
 			if(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY)
 			{
-				executeTask(bestTask);
+				if(!executeTask(bestTask))
+					return;
+
 				updateAiState(i, true);
 			}
 			else
@@ -278,83 +347,108 @@ void Nullkiller::makeTurn()
 			}
 		}
 
-		Goals::TTaskVec bestTasks = {
-			bestTask,
-			choseBestTask(sptr(RecruitHeroBehavior()), 1),
-			choseBestTask(sptr(CaptureObjectsBehavior()), 1),
-			choseBestTask(sptr(ClusterBehavior()), MAX_DEPTH),
-			choseBestTask(sptr(DefenceBehavior()), MAX_DEPTH),
-			choseBestTask(sptr(GatherArmyBehavior()), MAX_DEPTH),
-			choseBestTask(sptr(StayAtTownBehavior()), MAX_DEPTH)
-		};
+		decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
+		decompose(bestTasks, sptr(CaptureObjectsBehavior()), 1);
+		decompose(bestTasks, sptr(ClusterBehavior()), MAX_DEPTH);
+		decompose(bestTasks, sptr(DefenceBehavior()), MAX_DEPTH);
+		decompose(bestTasks, sptr(GatherArmyBehavior()), MAX_DEPTH);
+		decompose(bestTasks, sptr(StayAtTownBehavior()), MAX_DEPTH);
 
 		if(cb->getDate(Date::DAY) == 1)
 		{
-			bestTasks.push_back(choseBestTask(sptr(StartupBehavior()), 1));
+			decompose(bestTasks, sptr(StartupBehavior()), 1);
 		}
 
-		bestTask = choseBestTask(bestTasks);
+		auto selectedTasks = buildPlan(bestTasks);
 
-		std::string taskDescription = bestTask->toString();
-		HeroPtr hero = bestTask->getHero();
-		HeroRole heroRole = HeroRole::MAIN;
+		logAi->debug("Decission madel in %ld", timeElapsed(start));
 
-		if(hero.validAndSet())
-			heroRole = heroManager->getHeroRole(hero);
+		if(selectedTasks.empty())
+		{
+			return;
+		}
 
-		if(heroRole != HeroRole::MAIN || bestTask->getHeroExchangeCount() <= 1)
-			useHeroChain = false;
+		bool hasAnySuccess = false;
 
-		// TODO: better to check turn distance here instead of priority
-		if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY)
-			&& scanDepth == ScanDepth::MAIN_FULL)
+		for(auto bestTask : selectedTasks)
 		{
-			useHeroChain = false;
-			scanDepth = ScanDepth::SMALL;
+			if(cb->getPlayerStatus(playerID) != EPlayerStatus::INGAME)
+				return;
 
-			logAi->trace(
-				"Goal %s has low priority %f so decreasing  scan depth to gain performance.",
-				taskDescription,
-				bestTask->priority);
-		}
+			std::string taskDescription = bestTask->toString();
+			HeroPtr hero = bestTask->getHero();
+			HeroRole heroRole = HeroRole::MAIN;
 
-		if(bestTask->priority < MIN_PRIORITY)
-		{
-			auto heroes = cb->getHeroesInfo();
-			auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
-				{
-					return h->movementPointsRemaining() > 100;
-				});
+			if(hero.validAndSet())
+				heroRole = heroManager->getHeroRole(hero);
 
-			if(hasMp && scanDepth != ScanDepth::ALL_FULL)
+			if(heroRole != HeroRole::MAIN || bestTask->getHeroExchangeCount() <= 1)
+				useHeroChain = false;
+
+			// TODO: better to check turn distance here instead of priority
+			if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY)
+				&& scanDepth == ScanDepth::MAIN_FULL)
 			{
+				useHeroChain = false;
+				scanDepth = ScanDepth::SMALL;
+
 				logAi->trace(
-					"Goal %s has too low priority %f so increasing scan depth to full.",
+					"Goal %s has low priority %f so decreasing  scan depth to gain performance.",
 					taskDescription,
 					bestTask->priority);
+			}
+
+			if(bestTask->priority < MIN_PRIORITY)
+			{
+				auto heroes = cb->getHeroesInfo();
+				auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
+					{
+						return h->movementPointsRemaining() > 100;
+					});
+
+				if(hasMp && scanDepth != ScanDepth::ALL_FULL)
+				{
+					logAi->trace(
+						"Goal %s has too low priority %f so increasing scan depth to full.",
+						taskDescription,
+						bestTask->priority);
+
+					scanDepth = ScanDepth::ALL_FULL;
+					useHeroChain = false;
+					hasAnySuccess = true;
+					break;;
+				}
+
+				logAi->trace("Goal %s has too low priority. It is not worth doing it.", taskDescription);
 
-				scanDepth = ScanDepth::ALL_FULL;
-				useHeroChain = false;
 				continue;
 			}
 
-			logAi->trace("Goal %s has too low priority. It is not worth doing it. Ending turn.", taskDescription);
+			if(!executeTask(bestTask))
+			{
+				if(hasAnySuccess)
+					break;
+				else
+					return;
+			}
 
-			return;
+			hasAnySuccess = true;
 		}
 
-		logAi->debug("Decission madel in %ld", timeElapsed(start));
-
-		executeTask(bestTask);
+		if(!hasAnySuccess)
+		{
+			logAi->trace("Nothing was done this turn. Ending turn.");
+			return;
+		}
 
 		if(i == settings->getMaxPass())
 		{
-			logAi->warn("Goal %s exceeded maxpass. Terminating AI turn.", taskDescription);
+			logAi->warn("Maxpass exceeded. Terminating AI turn.");
 		}
 	}
 }
 
-void Nullkiller::executeTask(Goals::TTask task)
+bool Nullkiller::executeTask(Goals::TTask task)
 {
 	auto start = std::chrono::high_resolution_clock::now();
 	std::string taskDescr = task->toString();
@@ -376,10 +470,10 @@ void Nullkiller::executeTask(Goals::TTask task)
 		logAi->error("Failed to realize subgoal of type %s, I will stop.", taskDescr);
 		logAi->error("The error message was: %s", e.what());
 
-#if NKAI_TRACE_LEVEL == 0
-		throw; // will be recatched and AI turn ended
-#endif
+		return false;
 	}
+
+	return true;
 }
 
 TResources Nullkiller::getFreeResources() const

+ 22 - 3
AI/Nullkiller/Engine/Nullkiller.h

@@ -47,6 +47,24 @@ enum class ScanDepth
 	ALL_FULL = 2
 };
 
+struct TaskPlanItem
+{
+	std::vector<ObjectInstanceID> affectedObjects;
+	Goals::TSubgoal task;
+
+	TaskPlanItem(Goals::TSubgoal goal);
+};
+
+class TaskPlan
+{
+private:
+	std::vector<TaskPlanItem> tasks;
+
+public:
+	Goals::TTaskVec getTasks() const;
+	void merge(Goals::TSubgoal task);
+};
+
 class Nullkiller
 {
 private:
@@ -102,9 +120,10 @@ public:
 private:
 	void resetAiState();
 	void updateAiState(int pass, bool fast = false);
-	Goals::TTask choseBestTask(Goals::TSubgoal behavior, int decompositionMaxDepth) const;
-	Goals::TTask choseBestTask(Goals::TTaskVec & tasks) const;
-	void executeTask(Goals::TTask task);
+	void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const;
+	Goals::TTask choseBestTask(Goals::TGoalVec & tasks) const;
+	Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks) const;
+	bool executeTask(Goals::TTask task);
 };
 
 }

+ 22 - 21
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -85,7 +85,7 @@ void PriorityEvaluator::initVisitTile()
 	rewardTypeVariable = engine->getInputVariable("rewardType");
 	closestHeroRatioVariable = engine->getInputVariable("closestHeroRatio");
 	strategicalValueVariable = engine->getInputVariable("strategicalValue");
-	goldPreasureVariable = engine->getInputVariable("goldPreasure");
+	goldPressureVariable = engine->getInputVariable("goldPressure");
 	goldCostVariable = engine->getInputVariable("goldCost");
 	fearVariable = engine->getInputVariable("fear");
 	value = engine->getOutputVariable("Value");
@@ -158,6 +158,8 @@ uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHero
 
 	const auto& slots = hero->Slots();
 	ui64 weakestStackPower = 0;
+	int duplicatingSlots = getDuplicatingSlots(hero);
+
 	if (slots.size() >= GameConstants::ARMY_SIZE)
 	{
 		//No free slot, we might discard our weakest stack
@@ -172,7 +174,7 @@ uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHero
 	{
 		//Only if hero has slot for this creature in the army
 		auto ccre = dynamic_cast<const CCreature*>(c.data.type);
-		if (hero->getSlotFor(ccre).validSlot())
+		if (hero->getSlotFor(ccre).validSlot() || duplicatingSlots > 0)
 		{
 			result += (c.data.type->getAIValue() * c.data.count) * c.chance;
 		}
@@ -655,7 +657,7 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
 	case Obj::RESOURCE:
 	{
 		auto * res = dynamic_cast<const CGResource*>(target);
-		return res->resourceID() == GameResID::GOLD ? 600 : 100;
+		return res && res->resourceID() == GameResID::GOLD ? 600 : 100;
 	}
 	case Obj::TREASURE_CHEST:
 		return 1500;
@@ -711,8 +713,8 @@ public:
 
 		uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai);
 
-		evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero.get()->getArmyStrength());
-		evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero.get());
+		evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength());
+		evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero);
 	}
 };
 
@@ -743,7 +745,7 @@ public:
 
 		Goals::StayAtTown & stayAtTown = dynamic_cast<Goals::StayAtTown &>(*task);
 
-		evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero().get());
+		evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero());
 		evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
 		evaluationContext.movementCost += stayAtTown.getMovementWasted();
 	}
@@ -792,7 +794,7 @@ public:
 		if(defendTown.getTurn() > 0 && defendTown.isCounterAttack())
 		{
 			auto ourSpeed = defendTown.hero->movementPointsLimit(true);
-			auto enemySpeed = treat.hero->movementPointsLimit(true);
+			auto enemySpeed = treat.hero.get(evaluationContext.evaluator.ai->cb.get())->movementPointsLimit(true);
 
 			if(enemySpeed > ourSpeed) multiplier *= 0.7f;
 		}
@@ -847,13 +849,12 @@ public:
 			evaluationContext.movementCostByRole[role] += pair.second;
 		}
 
-		auto heroPtr = task->hero;
-		auto hero = heroPtr.get(ai->cb.get());
+		auto hero = task->hero;
 		bool checkGold = evaluationContext.danger == 0;
 		auto army = path.heroArmy;
 
 		const CGObjectInstance * target = ai->cb->getObj((ObjectInstanceID)task->objid, false);
-		auto heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroPtr);
+		auto heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(hero);
 
 		if(heroRole == HeroRole::MAIN)
 			evaluationContext.heroRole = heroRole;
@@ -887,21 +888,21 @@ public:
 		Goals::UnlockCluster & clusterGoal = dynamic_cast<Goals::UnlockCluster &>(*task);
 		std::shared_ptr<ObjectCluster> cluster = clusterGoal.getCluster();
 
-		auto hero = clusterGoal.hero.get();
-		auto role = evaluationContext.evaluator.ai->heroManager->getHeroRole(clusterGoal.hero);
+		auto hero = clusterGoal.hero;
+		auto role = evaluationContext.evaluator.ai->heroManager->getHeroRole(hero);
 
-		std::vector<std::pair<const CGObjectInstance *, ClusterObjectInfo>> objects(cluster->objects.begin(), cluster->objects.end());
+		std::vector<std::pair<ObjectInstanceID, ClusterObjectInfo>> objects(cluster->objects.begin(), cluster->objects.end());
 
-		std::sort(objects.begin(), objects.end(), [](std::pair<const CGObjectInstance *, ClusterObjectInfo> o1, std::pair<const CGObjectInstance *, ClusterObjectInfo> o2) -> bool
+		std::sort(objects.begin(), objects.end(), [](std::pair<ObjectInstanceID, ClusterObjectInfo> o1, std::pair<ObjectInstanceID, ClusterObjectInfo> o2) -> bool
 		{
 			return o1.second.priority > o2.second.priority;
 		});
 
 		int boost = 1;
 
-		for(auto objInfo : objects)
+		for(auto & objInfo : objects)
 		{
-			auto target = objInfo.first;
+			auto target = evaluationContext.evaluator.ai->cb->getObj(objInfo.first);
 			bool checkGold = objInfo.second.danger == 0;
 			auto army = hero;
 
@@ -960,7 +961,7 @@ public:
 			return;
 
 		Goals::DismissHero & dismissCommand = dynamic_cast<Goals::DismissHero &>(*task);
-		const CGHeroInstance * dismissedHero = dismissCommand.getHero().get();
+		const CGHeroInstance * dismissedHero = dismissCommand.getHero();
 
 		auto role = ai->heroManager->getHeroRole(dismissedHero);
 		auto mpLeft = dismissedHero->movementPointsRemaining();
@@ -1017,9 +1018,9 @@ public:
 		
 		if(evaluationContext.goldReward)
 		{
-			auto goldPreasure = evaluationContext.evaluator.ai->buildAnalyzer->getGoldPreasure();
+			auto goldPressure = evaluationContext.evaluator.ai->buildAnalyzer->getGoldPressure();
 
-			evaluationContext.addNonCriticalStrategicalValue(evaluationContext.goldReward * goldPreasure / 3500.0f / bi.prerequisitesCount);
+			evaluationContext.addNonCriticalStrategicalValue(evaluationContext.goldReward * goldPressure / 3500.0f / bi.prerequisitesCount);
 		}
 
 		if(bi.notEnoughRes && bi.prerequisitesCount == 1)
@@ -1111,7 +1112,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
 		rewardTypeVariable->setValue(rewardType);
 		closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio);
 		strategicalValueVariable->setValue(evaluationContext.strategicalValue);
-		goldPreasureVariable->setValue(ai->buildAnalyzer->getGoldPreasure());
+		goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure());
 		goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f));
 		turnVariable->setValue(evaluationContext.turn);
 		fearVariable->setValue(evaluationContext.enemyHeroDangerRatio);
@@ -1126,7 +1127,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
 	}
 
 #if NKAI_TRACE_LEVEL >= 2
-	logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %d, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f",
+	logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f",
 		task->toString(),
 		evaluationContext.armyLossPersentage,
 		(int)evaluationContext.turn,

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

@@ -109,7 +109,7 @@ private:
 	fl::InputVariable * strategicalValueVariable;
 	fl::InputVariable * rewardTypeVariable;
 	fl::InputVariable * closestHeroRatioVariable;
-	fl::InputVariable * goldPreasureVariable;
+	fl::InputVariable * goldPressureVariable;
 	fl::InputVariable * goldCostVariable;
 	fl::InputVariable * fearVariable;
 	fl::OutputVariable * value;

+ 14 - 27
AI/Nullkiller/Engine/Settings.cpp

@@ -18,7 +18,7 @@
 #include "../../../lib/modding/CModHandler.h"
 #include "../../../lib/VCMI_Lib.h"
 #include "../../../lib/filesystem/Filesystem.h"
-#include "../../../lib/json/JsonNode.h"
+#include "../../../lib/json/JsonUtils.h"
 
 namespace NKAI
 {
@@ -26,31 +26,13 @@ namespace NKAI
 		: maxRoamingHeroes(8),
 		mainHeroTurnDistanceLimit(10),
 		scoutHeroTurnDistanceLimit(5),
-		maxGoldPreasure(0.3f), 
-		maxpass(30),
-		allowObjectGraph(false)
+		maxGoldPressure(0.3f), 
+		maxpass(10),
+		allowObjectGraph(false),
+		useTroopsFromGarrisons(false)
 	{
-		ResourcePath resource("config/ai/nkai/nkai-settings", EResType::JSON);
+		JsonNode node = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings");
 
-		loadFromMod("core", resource);
-
-		for(const auto & modName : VLC->modh->getActiveMods())
-		{
-			if(CResourceHandler::get(modName)->existsResource(resource))
-				loadFromMod(modName, resource);
-		}
-	}
-
-	void Settings::loadFromMod(const std::string & modName, const ResourcePath & resource)
-	{
-		if(!CResourceHandler::get(modName)->existsResource(resource))
-		{
-			logGlobal->error("Failed to load font %s from mod %s", resource.getName(), modName);
-			return;
-		}
-
-	    JsonNode node(JsonPath::fromResource(resource), modName);
-		
 		if(node.Struct()["maxRoamingHeroes"].isNumber())
 		{
 			maxRoamingHeroes = node.Struct()["maxRoamingHeroes"].Integer();
@@ -71,14 +53,19 @@ namespace NKAI
 			maxpass = node.Struct()["maxpass"].Integer();
 		}
 
-		if(node.Struct()["maxGoldPreasure"].isNumber())
+		if(node.Struct()["maxGoldPressure"].isNumber())
 		{
-			maxGoldPreasure = node.Struct()["maxGoldPreasure"].Float();
+			maxGoldPressure = node.Struct()["maxGoldPressure"].Float();
 		}
 
 		if(!node.Struct()["allowObjectGraph"].isNull())
 		{
 			allowObjectGraph = node.Struct()["allowObjectGraph"].Bool();
 		}
+
+		if(!node.Struct()["useTroopsFromGarrisons"].isNull())
+		{
+			useTroopsFromGarrisons = node.Struct()["useTroopsFromGarrisons"].Bool();
+		}
 	}
-}
+}

+ 5 - 6
AI/Nullkiller/Engine/Settings.h

@@ -25,20 +25,19 @@ namespace NKAI
 		int mainHeroTurnDistanceLimit;
 		int scoutHeroTurnDistanceLimit;
 		int maxpass;
-		float maxGoldPreasure;
+		float maxGoldPressure;
 		bool allowObjectGraph;
+		bool useTroopsFromGarrisons;
 
 	public:
 		Settings();
 
 		int getMaxPass() const { return maxpass; }
-		float getMaxGoldPreasure() const { return maxGoldPreasure; }
+		float getMaxGoldPressure() const { return maxGoldPressure; }
 		int getMaxRoamingHeroes() const { return maxRoamingHeroes; }
 		int getMainHeroTurnDistanceLimit() const { return mainHeroTurnDistanceLimit; }
 		int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; }
 		bool isObjectGraphAllowed() const { return allowObjectGraph; }
-
-	private:
-		void loadFromMod(const std::string & modName, const ResourcePath & resource);
+		bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; }
 	};
-}
+}

+ 5 - 3
AI/Nullkiller/Goals/AbstractGoal.cpp

@@ -31,12 +31,12 @@ TTask Goals::taskptr(const AbstractGoal & tmp)
 	if(!tmp.isElementar())
 		throw cannotFulfillGoalException(tmp.toString() + " is not elementar");
 
-	ptr.reset(dynamic_cast<ITask *>(tmp.clone()));
+	ptr.reset(tmp.clone()->asTask());
 
 	return ptr;
 }
 
-std::string AbstractGoal::toString() const //TODO: virtualize
+std::string AbstractGoal::toString() const
 {
 	std::string desc;
 	switch(goalType)
@@ -63,8 +63,10 @@ std::string AbstractGoal::toString() const //TODO: virtualize
 	default:
 		return std::to_string(goalType);
 	}
-	if(hero.get(true)) //FIXME: used to crash when we lost hero and failed goal
+
+	if(hero)
 		desc += " (" + hero->getNameTranslated() + ")";
+
 	return desc;
 }
 

+ 12 - 5
AI/Nullkiller/Goals/AbstractGoal.h

@@ -10,9 +10,8 @@
 #pragma once
 
 #include "../../../lib/VCMI_Lib.h"
-#include "../../../lib/CBuildingHandler.h"
-#include "../../../lib/CCreatureHandler.h"
-#include "../../../lib/CTownHandler.h"
+#include "../../../lib/mapObjects/CGTownInstance.h"
+#include "../../../lib/mapObjects/CGHeroInstance.h"
 #include "../AIUtility.h"
 
 namespace NKAI
@@ -106,7 +105,7 @@ namespace Goals
 		int objid; SETTER(int, objid)
 		int aid; SETTER(int, aid)
 		int3 tile; SETTER(int3, tile)
-		HeroPtr hero; SETTER(HeroPtr, hero)
+		const CGHeroInstance * hero; SETTER(CGHeroInstance *, hero)
 		const CGTownInstance *town; SETTER(CGTownInstance *, town)
 		int bid; SETTER(int, bid)
 
@@ -119,6 +118,7 @@ namespace Goals
 			objid = -1;
 			tile = int3(-1, -1, -1);
 			town = nullptr;
+			hero = nullptr;
 			bid = -1;
 			goldCost = 0;
 		}
@@ -147,6 +147,11 @@ namespace Goals
 		virtual bool hasHash() const { return false; }
 
 		virtual uint64_t getHash() const { return 0; }
+
+		virtual ITask * asTask()
+		{
+			throw std::runtime_error("Abstract goal is not a task");
+		}
 		
 		bool operator!=(const AbstractGoal & g) const
 		{
@@ -165,9 +170,11 @@ namespace Goals
 		//TODO: make accept work for std::shared_ptr... somehow
 		virtual void accept(AIGateway * ai) = 0; //unhandled goal will report standard error
 		virtual std::string toString() const = 0;
-		virtual HeroPtr getHero() const = 0;
+		virtual const CGHeroInstance * getHero() const = 0;
 		virtual ~ITask() {}
 		virtual int getHeroExchangeCount() const = 0;
+		virtual bool isObjectAffected(ObjectInstanceID h) const = 0;
+		virtual std::vector<ObjectInstanceID> getAffectedObjects() const = 0;
 	};
 }
 

+ 3 - 3
AI/Nullkiller/Goals/AdventureSpellCast.cpp

@@ -18,12 +18,12 @@ using namespace Goals;
 
 bool AdventureSpellCast::operator==(const AdventureSpellCast & other) const
 {
-	return hero.h == other.hero.h;
+	return hero == other.hero;
 }
 
 void AdventureSpellCast::accept(AIGateway * ai)
 {
-	if(!hero.validAndSet())
+	if(!hero)
 		throw cannotFulfillGoalException("Invalid hero!");
 
 	auto spell = getSpell();
@@ -56,7 +56,7 @@ void AdventureSpellCast::accept(AIGateway * ai)
 	auto wait = cb->waitTillRealize;
 
 	cb->waitTillRealize = true;
-	cb->castSpell(hero.h, spellID, tile);
+	cb->castSpell(hero, spellID, tile);
 
 	if(town && spellID == SpellID::TOWN_PORTAL)
 	{

+ 1 - 1
AI/Nullkiller/Goals/AdventureSpellCast.h

@@ -22,7 +22,7 @@ namespace Goals
 		SpellID spellID;
 
 	public:
-		AdventureSpellCast(HeroPtr hero, SpellID spellID)
+		AdventureSpellCast(const CGHeroInstance * hero, SpellID spellID)
 			: ElementarGoal(Goals::ADVENTURE_SPELL_CAST), spellID(spellID)
 		{
 			sethero(hero);

+ 29 - 2
AI/Nullkiller/Goals/CGoal.h

@@ -14,7 +14,6 @@
 namespace NKAI
 {
 
-struct HeroPtr;
 class AIGateway;
 
 namespace Goals
@@ -92,9 +91,37 @@ namespace Goals
 
 		bool isElementar() const override { return true; }
 
-		HeroPtr getHero() const override { return AbstractGoal::hero; }
+		const CGHeroInstance * getHero() const override { return AbstractGoal::hero; }
 
 		int getHeroExchangeCount() const override { return 0; }
+
+		bool isObjectAffected(ObjectInstanceID id) const override
+		{
+			return (AbstractGoal::hero && AbstractGoal::hero->id == id)
+				|| AbstractGoal::objid == id
+				|| (AbstractGoal::town && AbstractGoal::town->id == id);
+		}
+
+		std::vector<ObjectInstanceID> getAffectedObjects() const override
+		{
+			auto result = std::vector<ObjectInstanceID>();
+
+			if(AbstractGoal::hero)
+				result.push_back(AbstractGoal::hero->id);
+
+			if(AbstractGoal::objid != -1)
+				result.push_back(ObjectInstanceID(AbstractGoal::objid));
+
+			if(AbstractGoal::town)
+				result.push_back(AbstractGoal::town->id);
+
+			return result;
+		}
+
+		ITask * asTask() override
+		{
+			return this;
+		}
 	};
 }
 

+ 1 - 1
AI/Nullkiller/Goals/CompleteQuest.cpp

@@ -94,7 +94,7 @@ std::string CompleteQuest::questToString() const
 		return "find " + VLC->generaltexth->tentColors[q.obj->subID] + " keymaster tent";
 	}
 
-	if(q.quest->questName == CQuest::missionName(0))
+	if(q.quest->questName == CQuest::missionName(EQuestMission::NONE))
 		return "inactive quest";
 
 	MetaString ms;

+ 32 - 0
AI/Nullkiller/Goals/Composition.cpp

@@ -117,4 +117,36 @@ int Composition::getHeroExchangeCount() const
 	return result;
 }
 
+std::vector<ObjectInstanceID> Composition::getAffectedObjects() const
+{
+	std::vector<ObjectInstanceID> affectedObjects;
+
+	for(auto sequence : subtasks)
+	{
+		for(auto task : sequence)
+		{
+			if(task->isElementar())
+				vstd::concatenate(affectedObjects, task->asTask()->getAffectedObjects());
+		}
+	}
+
+	vstd::removeDuplicates(affectedObjects);
+
+	return affectedObjects;
+}
+
+bool Composition::isObjectAffected(ObjectInstanceID id) const
+{
+	for(auto sequence : subtasks)
+	{
+		for(auto task : sequence)
+		{
+			if(task->isElementar() && task->asTask()->isObjectAffected(id))
+				return true;
+		}
+	}
+
+	return false;
+}
+
 }

+ 3 - 0
AI/Nullkiller/Goals/Composition.h

@@ -35,6 +35,9 @@ namespace Goals
 		TGoalVec decompose(const Nullkiller * ai) const override;
 		bool isElementar() const override;
 		int getHeroExchangeCount() const override;
+
+		std::vector<ObjectInstanceID> getAffectedObjects() const override;
+		bool isObjectAffected(ObjectInstanceID id) const override;
 	};
 }
 

+ 1 - 1
AI/Nullkiller/Goals/DigAtTile.cpp

@@ -20,7 +20,7 @@ using namespace Goals;
 
 bool DigAtTile::operator==(const DigAtTile & other) const
 {
-	return other.hero.h == hero.h && other.tile == tile;
+	return other.hero == hero && other.tile == tile;
 }
 //
 //TSubgoal DigAtTile::decomposeSingle() const

+ 4 - 4
AI/Nullkiller/Goals/DismissHero.cpp

@@ -18,22 +18,22 @@ using namespace Goals;
 
 bool DismissHero::operator==(const DismissHero & other) const
 {
-	return hero.h == other.hero.h;
+	return hero == other.hero;
 }
 
 void DismissHero::accept(AIGateway * ai)
 {
-	if(!hero.validAndSet())
+	if(!hero)
 		throw cannotFulfillGoalException("Invalid hero!");
 
-	cb->dismissHero(hero.h);
+	cb->dismissHero(hero);
 
 	throw goalFulfilledException(sptr(*this));
 }
 
 std::string DismissHero::toString() const
 {
-	return "DismissHero " + hero.name;
+	return "DismissHero " + heroName;
 }
 
 }

+ 5 - 1
AI/Nullkiller/Goals/DismissHero.h

@@ -17,11 +17,15 @@ namespace Goals
 {
 	class DLL_EXPORT DismissHero : public ElementarGoal<DismissHero>
 	{
+	private:
+		std::string heroName;
+
 	public:
-		DismissHero(HeroPtr hero)
+		DismissHero(const CGHeroInstance * hero)
 			: ElementarGoal(Goals::DISMISS_HERO)
 		{
 			sethero(hero);
+			heroName = hero->getNameTranslated();
 		}
 
 		void accept(AIGateway * ai) override;

+ 20 - 0
AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp

@@ -26,6 +26,26 @@ ExchangeSwapTownHeroes::ExchangeSwapTownHeroes(
 {
 }
 
+std::vector<ObjectInstanceID> ExchangeSwapTownHeroes::getAffectedObjects() const
+{
+	std::vector<ObjectInstanceID> affectedObjects = { town->id };
+
+	if(town->garrisonHero)
+		affectedObjects.push_back(town->garrisonHero->id);
+
+	if(town->visitingHero)
+		affectedObjects.push_back(town->visitingHero->id);
+
+	return affectedObjects;
+}
+
+bool ExchangeSwapTownHeroes::isObjectAffected(ObjectInstanceID id) const
+{
+	return town->id == id
+		|| (town->visitingHero && town->visitingHero->id == id)
+		|| (town->garrisonHero && town->garrisonHero->id == id);
+}
+
 std::string ExchangeSwapTownHeroes::toString() const
 {
 	return "Exchange and swap heroes of " + town->getNameTranslated();

+ 3 - 0
AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h

@@ -35,6 +35,9 @@ namespace Goals
 
 		const CGHeroInstance * getGarrisonHero() const { return garrisonHero; }
 		HeroLockedReason getLockingReason() const { return lockingReason; }
+
+		std::vector<ObjectInstanceID> getAffectedObjects() const override;
+		bool isObjectAffected(ObjectInstanceID id) const override;
 	};
 }
 

+ 39 - 0
AI/Nullkiller/Goals/ExecuteHeroChain.cpp

@@ -42,6 +42,38 @@ bool ExecuteHeroChain::operator==(const ExecuteHeroChain & other) const
 		&& chainPath.chainMask == other.chainPath.chainMask;
 }
 
+std::vector<ObjectInstanceID> ExecuteHeroChain::getAffectedObjects() const
+{
+	std::vector<ObjectInstanceID> affectedObjects = { chainPath.targetHero->id };
+
+	if(objid != -1)
+		affectedObjects.push_back(ObjectInstanceID(objid));
+
+	for(auto & node : chainPath.nodes)
+	{
+		if(node.targetHero)
+			affectedObjects.push_back(node.targetHero->id);
+	}
+
+	vstd::removeDuplicates(affectedObjects);
+
+	return affectedObjects;
+}
+
+bool ExecuteHeroChain::isObjectAffected(ObjectInstanceID id) const
+{
+	if(chainPath.targetHero->id == id || objid == id)
+		return true;
+
+	for(auto & node : chainPath.nodes)
+	{
+		if(node.targetHero && node.targetHero->id == id)
+			return true;
+	}
+
+	return false;
+}
+
 void ExecuteHeroChain::accept(AIGateway * ai)
 {
 	logAi->debug("Executing hero chain towards %s. Path %s", targetName, chainPath.toString());
@@ -72,6 +104,13 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 		const CGHeroInstance * hero = node->targetHero;
 		HeroPtr heroPtr = hero;
 
+		if(!heroPtr.validAndSet())
+		{
+			logAi->error("Hero %s was lost. Exit hero chain.", heroPtr.name);
+
+			return;
+		}
+
 		if(node->parentIndex >= i)
 		{
 			logAi->error("Invalid parentIndex while executing node " + node->coord.toString());

+ 3 - 0
AI/Nullkiller/Goals/ExecuteHeroChain.h

@@ -34,6 +34,9 @@ namespace Goals
 
 		int getHeroExchangeCount() const override { return chainPath.exchangeCount; }
 
+		std::vector<ObjectInstanceID> getAffectedObjects() const override;
+		bool isObjectAffected(ObjectInstanceID id) const override;
+
 	private:
 		bool moveHeroToTile(AIGateway * ai, const CGHeroInstance * hero, const int3 & tile);
 	};

+ 1 - 1
AI/Nullkiller/Goals/StayAtTown.cpp

@@ -46,7 +46,7 @@ void StayAtTown::accept(AIGateway * ai)
 		logAi->error("Hero %s expected visiting town %s", hero->getNameTranslated(), town->getNameTranslated());
 	}
 
-	ai->nullkiller->lockHero(hero.get(), HeroLockedReason::DEFENCE);
+	ai->nullkiller->lockHero(hero, HeroLockedReason::DEFENCE);
 }
 
 }

+ 1 - 1
AI/Nullkiller/Markers/ArmyUpgrade.cpp

@@ -22,7 +22,7 @@ ArmyUpgrade::ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * up
 	: CGoal(Goals::ARMY_UPGRADE), upgrader(upgrader), upgradeValue(upgrade.upgradeValue),
 	initialValue(upgradePath.heroArmy->getArmyStrength()), goldCost(upgrade.upgradeCost[EGameResID::GOLD])
 {
-	sethero(upgradePath.targetHero);
+	hero = upgradePath.targetHero;
 }
 
 ArmyUpgrade::ArmyUpgrade(const CGHeroInstance * targetMain, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade)

+ 1 - 1
AI/Nullkiller/Markers/DefendTown.cpp

@@ -22,7 +22,7 @@ DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, co
 	: CGoal(Goals::DEFEND_TOWN), treat(treat), defenceArmyStrength(defencePath.getHeroStrength()), turn(defencePath.turn()), counterattack(isCounterAttack)
 {
 	settown(town);
-	sethero(defencePath.targetHero);
+	hero = defencePath.targetHero;
 }
 
 DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const CGHeroInstance * defender)

+ 2 - 2
AI/Nullkiller/Markers/HeroExchange.cpp

@@ -26,12 +26,12 @@ bool HeroExchange::operator==(const HeroExchange & other) const
 
 std::string HeroExchange::toString() const
 {
-	return "Hero exchange for " +hero.get()->getObjectName() + " by " + exchangePath.toString();
+	return "Hero exchange for " +hero->getObjectName() + " by " + exchangePath.toString();
 }
 
 uint64_t HeroExchange::getReinforcementArmyStrength(const Nullkiller * ai) const
 {
-	uint64_t armyValue = ai->armyManager->howManyReinforcementsCanGet(hero.get(), exchangePath.heroArmy);
+	uint64_t armyValue = ai->armyManager->howManyReinforcementsCanGet(hero, exchangePath.heroArmy);
 
 	return armyValue;
 }

+ 1 - 1
AI/Nullkiller/Markers/UnlockCluster.h

@@ -33,7 +33,7 @@ namespace Goals
 			: CGoal(Goals::UNLOCK_CLUSTER), cluster(cluster), pathToCenter(pathToCenter)
 		{
 			tile = cluster->blocker->visitablePos();
-			sethero(pathToCenter.targetHero);
+			hero = pathToCenter.targetHero;
 		}
 
 		bool operator==(const UnlockCluster & other) const override;

+ 4 - 0
AI/Nullkiller/Pathfinding/AIPathfinder.cpp

@@ -105,7 +105,11 @@ void AIPathfinder::updatePaths(const std::map<const CGHeroInstance *, HeroRole>
 	cb->calculatePaths(config);
 
 	if(!pathfinderSettings.useHeroChain)
+	{
+		logAi->trace("Recalculated paths in %ld", timeElapsed(start));
+
 		return;
+	}
 
 	do
 	{

+ 4 - 1
AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp

@@ -35,7 +35,10 @@ namespace AIPathfinding
 			return dynamic_cast<const IQuestObject *>(questInfo.obj)->checkQuest(hero);
 		}
 
-		return questInfo.quest->activeForPlayers.count(hero->getOwner())
+		auto notActivated = !questInfo.obj->wasVisited(ai->playerID)
+			&& !questInfo.quest->activeForPlayers.count(hero->getOwner());
+		
+		return notActivated
 			|| questInfo.quest->checkQuest(hero);
 	}
 

+ 9 - 2
AI/Nullkiller/Pathfinding/ObjectGraph.cpp

@@ -112,7 +112,9 @@ public:
 
 	void addMinimalDistanceJunctions()
 	{
-		pforeachTilePaths(ai->cb->getMapSize(), ai, [this](const int3 & pos, std::vector<AIPath> & paths)
+		tbb::concurrent_unordered_set<int3, std::hash<int3>> junctions;
+
+		pforeachTilePaths(ai->cb->getMapSize(), ai, [this, &junctions](const int3 & pos, std::vector<AIPath> & paths)
 			{
 				if(target->hasNodeAt(pos))
 					return;
@@ -129,9 +131,14 @@ public:
 
 				if(currentCost.avg < neighborCost)
 				{
-					addJunctionActor(pos);
+					junctions.insert(pos);
 				}
 			});
+
+		for(auto pos : junctions)
+		{
+			addJunctionActor(pos);
+		}
 	}
 
 private:

+ 4 - 1
AI/VCAI/Goals/CollectRes.cpp

@@ -146,7 +146,10 @@ TSubgoal CollectRes::whatToDoToTrade()
 	markets.erase(boost::remove_if(markets, [](const IMarket * market) -> bool
 	{
 		auto * o = dynamic_cast<const CGObjectInstance *>(market);
-		if(o && !(o->ID == Obj::TOWN && o->tempOwner == ai->playerID))
+		// FIXME: disabled broken visitation of external markets
+		//if(o && !(o->ID == Obj::TOWN && o->tempOwner == ai->playerID))
+
+		if(o && o->ID == Obj::TOWN)
 		{
 			if(!ai->isAccessible(o->visitablePos()))
 				return true;

+ 1 - 1
AI/VCAI/Goals/CompleteQuest.cpp

@@ -99,7 +99,7 @@ std::string CompleteQuest::completeMessage() const
 
 std::string CompleteQuest::questToString() const
 {
-	if(q.quest->questName == CQuest::missionName(0))
+	if(q.quest->questName == CQuest::missionName(EQuestMission::NONE))
 		return "inactive quest";
 
 	MetaString ms;

+ 6 - 5
AI/VCAI/VCAI.cpp

@@ -16,6 +16,7 @@
 
 #include "../../lib/ArtifactUtils.h"
 #include "../../lib/UnlockGuard.h"
+#include "../../lib/StartInfo.h"
 #include "../../lib/mapObjects/MapObjects.h"
 #include "../../lib/mapObjects/ObjectTemplate.h"
 #include "../../lib/CConfigHandler.h"
@@ -654,9 +655,9 @@ void VCAI::commanderGotLevel(const CCommanderInstance * commander, std::vector<u
 	requestActionASAP([=](){ answerQuery(queryID, 0); });
 }
 
-void VCAI::showBlockingDialog(const std::string & text, const std::vector<Component> & components, QueryID askID, const int soundID, bool selection, bool cancel)
+void VCAI::showBlockingDialog(const std::string & text, const std::vector<Component> & components, QueryID askID, const int soundID, bool selection, bool cancel, bool safeToAutoaccept)
 {
-	LOG_TRACE_PARAMS(logAi, "text '%s', askID '%i', soundID '%i', selection '%i', cancel '%i'", text % askID % soundID % selection % cancel);
+	LOG_TRACE_PARAMS(logAi, "text '%s', askID '%i', soundID '%i', selection '%i', cancel '%i', autoaccept '%i'", text % askID % soundID % selection % cancel % safeToAutoaccept);
 	NET_EVENT_HANDLER;
 	int sel = 0;
 	status.addQuery(askID, boost::str(boost::format("Blocking dialog query with %d components - %s")
@@ -732,7 +733,7 @@ void VCAI::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance *
 	//you can't request action from action-response thread
 	requestActionASAP([=]()
 	{
-		if(removableUnits)
+		if(removableUnits && !cb->getStartInfo()->isSteadwickFallCampaignMission())
 			pickBestCreatures(down, up);
 
 		answerQuery(queryID, 0);
@@ -1837,7 +1838,7 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
 	{
 		//FIXME: this assertion fails also if AI moves onto defeated guarded object
 		assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object
-		cb->moveHero(*h, h->convertFromVisitablePos(dst));
+		cb->moveHero(*h, h->convertFromVisitablePos(dst), false);
 		afterMovementCheck(); // TODO: is it feasible to hero get killed there if game work properly?
 		// If revisiting, teleport probing is never done, and so the entries into the list would remain unused and uncleared
 		teleportChannelProbingList.clear();
@@ -1899,7 +1900,7 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
 			destinationTeleport = exitId;
 			if(exitPos.valid())
 				destinationTeleportPos = h->convertFromVisitablePos(exitPos);
-			cb->moveHero(*h, h->pos);
+			cb->moveHero(*h, h->pos, false);
 			destinationTeleport = ObjectInstanceID();
 			destinationTeleportPos = int3(-1);
 			afterMovementCheck();

+ 1 - 1
AI/VCAI/VCAI.h

@@ -148,7 +148,7 @@ public:
 
 	void heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector<SecondarySkill> & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id
 	void commanderGotLevel(const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID) override; //TODO
-	void showBlockingDialog(const std::string & text, const std::vector<Component> & components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID.
+	void showBlockingDialog(const std::string & text, const std::vector<Component> & components, QueryID askID, const int soundID, bool selection, bool cancel, bool safeToAutoaccept) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID.
 	void showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) override; //all stacks operations between these objects become allowed, interface has to call onEnd when done
 	void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override;
 	void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects) override;

+ 8 - 3
CCallback.cpp

@@ -34,11 +34,16 @@ bool CCallback::teleportHero(const CGHeroInstance *who, const CGTownInstance *wh
 	return true;
 }
 
-bool CCallback::moveHero(const CGHeroInstance *h, int3 dst, bool transit)
+void CCallback::moveHero(const CGHeroInstance *h, const int3 & destination, bool transit)
 {
-	MoveHero pack(dst,h->id,transit);
+	MoveHero pack({destination}, h->id, transit);
+	sendRequest(&pack);
+}
+
+void CCallback::moveHero(const CGHeroInstance *h, const std::vector<int3> & path, bool transit)
+{
+	MoveHero pack(path, h->id, transit);
 	sendRequest(&pack);
-	return true;
 }
 
 int CCallback::selectionMade(int selection, QueryID queryID)

+ 4 - 2
CCallback.h

@@ -67,7 +67,8 @@ class IGameActionCallback
 {
 public:
 	//hero
-	virtual bool moveHero(const CGHeroInstance *h, int3 dst, bool transit) =0; //dst must be free, neighbouring tile (this function can move hero only by one tile)
+	virtual void moveHero(const CGHeroInstance *h, const std::vector<int3> & path, bool transit) =0; //moves hero alongside provided path
+	virtual void moveHero(const CGHeroInstance *h, const int3 & destination, bool transit) =0; //moves hero alongside provided path
 	virtual bool dismissHero(const CGHeroInstance * hero)=0; //dismisses given hero; true - successfuly, false - not successfuly
 	virtual void dig(const CGObjectInstance *hero)=0;
 	virtual void castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos = int3(-1, -1, -1))=0; //cast adventure map spell
@@ -159,7 +160,8 @@ public:
 	void unregisterBattleInterface(std::shared_ptr<IBattleEventsReceiver> battleEvents);
 
 //commands
-	bool moveHero(const CGHeroInstance *h, int3 dst, bool transit = false) override; //dst must be free, neighbouring tile (this function can move hero only by one tile)
+	void moveHero(const CGHeroInstance *h, const std::vector<int3> & path, bool transit) override;
+	void moveHero(const CGHeroInstance *h, const int3 & destination, bool transit) override;
 	bool teleportHero(const CGHeroInstance *who, const CGTownInstance *where);
 	int selectionMade(int selection, QueryID queryID) override;
 	int sendQueryReply(std::optional<int32_t> reply, QueryID queryID) override;

+ 1 - 1
CI/ios/before_install.sh

@@ -3,5 +3,5 @@
 echo DEVELOPER_DIR=/Applications/Xcode_14.2.app >> $GITHUB_ENV
 
 mkdir ~/.conan ; cd ~/.conan
-curl -L 'https://github.com/vcmi/vcmi-ios-deps/releases/download/1.2/ios-arm64.txz' \
+curl -L 'https://github.com/vcmi/vcmi-ios-deps/releases/download/1.2.1/ios-arm64.txz' \
 	| tar -xf -

+ 1 - 1
CI/linux-qt6/before_install.sh

@@ -3,7 +3,7 @@
 sudo apt-get update
 
 # Dependencies
-sudo apt-get install libboost-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-locale-dev \
+sudo apt-get install libboost-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-locale-dev libboost-iostreams-dev \
 libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \
 qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools \
 ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \

+ 1 - 1
CI/linux/before_install.sh

@@ -3,7 +3,7 @@
 sudo apt-get update
 
 # Dependencies
-sudo apt-get install libboost-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-locale-dev \
+sudo apt-get install libboost-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-locale-dev libboost-iostreams-dev \
 libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \
 qtbase5-dev \
 ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \

+ 1 - 1
CI/mac/before_install.sh

@@ -5,5 +5,5 @@ echo DEVELOPER_DIR=/Applications/Xcode_14.2.app >> $GITHUB_ENV
 brew install ninja
 
 mkdir ~/.conan ; cd ~/.conan
-curl -L "https://github.com/vcmi/vcmi-deps-macos/releases/download/1.2/$DEPS_FILENAME.txz" \
+curl -L "https://github.com/vcmi/vcmi-deps-macos/releases/download/1.2.1/$DEPS_FILENAME.txz" \
 	| tar -xf -

+ 1 - 1
CI/mingw-32/before_install.sh

@@ -12,5 +12,5 @@ curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-
   && sudo dpkg -i mingw-w64-i686-dev_10.0.0-3_all.deb;
 
 mkdir ~/.conan ; cd ~/.conan
-curl -L "https://github.com/vcmi/vcmi-deps-windows-conan/releases/download/1.1/vcmi-deps-windows-conan-w32.tgz" \
+curl -L "https://github.com/vcmi/vcmi-deps-windows-conan/releases/download/1.2/vcmi-deps-windows-conan-w32.tgz" \
 	| tar -xzf -

+ 1 - 1
CI/mingw/before_install.sh

@@ -12,5 +12,5 @@ curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-
   && sudo dpkg -i mingw-w64-x86-64-dev_10.0.0-3_all.deb;
 
 mkdir ~/.conan ; cd ~/.conan
-curl -L "https://github.com/vcmi/vcmi-deps-windows-conan/releases/download/1.1/vcmi-deps-windows-conan-w64.tgz" \
+curl -L "https://github.com/vcmi/vcmi-deps-windows-conan/releases/download/1.2/vcmi-deps-windows-conan-w64.tgz" \
 	| tar -xzf -

+ 8 - 1
CMakeLists.txt

@@ -88,6 +88,9 @@ if(ENABLE_ERM AND NOT ENABLE_LUA)
 	set(ENABLE_LUA ON)
 endif()
 
+include(CMakeDependentOption)
+cmake_dependent_option(ENABLE_INNOEXTRACT "Enable innoextract for GOG file extraction in launcher" ON "ENABLE_LAUNCHER" OFF)
+
 ############################################
 #        Miscellaneous options             #
 ############################################
@@ -453,7 +456,11 @@ endif()
 #        Finding packages                  #
 ############################################
 
-find_package(Boost 1.48.0 REQUIRED COMPONENTS date_time filesystem locale program_options system thread)
+set(BOOST_COMPONENTS date_time filesystem locale program_options system thread)
+if(ENABLE_INNOEXTRACT)
+	list(APPEND BOOST_COMPONENTS iostreams)
+endif()
+find_package(Boost 1.48.0 REQUIRED COMPONENTS ${BOOST_COMPONENTS})
 
 find_package(ZLIB REQUIRED)
 # Conan compatibility

+ 32 - 0
ChangeLog.md

@@ -16,13 +16,24 @@
 * Fixed crash when player has manual control of arrow towers during siege
 * Fixed crash on attempt to attack with Magma Elementals with Erdamon as hero
 * Fixed crash on attempt to access removed Quest Guard
+* Fixed crash on moving through whirlpool when hero has no troops other than commander
+* Fixed possible freeze when moving hero over events that give enough experience to cause a level-up
+* Fixed possible crash on movement of double-wide creatures next to gates during siege
 
 ### Multiplayer
 * Implemented new lobby, available in game with persistent accounts and chat
 * Removed old lobby previously available in launcher
 * Fixed potential crash that could occur if two players act at the very same time
+* Game will no longer pause due to network lag after every tile when instant movement speed is selected in multiplayer
 
 ### Interface
+* Implemented configurable keyboard shortcuts, editable in file config/shortcutsConfig.json
+* Fixed broken keyboard shortcuts in main menu
+* If UI Enhancements are enabled, the game will skip confirmation dialogs when entering owned dwellings or refugee camp.
+* It is now possible to move artifact to or from backpack using Alt+click
+* It is now possible to transfer artifact to another hero during exchange using Ctrl+click
+* It is no longer possible to start single scenario by pressing "Enter", in line with H3 and to prevent interference with game chat
+* Empty treasure banks will no longer ask for confirmation when entering
 * Game will now save last used difficulty settings
 * Town Portal dialog will now show town icons
 * Town Portal dialog will now show town info on right click
@@ -44,6 +55,10 @@
 * Fixed translation of some bonuses using incorrect language
 * Added option to use 'nearest' rounding mode for UI scaling
 * Fixed various minor bugs in trade window interface
+* Game will now correctly reset artifact drag-and-drop cursor if player opens another dialog on top of hero window
+* If player has no valid saves, game will pick "NEWGAME" as proposed save name instead of empty field
+* Fixed incorrect visitation sounds of Crypt, Shipwreck and Abandoned Ship
+* Fixed double sound playback on capturing mines
 
 ### Campaigns
 * Game will now correctly track who defeated the hero or wandering monsters for related quests and victory conditions
@@ -52,6 +67,7 @@
 * Birth of a Barbarian: Yog can no longer purchase spellbook from the Mage Guild
 * Birth of a Barbarian: Yog will no longer gain Spellpower or Knowledge when leveling up
 * Birth of a Barbarian: Scenarios with mission to deliver an artifact will no longer end after just defeating enemies
+* Dungeons and Devils: AI will no longer take troops from garrisons in "Fall of Steadwick" scenario, in line with H3
 * Gem will now have her class set to "Sorceress" in campaigns
 * Fixed missing names for heroes who have their names customized in map after being transferred to the next scenario
 * Artifact transfer will now work correctly if the hero holding the transferable artifact is not also transferring
@@ -59,6 +75,7 @@
 * Fixed crash on advancing to campaign mission in which you can pick hero as starting bonus
 * It is now possible to replay the intro movie from the scenario information window
 * When playing the intro video, the subtitles are now correctly synchronized with the audio
+* Fixed invalid string on right-clicking secondary skill starting bonus
 
 ### Battles
 * Added option to enable unlimited combat replays during game setup
@@ -86,12 +103,15 @@
 * It is no longer possible to use summoning spells if such spell would summon 0 creatures
 * It is now possible to assemble or disassemble artifacts while in Altar of Sacrifice
 * It is no longer possible to move war machines to Altar of Sacrifice
+* If HotA mod is enabled, game will no longer incorrectly replace all prisons on map with HotA version
+* Fixed regression leading to large elemental dwellings being used as replacements for random dwellings
 
 ### Random Maps Generator
 * Game will now save last used RMG settings in game and in editor
 * Reduced number of obstacles placed in water zones
 * Treasure values in water zone should now be similar to values from HotA, due to bugs in H3:SoD values
 * Random map templates can now have optional description visible in random map setup
+* Implemented biomes system, for more consistent and natural obstacles placement
 * Implemented Penrose tiling to produce more natural zone edges
 * Increased minimal density of obstacles on surface level of the map
 * Decreased minimal density of obstacles on undergound level of the map
@@ -101,6 +121,11 @@
 * Windmill will now appear on top of all other objects
 
 ### Launcher
+* Launcher now supports installation of Heroes 3 data using gog.com offline installer thanks to innoextract tool
+* Fixed loading of mod screenshots if player opens screenshots tab without any preloaded screenshots
+* Fixed installation of mods if it has non-installed submod as dependency
+* It is now possible to import game settings using drag-and-drop
+* Added button to import mods, maps, or settings in addition to drag-and-drop
 * Added Spanish translation to launcher
 * Added Portuguese translation to launcher
 
@@ -120,6 +145,11 @@
 * Reduced memory usage and improved performance of AI pathfinding
 * Added experimental and disabled by default implementation of object graph
 * It is now possible to configure AI settings via config file
+* Improved parallelization when AI has multiple heroes
+* AI-controlled creatures will now correctly move across wide moat in Fortress
+* Fixed system error messages caused by visitation of Trading Posts by VCAI 
+* Patrolling heroes will never retreat from the battle
+* AI will now consider strength of town garrison and not just strength of visiting hero when deciding to attack town
 
 ### Modding
 * Added new game setting that allows inviting heroes to taverns
@@ -130,6 +160,8 @@
 * Replaced bonus MANA_PER_KNOWLEDGE with MANA_PER_KNOWLEDGE_PERCENTAGE to avoid rounding error with mysticism
 * Factions can now be marked as 'special', banning them from random selection
 * Replaced 'convert txt' text export command with more convenient 'translate' and 'translate maps' commands
+* Game will now report cases where minimal damage of a creature is greater than maximal damage
+* Added bonuses RESOURCES_CONSTANT_BOOST and RESOURCES_TOWN_MULTIPLYING_BOOST
 
 # 1.4.4 -> 1.4.5
 

二进制
Mods/vcmi/Data/lobby/iconEnter.png


+ 81 - 2
Mods/vcmi/config/vcmi/chinese.json

@@ -71,6 +71,7 @@
 	"vcmi.lobby.noPreview" : "无地上部分",
 	"vcmi.lobby.noUnderground" : "无地下部分",
 	"vcmi.lobby.sortDate" : "以修改时间排序地图",
+	"vcmi.lobby.backToLobby" : "返回大厅",
 
 	"vcmi.lobby.login.title" : "VCMI大厅",
 	"vcmi.lobby.login.username" : "用户名:",
@@ -78,7 +79,17 @@
 	"vcmi.lobby.login.error" : "连接错误: %s",
 	"vcmi.lobby.login.create" : "新账号",
 	"vcmi.lobby.login.login" : "登录",
-
+	"vcmi.lobby.login.as" : "以 %s 身份登录",
+	"vcmi.lobby.header.rooms" : "游戏房间 - %d",
+	"vcmi.lobby.header.channels" : "聊天频道",
+	"vcmi.lobby.header.chat.global" : "全局游戏聊天 - %s", // %s -> language name
+	"vcmi.lobby.header.chat.match" : "在 %s 的游戏聊天", // %s -> game start date & time
+	"vcmi.lobby.header.chat.player" : "与 %s 私聊", // %s -> nickname of another player
+	"vcmi.lobby.header.history" : "你的历史游戏",
+	"vcmi.lobby.header.players" : "在线玩家 - %d",
+	"vcmi.lobby.match.solo" : "单人游戏",
+	"vcmi.lobby.match.duel" : "与 %s 的游戏", // %s -> nickname of another player
+	"vcmi.lobby.match.multi" : "%d 个玩家",
 	"vcmi.lobby.room.create" : "创建房间",
 	"vcmi.lobby.room.players.limit" : "玩家限制",
 	"vcmi.lobby.room.public" : "公开",
@@ -92,6 +103,10 @@
 	"vcmi.lobby.room.load" : "加载游戏",
 	"vcmi.lobby.room.type" : "房间类型",
 	"vcmi.lobby.room.mode" : "游戏模式",
+	"vcmi.lobby.room.state.public" : "公开",
+	"vcmi.lobby.room.state.private" : "私有",
+	"vcmi.lobby.room.state.busy" : "游戏中",
+	"vcmi.lobby.room.state.invited" : "已邀请",
 
 	"vcmi.client.errors.invalidMap" : "{非法地图或战役}\n\n启动游戏失败,选择的地图或者战役,无效或被污染。原因:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{找不到数据文件}\n\n没有找到战役数据文件!你可能使用了不完整或损坏的英雄无敌3数据文件,请重新安装数据文件。",
@@ -103,6 +118,8 @@
 	"vcmi.server.errors.modConflict" : "读取的mod包 {'%s'}无法运行!\n 与另一个mod {'%s'}冲突!\n",
 	"vcmi.server.errors.unknownEntity" : "加载保存失败! 在保存的游戏中发现未知实体'%s'! 保存可能与当前安装的mod版本不兼容!",
 
+	"vcmi.dimensionDoor.seaToLandError" : "无法在陆地与海洋之间使用异次元之门传送。",
+
 	"vcmi.settingsMainWindow.generalTab.hover"   : "常规",
 	"vcmi.settingsMainWindow.generalTab.help"    : "切换到“常规”选项卡 - 配置客户端常规内容",
 	"vcmi.settingsMainWindow.battleTab.hover"    : "战斗",
@@ -279,7 +296,6 @@
 
 	"vcmi.optionsTab.turnOptions.hover" : "回合选项",
 	"vcmi.optionsTab.turnOptions.help" : "选择回合计时器并同步回合选项",
-	"vcmi.optionsTab.selectPreset" : "预设",
 
 	"vcmi.optionsTab.chessFieldBase.hover" : "基本计时器",
 	"vcmi.optionsTab.chessFieldTurn.hover" : "回合计时器",
@@ -371,6 +387,69 @@
 	"vcmi.stackExperience.rank.9" : "中校 10级",
 	"vcmi.stackExperience.rank.10" : "上校 11级",
 
+	// Strings for HotA Seer Hut / Quest Guards
+	"core.seerhut.quest.heroClass.complete.0" : "啊,你正是%s。这是给你的礼物,你接受吗?",
+	"core.seerhut.quest.heroClass.complete.1" : "啊,你正是%s。这是给你的礼物,你接受吗?",
+	"core.seerhut.quest.heroClass.complete.2" : "啊,你正是%s。这是给你的礼物,你接受吗?",
+	"core.seerhut.quest.heroClass.complete.3" : "守卫认出了你正是%s并允许你通过,你现在要通过吗?",
+	"core.seerhut.quest.heroClass.complete.4" : "守卫认出了你正是%s并允许你通过,你现在要通过吗?",
+	"core.seerhut.quest.heroClass.complete.5" : "守卫认出了你正是%s并允许你通过,你现在要通过吗?",
+	"core.seerhut.quest.heroClass.description.0" : "运送%s到%s",
+	"core.seerhut.quest.heroClass.description.1" : "运送%s到%s",
+	"core.seerhut.quest.heroClass.description.2" : "运送%s到%s",
+	"core.seerhut.quest.heroClass.description.3" : "送达%s来打开大门",
+	"core.seerhut.quest.heroClass.description.4" : "送达%s来打开大门",
+	"core.seerhut.quest.heroClass.description.5" : "送达%s来打开大门",
+	"core.seerhut.quest.heroClass.hover.0" : "(寻找%s类英雄)",
+	"core.seerhut.quest.heroClass.hover.1" : "(寻找%s类英雄)",
+	"core.seerhut.quest.heroClass.hover.2" : "(寻找%s类英雄)",
+	"core.seerhut.quest.heroClass.hover.3" : "(寻找%s类英雄)",
+	"core.seerhut.quest.heroClass.hover.4" : "(寻找%s类英雄)",
+	"core.seerhut.quest.heroClass.hover.5" : "(寻找%s类英雄)",
+	"core.seerhut.quest.heroClass.receive.0" : "我为%s准备了一个礼物。",
+	"core.seerhut.quest.heroClass.receive.1" : "我为%s准备了一个礼物。",
+	"core.seerhut.quest.heroClass.receive.2" : "我为%s准备了一个礼物。",
+	"core.seerhut.quest.heroClass.receive.3" : "这里的守卫说他们只允许%s通过。",
+	"core.seerhut.quest.heroClass.receive.4" : "这里的守卫说他们只允许%s通过。",
+	"core.seerhut.quest.heroClass.receive.5" : "这里的守卫说他们只允许%s通过。",
+	"core.seerhut.quest.heroClass.visit.0" : "你不是%s,我不会给你任何东西,快滚!",
+	"core.seerhut.quest.heroClass.visit.1" : "你不是%s,我不会给你任何东西,快滚!",
+	"core.seerhut.quest.heroClass.visit.2" : "你不是%s,我不会给你任何东西,快滚!",
+	"core.seerhut.quest.heroClass.visit.3" : "这里的守卫只允许%s通过。",
+	"core.seerhut.quest.heroClass.visit.4" : "这里的守卫只允许%s通过。",
+	"core.seerhut.quest.heroClass.visit.5" : "这里的守卫只允许%s通过。",
+
+	"core.seerhut.quest.reachDate.complete.0" : "我自由了!这是我送给你的,你接受吗?",
+	"core.seerhut.quest.reachDate.complete.1" : "我自由了!这是我送给你的,你接受吗?",
+	"core.seerhut.quest.reachDate.complete.2" : "我自由了!这是我送给你的,你接受吗?",
+	"core.seerhut.quest.reachDate.complete.3" : "你现在可以自由通行了。你想过去吗?",
+	"core.seerhut.quest.reachDate.complete.4" : "你现在可以自由通行了。你想过去吗?",
+	"core.seerhut.quest.reachDate.complete.5" : "你现在可以自由通行了。你想过去吗?",
+	"core.seerhut.quest.reachDate.description.0" : "请等待到%s/%s",
+	"core.seerhut.quest.reachDate.description.1" : "请等待到%s/%s",
+	"core.seerhut.quest.reachDate.description.2" : "请等待到%s/%s",
+	"core.seerhut.quest.reachDate.description.3" : "请等待到%s,大门将会打开",
+	"core.seerhut.quest.reachDate.description.4" : "请等待到%s,大门将会打开",
+	"core.seerhut.quest.reachDate.description.5" : "请等待到%s,大门将会打开",
+	"core.seerhut.quest.reachDate.hover.0" : "(在%s之后回来)",
+	"core.seerhut.quest.reachDate.hover.1" : "(在%s之后回来)",
+	"core.seerhut.quest.reachDate.hover.2" : "(在%s之后回来)",
+	"core.seerhut.quest.reachDate.hover.3" : "(在%s之后回来)",
+	"core.seerhut.quest.reachDate.hover.4" : "(在%s之后回来)",
+	"core.seerhut.quest.reachDate.hover.5" : "(在%s之后回来)",
+	"core.seerhut.quest.reachDate.receive.0" : "我很忙。请在%s之后再来。",
+	"core.seerhut.quest.reachDate.receive.1" : "我很忙。请在%s之后再来。",
+	"core.seerhut.quest.reachDate.receive.2" : "我很忙。请在%s之后再来。",
+	"core.seerhut.quest.reachDate.receive.3" : "关门直到%s。",
+	"core.seerhut.quest.reachDate.receive.4" : "关门直到%s。",
+	"core.seerhut.quest.reachDate.receive.5" : "关门直到%s。",
+	"core.seerhut.quest.reachDate.visit.0" : "我很忙。请在%s之后再来。",
+	"core.seerhut.quest.reachDate.visit.1" : "我很忙。请在%s之后再来。",
+	"core.seerhut.quest.reachDate.visit.2" : "我很忙。请在%s之后再来。",
+	"core.seerhut.quest.reachDate.visit.3" : "关门直到%s。",
+	"core.seerhut.quest.reachDate.visit.4" : "关门直到%s。",
+	"core.seerhut.quest.reachDate.visit.5" : "关门直到%s。",
+
 	"core.bonus.ADDITIONAL_ATTACK.name": "双击",
 	"core.bonus.ADDITIONAL_ATTACK.description": "生物可以攻击两次",
 	"core.bonus.ADDITIONAL_RETALIATION.name": "额外反击",

+ 0 - 1
Mods/vcmi/config/vcmi/czech.json

@@ -243,7 +243,6 @@
 
 	"vcmi.optionsTab.turnOptions.hover" : "Možnosti tahu",
 	"vcmi.optionsTab.turnOptions.help" : "Vyberte odpočítávadlo tahů a nastavení souběžných tahů",
-	"vcmi.optionsTab.selectPreset" : "Preset",
 
 	"vcmi.optionsTab.chessFieldBase.hover" : "Base timer",
 	"vcmi.optionsTab.chessFieldTurn.hover" : "Turn timer",

+ 83 - 2
Mods/vcmi/config/vcmi/english.json

@@ -99,6 +99,20 @@
 	"vcmi.lobby.room.description.limit" : "Up to %d players can enter your room, including you.",
 	"vcmi.lobby.invite.header" : "Invite Players",
 	"vcmi.lobby.invite.notification" : "Player has invited you to their game room. You can now join their private room.",
+	"vcmi.lobby.preview.title" : "Join Game Room",
+	"vcmi.lobby.preview.subtitle" : "Game on %s, hosted by %s", //TL Note: 1) name of map or RMG template 2) nickname of game host
+	"vcmi.lobby.preview.version" : "Game version:",
+	"vcmi.lobby.preview.players" : "Players:",
+	"vcmi.lobby.preview.mods" : "Used mods:",
+	"vcmi.lobby.preview.title" : "Join Game Room",
+	"vcmi.lobby.preview.allowed" : "Join the game room?",
+	"vcmi.lobby.preview.error.header" : "Unable to join this room.",
+	"vcmi.lobby.preview.error.playing" : "You need to leave your current game first.",
+	"vcmi.lobby.preview.error.full" : "The room is already full.",
+	"vcmi.lobby.preview.error.busy" : "The room no longer accepts new players.",
+	"vcmi.lobby.preview.error.invite" : "You were not invited to this room.",
+	"vcmi.lobby.preview.error.mods" : "You are using different set of mods.",
+	"vcmi.lobby.preview.error.version" : "You are using different version of VCMI.",
 	"vcmi.lobby.room.new" : "New Game",
 	"vcmi.lobby.room.load" : "Load Game",
 	"vcmi.lobby.room.type" : "Room Type",
@@ -107,6 +121,11 @@
 	"vcmi.lobby.room.state.private" : "Private",
 	"vcmi.lobby.room.state.busy" : "In Game",
 	"vcmi.lobby.room.state.invited" : "Invited",
+	"vcmi.lobby.mod.state.compatible" : "Compatible",
+	"vcmi.lobby.mod.state.disabled" : "Must be enabled",
+	"vcmi.lobby.mod.state.version" : "Version mismatch",
+	"vcmi.lobby.mod.state.excessive" : "Must be disabled",
+	"vcmi.lobby.mod.state.missing" : "Not installed",
 
 	"vcmi.client.errors.invalidMap" : "{Invalid map or campaign}\n\nFailed to start game! Selected map or campaign might be invalid or corrupted. Reason:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{Missing data files}\n\nCampaigns data files were not found! You may be using incomplete or corrupted Heroes 3 data files. Please reinstall game data.",
@@ -233,7 +252,7 @@
 	"vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s were killed by accurate shots!",
 	"vcmi.battleWindow.endWithAutocombat" : "Are you sure you wish to end the battle with auto combat?",
 
-	"vcmi.battleResultsWindow.applyResultsLabel" : "Apply battle result",
+	"vcmi.battleResultsWindow.applyResultsLabel" : "Accept battle result?",
 
 	"vcmi.tutorialWindow.title" : "Touchscreen Introduction",
 	"vcmi.tutorialWindow.decription.RightClick" : "Touch and hold the element on which you want to right-click. Touch the free area to close.",
@@ -296,7 +315,6 @@
 
 	"vcmi.optionsTab.turnOptions.hover" : "Turn Options",
 	"vcmi.optionsTab.turnOptions.help" : "Select turn timer and simultaneous turns options",
-	"vcmi.optionsTab.selectPreset" : "Preset",
 
 	"vcmi.optionsTab.chessFieldBase.hover" : "Base timer",
 	"vcmi.optionsTab.chessFieldTurn.hover" : "Turn timer",
@@ -387,6 +405,69 @@
 	"vcmi.stackExperience.rank.8" : "Elite",
 	"vcmi.stackExperience.rank.9" : "Master",
 	"vcmi.stackExperience.rank.10" : "Ace",
+	
+	// Strings for HotA Seer Hut / Quest Guards
+	"core.seerhut.quest.heroClass.complete.0" : "Ah, you are %s.  Here's a gift for you.  Do you accept?",
+	"core.seerhut.quest.heroClass.complete.1" : "Ah, you are %s.  Here's a gift for you.  Do you accept?",
+	"core.seerhut.quest.heroClass.complete.2" : "Ah, you are %s.  Here's a gift for you.  Do you accept?",
+	"core.seerhut.quest.heroClass.complete.3" : "The guards note that you are %s and offer to let you pass.  Do you accept?",
+	"core.seerhut.quest.heroClass.complete.4" : "The guards note that you are %s and offer to let you pass.  Do you accept?",
+	"core.seerhut.quest.heroClass.complete.5" : "The guards note that you are %s and offer to let you pass.  Do you accept?",
+	"core.seerhut.quest.heroClass.description.0" : "Send %s to %s",
+	"core.seerhut.quest.heroClass.description.1" : "Send %s to %s",
+	"core.seerhut.quest.heroClass.description.2" : "Send %s to %s",
+	"core.seerhut.quest.heroClass.description.3" : "Send %s to open gate",
+	"core.seerhut.quest.heroClass.description.4" : "Send %s to open gate",
+	"core.seerhut.quest.heroClass.description.5" : "Send %s to open gate",
+	"core.seerhut.quest.heroClass.hover.0" : "(seeks hero of %s class)",
+	"core.seerhut.quest.heroClass.hover.1" : "(seeks hero of %s class)",
+	"core.seerhut.quest.heroClass.hover.2" : "(seeks hero of %s class)",
+	"core.seerhut.quest.heroClass.hover.3" : "(seeks hero of %s class)",
+	"core.seerhut.quest.heroClass.hover.4" : "(seeks hero of %s class)",
+	"core.seerhut.quest.heroClass.hover.5" : "(seeks hero of %s class)",
+	"core.seerhut.quest.heroClass.receive.0" : "I've got a gift for %s.",
+	"core.seerhut.quest.heroClass.receive.1" : "I've got a gift for %s.",
+	"core.seerhut.quest.heroClass.receive.2" : "I've got a gift for %s.",
+	"core.seerhut.quest.heroClass.receive.3" : "The guards here say they will only let %s pass.",
+	"core.seerhut.quest.heroClass.receive.4" : "The guards here say they will only let %s pass.",
+	"core.seerhut.quest.heroClass.receive.5" : "The guards here say they will only let %s pass.",
+	"core.seerhut.quest.heroClass.visit.0" : "You are not %s.  I've got nothing for you. Begone!",
+	"core.seerhut.quest.heroClass.visit.1" : "You are not %s.  I've got nothing for you. Begone!",
+	"core.seerhut.quest.heroClass.visit.2" : "You are not %s.  I've got nothing for you. Begone!",
+	"core.seerhut.quest.heroClass.visit.3" : "The guards here will only let %s pass.",
+	"core.seerhut.quest.heroClass.visit.4" : "The guards here will only let %s pass.",
+	"core.seerhut.quest.heroClass.visit.5" : "The guards here will only let %s pass.",
+	
+	"core.seerhut.quest.reachDate.complete.0" : "I'm free now.  Here's what I've got for you.  Do you accept?",
+	"core.seerhut.quest.reachDate.complete.1" : "I'm free now.  Here's what I've got for you.  Do you accept?",
+	"core.seerhut.quest.reachDate.complete.2" : "I'm free now.  Here's what I've got for you.  Do you accept?",
+	"core.seerhut.quest.reachDate.complete.3" : "You are free to go through now.  Do you wish to pass?",
+	"core.seerhut.quest.reachDate.complete.4" : "You are free to go through now.  Do you wish to pass?",
+	"core.seerhut.quest.reachDate.complete.5" : "You are free to go through now.  Do you wish to pass?",
+	"core.seerhut.quest.reachDate.description.0" : "Wait till %s for %s",
+	"core.seerhut.quest.reachDate.description.1" : "Wait till %s for %s",
+	"core.seerhut.quest.reachDate.description.2" : "Wait till %s for %s",
+	"core.seerhut.quest.reachDate.description.3" : "Wait till %s to open gate",
+	"core.seerhut.quest.reachDate.description.4" : "Wait till %s to open gate",
+	"core.seerhut.quest.reachDate.description.5" : "Wait till %s to open gate",
+	"core.seerhut.quest.reachDate.hover.0" : "(Return not before %s)",
+	"core.seerhut.quest.reachDate.hover.1" : "(Return not before %s)",
+	"core.seerhut.quest.reachDate.hover.2" : "(Return not before %s)",
+	"core.seerhut.quest.reachDate.hover.3" : "(Return not before %s)",
+	"core.seerhut.quest.reachDate.hover.4" : "(Return not before %s)",
+	"core.seerhut.quest.reachDate.hover.5" : "(Return not before %s)",
+	"core.seerhut.quest.reachDate.receive.0" : "I'm busy.  Come back not before %s",
+	"core.seerhut.quest.reachDate.receive.1" : "I'm busy.  Come back not before %s",
+	"core.seerhut.quest.reachDate.receive.2" : "I'm busy.  Come back not before %s",
+	"core.seerhut.quest.reachDate.receive.3" : "Closed till %s.",
+	"core.seerhut.quest.reachDate.receive.4" : "Closed till %s.",
+	"core.seerhut.quest.reachDate.receive.5" : "Closed till %s.",
+	"core.seerhut.quest.reachDate.visit.0" : "I'm busy.  Come back not before %s.",
+	"core.seerhut.quest.reachDate.visit.1" : "I'm busy.  Come back not before %s.",
+	"core.seerhut.quest.reachDate.visit.2" : "I'm busy.  Come back not before %s.",
+	"core.seerhut.quest.reachDate.visit.3" : "Closed till %s.",
+	"core.seerhut.quest.reachDate.visit.4" : "Closed till %s.",
+	"core.seerhut.quest.reachDate.visit.5" : "Closed till %s.",
 
 	"core.bonus.ADDITIONAL_ATTACK.name": "Double Strike",
 	"core.bonus.ADDITIONAL_ATTACK.description": "Attacks twice",

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

@@ -258,7 +258,6 @@
 
 	"vcmi.optionsTab.turnOptions.hover" : "Spielzug-Optionen",
 	"vcmi.optionsTab.turnOptions.help" : "Optionen zu Spielzug-Timer und simultanen Zügen",
-	"vcmi.optionsTab.selectPreset" : "Voreinstellung",
 
 	"vcmi.optionsTab.chessFieldBase.hover" : "Basis-Timer",
 	"vcmi.optionsTab.chessFieldTurn.hover" : "Spielzug-Timer",

+ 0 - 1
Mods/vcmi/config/vcmi/polish.json

@@ -250,7 +250,6 @@
 
 	"vcmi.optionsTab.turnOptions.hover" : "Ustawienia tur",
 	"vcmi.optionsTab.turnOptions.help" : "Ustaw limity czasu oraz tury symultaniczne",
-	"vcmi.optionsTab.selectPreset" : "Szablonowe ustawienie",
 
 	"vcmi.optionsTab.chessFieldBase.hover" : "Zegar startowy",
 	"vcmi.optionsTab.chessFieldTurn.hover" : "Zegar tury",

+ 85 - 21
Mods/vcmi/config/vcmi/portuguese.json

@@ -87,7 +87,7 @@
 	"vcmi.lobby.header.chat.player" : "Bate-papo privado com %s", // %s -> apelido de outro jogador
 	"vcmi.lobby.header.history" : "Seus Jogos Anteriores",
 	"vcmi.lobby.header.players" : "Jogadores Online - %d",
-	"vcmi.lobby.match.solo" : "Jogo para um jogador",
+	"vcmi.lobby.match.solo" : "Jogo para um Jogador",
 	"vcmi.lobby.match.duel" : "Jogo com %s", // %s -> apelido de outro jogador
 	"vcmi.lobby.match.multi" : "%d jogadores",
 	"vcmi.lobby.room.create" : "Criar Nova Sala",
@@ -117,6 +117,8 @@
 	"vcmi.server.errors.modNoDependency" : "Falha ao carregar mod {'%s'}!\n Ele depende do mod {'%s'} que não está ativo!\n",
 	"vcmi.server.errors.modConflict" : "Falha ao carregar mod {'%s'}!\n Conflita com o mod ativo {'%s'}!\n",
 	"vcmi.server.errors.unknownEntity" : "Falha ao carregar salvamento! Entidade desconhecida '%s' encontrada no jogo salvo! O salvamento pode não ser compatível com a versão atualmente instalada dos mods!",
+	
+	"vcmi.dimensionDoor.seaToLandError" : "Não é possível teleportar do mar para a terra ou vice-versa com uma Porta Dimensional.",
 
 	"vcmi.settingsMainWindow.generalTab.hover" : "Geral",
 	"vcmi.settingsMainWindow.generalTab.help"     : "Muda para a aba de Opções Gerais, que contém configurações relacionadas ao comportamento geral do cliente do jogo.",
@@ -231,7 +233,7 @@
 	"vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s morreram por tiros precisos!",
 	"vcmi.battleWindow.endWithAutocombat" : "Tem certeza de que deseja terminar a batalha com o combate automático?",
 
-	"vcmi.battleResultsWindow.applyResultsLabel" : "Aplicar resultado da batalha",
+	"vcmi.battleResultsWindow.applyResultsLabel" : "Aplicar resultado da batalha?",
 
 	"vcmi.tutorialWindow.title" : "Introdução à Tela Sensível ao Toque",
 	"vcmi.tutorialWindow.decription.RightClick" : "Toque e mantenha pressionado o elemento sobre o qual deseja clicar com o botão direito. Toque na área livre para fechar.",
@@ -255,7 +257,7 @@
 	"vcmi.townHall.greetingKnowledge"	: "Estudando os glifos de %s, você adquire uma visão dos segredos sobre o funcionamento de várias magias (+1 de Conhecimento).",
 	"vcmi.townHall.greetingSpellPower"	: "%s ensina novas maneiras de concentrar seus poderes mágicos (+1 de Força).",
 	"vcmi.townHall.greetingExperience"	: "Uma visita em %s ensina muitas habilidades novas (+1000 de Experiência).",
-	"vcmi.townHall.greetingAttack"		: "Algum tempo passado em %s permite que você aprenda habilidades de combate mais eficazes (+1 de Habilidade de Ataque).",
+	"vcmi.townHall.greetingAttack"		: "Algum tempo passado em %s permite que você aprenda habilidades de combate mais eficazes (+1 de Ataque).",
 	"vcmi.townHall.greetingDefence"		: "Ao passar um tempo em %s, os guerreiros experientes lá dentro te ensinam habilidades defensivas adicionais (+1 de Defesa).",
 	"vcmi.townHall.hasNotProduced"		: "%s ainda não produziu nada.",
 	"vcmi.townHall.hasProduced"		: "%s produziu %d %s nesta semana.",
@@ -294,12 +296,11 @@
 
 	"vcmi.optionsTab.turnOptions.hover" : "Opções de Turno",
 	"vcmi.optionsTab.turnOptions.help" : "Selecione as opções de cronômetro do turno e turnos simultâneos",
-	"vcmi.optionsTab.selectPreset" : "Predefinição",
 
 	"vcmi.optionsTab.chessFieldBase.hover" : "Cronômetro Base",
-	"vcmi.optionsTab.chessFieldTurn.hover" : "Cronômetro do Turno",
+	"vcmi.optionsTab.chessFieldTurn.hover" : "Cronôm. Turno",
 	"vcmi.optionsTab.chessFieldBattle.hover" : "Cronômetro da Batalha",
-	"vcmi.optionsTab.chessFieldUnit.hover" : "Cronômetro da Unidade",
+	"vcmi.optionsTab.chessFieldUnit.hover" : "Cronôm. Unid.",
 	"vcmi.optionsTab.chessFieldBase.help" : "Usado quando o {Cronômetro do Turno} chega a 0. Definido uma vez no início do jogo. Ao atingir zero, encerra o turno atual. Qualquer combate em curso terminará com uma derrota.",
 	"vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Usado fora de combate ou quando o {Cronômetro da Batalha} se esgota. Restaurado a cada turno. O tempo restante é adicionado ao {Tempo Base} no final do turno.",
 	"vcmi.optionsTab.chessFieldTurnDiscard.help" : "Usado fora de combate ou quando o {Cronômetro da Batalha} se esgota. Restaurado a cada turno. Qualquer tempo não utilizado é perdido.",
@@ -373,7 +374,7 @@
 	"vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf" : "Infelizmente, você perdeu parte da Aliança Angelical. Tudo está perdido.",
 
 	// few strings from WoG used by vcmi
-	"vcmi.stackExperience.description" : "» D e t a l h e s   d e   E x p e r i ê n c i a   d o   G r u p o «\n\nTipo de Criatura ................... : %s\nRank de Experiência ................. : %s (%i)\nPontos de Experiência ............... : %i\nPontos de Experiência até o Próximo Ranque .. : %i\nMáximo de Experiência por Batalha ... : %i%% (%i)\nNúmero de Criaturas no grupo .... : %i\nNovos Recrutas Máximos\n sem perder o Ranque atual .... : %i\nMultiplicador de Experiência ........... : %.2f\nMultiplicador de Upgrade .............. : %.2f\nExperiência após o Ranque 10 ........ : %i\nNovos Recrutas Máximos para permanecer\n no Ranque 10 se na Experiência Máxima : %i",
+	"vcmi.stackExperience.description" : "» D e t a l h e s   d e   E x p e r i ê n c i a   d o   G r u p o «\n\nTipo de Criatura ................ : %s\nRanque de Experiência ........... : %s (%i)\nPontos de Experiência ........... : %i\nPontos de Experiência até o\nPróximo Ranque .................. : %i\nExperiência Máxima por Batalha .. : %i%% (%i)\nNúmero de Criaturas no grupo .... : %i\nMáximo de Novos Recrutas sem\nperder o Ranque atual .......... : %i\nMultiplicador de Experiência .... : %.2f\nMultiplicador de Atualização .... : %.2f\nExperiência após o Ranque 10 .... : %i\nMáximos de Novos Recrutas para\npermanecer no Ranque 10 se\nestiver na Experiência Máxima .. : %i",
 	"vcmi.stackExperience.rank.0" : "Básico",
 	"vcmi.stackExperience.rank.1" : "Novato",
 	"vcmi.stackExperience.rank.2" : "Treinado",
@@ -385,6 +386,69 @@
 	"vcmi.stackExperience.rank.8" : "Elite",
 	"vcmi.stackExperience.rank.9" : "Mestre",
 	"vcmi.stackExperience.rank.10" : "Ás",
+	
+	// Strings for HotA Seer Hut / Quest Guards
+	"core.seerhut.quest.heroClass.complete.0" : "Ah, você é %s. Aqui está um presente para você. Aceita?",
+	"core.seerhut.quest.heroClass.complete.1" : "Ah, você é %s. Aqui está um presente para você. Aceita?",
+	"core.seerhut.quest.heroClass.complete.2" : "Ah, você é %s. Aqui está um presente para você. Aceita?",
+	"core.seerhut.quest.heroClass.complete.3" : "Os guardas notam que você é %s e oferecem deixá-lo passar. Aceita?",
+	"core.seerhut.quest.heroClass.complete.4" : "Os guardas notam que você é %s e oferecem deixá-lo passar. Aceita?",
+	"core.seerhut.quest.heroClass.complete.5" : "Os guardas notam que você é %s e oferecem deixá-lo passar. Aceita?",
+	"core.seerhut.quest.heroClass.description.0" : "Envie %s para %s",
+	"core.seerhut.quest.heroClass.description.1" : "Envie %s para %s",
+	"core.seerhut.quest.heroClass.description.2" : "Envie %s para %s",
+	"core.seerhut.quest.heroClass.description.3" : "Envie %s para abrir o portão",
+	"core.seerhut.quest.heroClass.description.4" : "Envie %s para abrir o portão",
+	"core.seerhut.quest.heroClass.description.5" : "Envie %s para abrir o portão",
+	"core.seerhut.quest.heroClass.hover.0" : "(procure por herói da classe %s)",
+	"core.seerhut.quest.heroClass.hover.1" : "(procure por herói da classe %s)",
+	"core.seerhut.quest.heroClass.hover.2" : "(procure por herói da classe %s)",
+	"core.seerhut.quest.heroClass.hover.3" : "(procure por herói da classe %s)",
+	"core.seerhut.quest.heroClass.hover.4" : "(procure por herói da classe %s)",
+	"core.seerhut.quest.heroClass.hover.5" : "(procure por herói da classe %s)",
+	"core.seerhut.quest.heroClass.receive.0" : "Tenho um presente para %s.",
+	"core.seerhut.quest.heroClass.receive.1" : "Tenho um presente para %s.",
+	"core.seerhut.quest.heroClass.receive.2" : "Tenho um presente para %s.",
+	"core.seerhut.quest.heroClass.receive.3" : "Os guardas aqui dizem que só deixarão %s passar.",
+	"core.seerhut.quest.heroClass.receive.4" : "Os guardas aqui dizem que só deixarão %s passar.",
+	"core.seerhut.quest.heroClass.receive.5" : "Os guardas aqui dizem que só deixarão %s passar.",
+	"core.seerhut.quest.heroClass.visit.0" : "Você não é %s. Não tenho nada para você. Vá embora!",
+	"core.seerhut.quest.heroClass.visit.1" : "Você não é %s. Não tenho nada para você. Vá embora!",
+	"core.seerhut.quest.heroClass.visit.2" : "Você não é %s. Não tenho nada para você. Vá embora!",
+	"core.seerhut.quest.heroClass.visit.3" : "Os guardas aqui só deixarão passar %s.",
+	"core.seerhut.quest.heroClass.visit.4" : "Os guardas aqui só deixarão passar %s.",
+	"core.seerhut.quest.heroClass.visit.5" : "Os guardas aqui só deixarão passar %s.",
+	
+	"core.seerhut.quest.reachDate.complete.0" : "Estou livre agora. Aqui está o que tenho para você. Aceita?",
+	"core.seerhut.quest.reachDate.complete.1" : "Estou livre agora. Aqui está o que tenho para você. Aceita?",
+	"core.seerhut.quest.reachDate.complete.2" : "Estou livre agora. Aqui está o que tenho para você. Aceita?",
+	"core.seerhut.quest.reachDate.complete.3" : "Agora você está livre para passar. Deseja passar?",
+	"core.seerhut.quest.reachDate.complete.4" : "Agora você está livre para passar. Deseja passar?",
+	"core.seerhut.quest.reachDate.complete.5" : "Agora você está livre para passar. Deseja passar?",
+	"core.seerhut.quest.reachDate.description.0" : "Espere até %s para %s",
+	"core.seerhut.quest.reachDate.description.1" : "Espere até %s para %s",
+	"core.seerhut.quest.reachDate.description.2" : "Espere até %s para %s",
+	"core.seerhut.quest.reachDate.description.3" : "Espere até %s para abrir o portão",
+	"core.seerhut.quest.reachDate.description.4" : "Espere até %s para abrir o portão",
+	"core.seerhut.quest.reachDate.description.5" : "Espere até %s para abrir o portão",
+	"core.seerhut.quest.reachDate.hover.0" : "(Não retorne antes de %s)",
+	"core.seerhut.quest.reachDate.hover.1" : "(Não retorne antes de %s)",
+	"core.seerhut.quest.reachDate.hover.2" : "(Não retorne antes de %s)",
+	"core.seerhut.quest.reachDate.hover.3" : "(Não retorne antes de %s)",
+	"core.seerhut.quest.reachDate.hover.4" : "(Não retorne antes de %s)",
+	"core.seerhut.quest.reachDate.hover.5" : "(Não retorne antes de %s)",
+	"core.seerhut.quest.reachDate.receive.0" : "Estou ocupado. Não volte antes de %s",
+	"core.seerhut.quest.reachDate.receive.1" : "Estou ocupado. Não volte antes de %s",
+	"core.seerhut.quest.reachDate.receive.2" : "Estou ocupado. Não volte antes de %s",
+	"core.seerhut.quest.reachDate.receive.3" : "Fechado até %s.",
+	"core.seerhut.quest.reachDate.receive.4" : "Fechado até %s.",
+	"core.seerhut.quest.reachDate.receive.5" : "Fechado até %s.",
+	"core.seerhut.quest.reachDate.visit.0" : "Estou ocupado. Não volte antes de %s.",
+	"core.seerhut.quest.reachDate.visit.1" : "Estou ocupado. Não volte antes de %s.",
+	"core.seerhut.quest.reachDate.visit.2" : "Estou ocupado. Não volte antes de %s.",
+	"core.seerhut.quest.reachDate.visit.3" : "Fechado até %s.",
+	"core.seerhut.quest.reachDate.visit.4" : "Fechado até %s.",
+	"core.seerhut.quest.reachDate.visit.5" : "Fechado até %s.",
 
 	"core.bonus.ADDITIONAL_ATTACK.name" : "Ataque Duplo",
 	"core.bonus.ADDITIONAL_ATTACK.description" : "Ataca duas vezes",
@@ -441,14 +505,14 @@
 	"core.bonus.FEROCITY.name" : "Ferocidade",
 	"core.bonus.FEROCITY.description" : "Ataca ${val} vezes adicionais se matar alguém",
 	"core.bonus.FLYING.name" : "Voo",
-	"core.bonus.FLYING.description" : "Voar ao se mover (ignora obstáculos)",
+	"core.bonus.FLYING.description" : "Voa ao se mover (ignora obstáculos)",
 	"core.bonus.FREE_SHOOTING.name" : "Tiro Livre",
 	"core.bonus.FREE_SHOOTING.description" : "Pode usar ataques à distância em combate corpo a corpo",
 	"core.bonus.GARGOYLE.name" : "Gárgula",
 	"core.bonus.GARGOYLE.description" : "Não pode ser levantado ou curado",
 	"core.bonus.GENERAL_DAMAGE_REDUCTION.name" : "Redução de Dano (${val}%)",
 	"core.bonus.GENERAL_DAMAGE_REDUCTION.description" : "Reduz o dano físico de ataques à distância ou corpo a corpo",
-	"core.bonus.HATE.name" : "Odioso contra ${subtype.creature}",
+	"core.bonus.HATE.name" : "Odeia ${subtype.creature}",
 	"core.bonus.HATE.description" : "Causa ${val}% a mais de dano a ${subtype.creature}",
 	"core.bonus.HEALER.name" : "Curandeiro",
 	"core.bonus.HEALER.description" : "Cura unidades aliadas",
@@ -458,8 +522,8 @@
 	"core.bonus.JOUSTING.description" : "+${val}% de dano para cada hexágono percorrido",
 	"core.bonus.KING.name" : "Rei",
 	"core.bonus.KING.description" : "Vulnerável ao nível MATADOR ${val} ou superior",
-	"core.bonus.LEVEL_SPELL_IMMUNITY.name" : "Imunidade a Feitiços 1-${val}",
-	"core.bonus.LEVEL_SPELL_IMMUNITY.description" : "Imune a feitiços dos níveis 1-${val}",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.name" : "Imune a Feitiços 1-${val}",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.description" : "Imunidade a feitiços dos níveis 1-${val}",
 	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Alcance de Tiro Limitado",
 	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Incapaz de mirar unidades a uma distância maior que ${val} hexágonos",
 	"core.bonus.LIFE_DRAIN.name" : "Drenar Vida (${val}%)",
@@ -472,22 +536,22 @@
 	"core.bonus.MAGIC_MIRROR.description" : "Tem ${val}% de chance de redirecionar um feitiço ofensivo para uma unidade inimiga",
 	"core.bonus.MAGIC_RESISTANCE.name" : "Resistência Mágica (${val}%)",
 	"core.bonus.MAGIC_RESISTANCE.description" : "Tem ${val}% de chance de resistir a um feitiço inimigo",
-	"core.bonus.MIND_IMMUNITY.name" : "Imunidade a Feitiços Mentais",
-	"core.bonus.MIND_IMMUNITY.description" : "Imune a feitiços do tipo Mental",
-	"core.bonus.NO_DISTANCE_PENALTY.name" : "Sem Penalidade de Distância",
+	"core.bonus.MIND_IMMUNITY.name" : "Imune a Feitiços Mentais",
+	"core.bonus.MIND_IMMUNITY.description" : "Imunidade a feitiços do tipo Mental",
+	"core.bonus.NO_DISTANCE_PENALTY.name" : "Sem Penal. à Distância",
 	"core.bonus.NO_DISTANCE_PENALTY.description" : "Causa dano total a qualquer distância",
-	"core.bonus.NO_MELEE_PENALTY.name" : "Sem Penalidade em Combate Corpo a Corpo",
-	"core.bonus.NO_MELEE_PENALTY.description" : "A criatura não tem Penalidade em Combate Corpo a Corpo",
+	"core.bonus.NO_MELEE_PENALTY.name" : "Sem Penal. em Comb.",
+	"core.bonus.NO_MELEE_PENALTY.description" : "Sem penalidade no corpo a corpo",
 	"core.bonus.NO_MORALE.name" : "Moral Neutra",
 	"core.bonus.NO_MORALE.description" : "A criatura é imune aos efeitos de moral",
-	"core.bonus.NO_WALL_PENALTY.name" : "Sem Penalidade de Muralha",
-	"core.bonus.NO_WALL_PENALTY.description" : "Dano total durante cerco",
+	"core.bonus.NO_WALL_PENALTY.name" : "Sem Penal. por Muralha",
+	"core.bonus.NO_WALL_PENALTY.description" : "Causa dano total\ndurante cerco",
 	"core.bonus.NON_LIVING.name" : "Não Vivo",
 	"core.bonus.NON_LIVING.description" : "Imune a muitos efeitos",
 	"core.bonus.RANDOM_SPELLCASTER.name" : "Lançador de Feitiços Aleatório",
 	"core.bonus.RANDOM_SPELLCASTER.description" : "Pode lançar um feitiço aleatório",
 	"core.bonus.RANGED_RETALIATION.name" : "Contra-ataques à Distância",
-	"core.bonus.RANGED_RETALIATION.description" : "Pode realizar contra-ataques à distância",
+	"core.bonus.RANGED_RETALIATION.description" : "Realiza contra-ataques à distância",
 	"core.bonus.RECEPTIVE.name" : "Receptivo",
 	"core.bonus.RECEPTIVE.description" : "Sem Imunidade a Feitiços Amigáveis",
 	"core.bonus.REBIRTH.name" : "Renascimento (${val}%)",
@@ -496,7 +560,7 @@
 	"core.bonus.RETURN_AFTER_STRIKE.description" : "Volta após o ataque corpo a corpo",
 	"core.bonus.REVENGE.name" : "Vingança",
 	"core.bonus.REVENGE.description" : "Causa dano extra com base na saúde perdida do atacante em batalha",
-	"core.bonus.SHOOTER.name" : "À Distância",
+	"core.bonus.SHOOTER.name" : "Longo Alcance",
 	"core.bonus.SHOOTER.description" : "A criatura pode atirar",
 	"core.bonus.SHOOTS_ALL_ADJACENT.name" : "Atirar em Tudo ao Redor",
 	"core.bonus.SHOOTS_ALL_ADJACENT.description" : "Os ataques à distância desta criatura atingem todos os alvos em uma pequena área",
@@ -521,7 +585,7 @@
 	"core.bonus.SYNERGY_TARGET.name" : "Alvo Sinergizável",
 	"core.bonus.SYNERGY_TARGET.description" : "Esta criatura é vulnerável ao efeito de sinergia",
 	"core.bonus.TWO_HEX_ATTACK_BREATH.name" : "Sopro",
-	"core.bonus.TWO_HEX_ATTACK_BREATH.description" : "Ataque de Sopro (alcance de 2 hexágonos)",
+	"core.bonus.TWO_HEX_ATTACK_BREATH.description" : "Ataque de Sopro (alcança 2 hexágonos)",
 	"core.bonus.THREE_HEADED_ATTACK.name" : "Ataque das Três Cabeças",
 	"core.bonus.THREE_HEADED_ATTACK.description" : "Ataca três unidades adjacentes",
 	"core.bonus.TRANSMUTATION.name" : "Transmutação",

+ 0 - 1
Mods/vcmi/config/vcmi/spanish.json

@@ -259,7 +259,6 @@
 
 	"vcmi.optionsTab.turnOptions.hover" : "Opciones de turno",
 	"vcmi.optionsTab.turnOptions.help" : "Seleccionar temporizador de turno y opciones de turnos simultáneos",
-	"vcmi.optionsTab.selectPreset" : "Preconfigurado",
 
 	"vcmi.optionsTab.chessFieldBase.hover" : "Cronómetro base",
 	"vcmi.optionsTab.chessFieldTurn.hover" : "Cronómetro de turno",

+ 35 - 6
Mods/vcmi/config/vcmi/ukrainian.json

@@ -4,11 +4,11 @@
 	"vcmi.adventureMap.monsterThreat.levels.1"  : "Дуже слабкий",
 	"vcmi.adventureMap.monsterThreat.levels.2"  : "Слабкий",
 	"vcmi.adventureMap.monsterThreat.levels.3"  : "Трохи слабша",
-	"vcmi.adventureMap.monsterThreat.levels.4"  : "Відповідна",
+	"vcmi.adventureMap.monsterThreat.levels.4"  : "Рівна",
 	"vcmi.adventureMap.monsterThreat.levels.5"  : "Трохи сильніша",
 	"vcmi.adventureMap.monsterThreat.levels.6"  : "Сильніша",
 	"vcmi.adventureMap.monsterThreat.levels.7"  : "Дуже сильна",
-	"vcmi.adventureMap.monsterThreat.levels.8"  : "Кидає виклик",
+	"vcmi.adventureMap.monsterThreat.levels.8"  : "Надзвичайно сильна",
 	"vcmi.adventureMap.monsterThreat.levels.9"  : "Нездоланна",
 	"vcmi.adventureMap.monsterThreat.levels.10" : "Смертельна",
 	"vcmi.adventureMap.monsterThreat.levels.11" : "Неможлива",
@@ -20,6 +20,7 @@
 	"vcmi.adventureMap.playerAttacked"      : "Гравця атаковано: %s",
 	"vcmi.adventureMap.moveCostDetails" : "Очки руху - Вартість: %TURNS ходів + %POINTS очок. Залишок очок: %REMAINING",
 	"vcmi.adventureMap.moveCostDetailsNoTurns" : "Очки руху - Вартість: %POINTS очок, Залишок очок: %REMAINING",
+	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Вибачте, функція повтору ходу суперника ще не реалізована!",
 
 	"vcmi.capitalColors.0" : "Червоний",
 	"vcmi.capitalColors.1" : "Синій",
@@ -62,7 +63,6 @@
 	"vcmi.mainMenu.serverClosing" : "Завершення...",
 	"vcmi.mainMenu.hostTCP" : "Створити TCP/IP гру",
 	"vcmi.mainMenu.joinTCP" : "Приєднатися до TCP/IP гри",
-	"vcmi.mainMenu.playerName" : "Гравець",
 	
 	"vcmi.lobby.filepath" : "Назва файлу",
 	"vcmi.lobby.creationDate" : "Дата створення",
@@ -71,6 +71,7 @@
 	"vcmi.lobby.noPreview" : "огляд недоступний",
 	"vcmi.lobby.noUnderground" : "немає підземелля",
 	"vcmi.lobby.sortDate" : "Сортувати мапи за датою зміни",
+	"vcmi.lobby.backToLobby" : "Назад до лобі",
 
 	"vcmi.lobby.login.title" : "Онлайн лобі VCMI",
 	"vcmi.lobby.login.username" : "Логін:",
@@ -96,6 +97,22 @@
 	"vcmi.lobby.room.description.new" : "Щоб почати гру, виберіть сценарій або налаштуйте випадкову карту.",
 	"vcmi.lobby.room.description.load" : "Щоб почати гру, виберіть одну з ваших збережених ігор.",
 	"vcmi.lobby.room.description.limit" : "До %d гравців можуть зайти у вашу кімнату, включаючи вас.",
+	"vcmi.lobby.invite.header" : "Запросити гравців",
+	"vcmi.lobby.invite.notification" : "Гравець запросив вас до своєї ігрової кімнати. Тепер ви можете приєднатися до його приватної кімнати.",
+	"vcmi.lobby.preview.title" : "Join Game Room",
+	"vcmi.lobby.preview.subtitle" : "Гра на %s, яку проводить %s", //TL Note: 1) name of map or RMG template 2) nickname of game host
+	"vcmi.lobby.preview.version" : "Версія гри:",
+	"vcmi.lobby.preview.players" : "Гравці:",
+	"vcmi.lobby.preview.mods" : "Активні модифікації:",
+	"vcmi.lobby.preview.title" : "Приєднатися до кімнати",
+	"vcmi.lobby.preview.allowed" : "Приєднатися до цієї ігрової кімнати?",
+	"vcmi.lobby.preview.error.header" : "Неможливо приєднатися до цієї кімнати.",
+	"vcmi.lobby.preview.error.playing" : "Ви повинні спочатку вийти з поточної гри.",
+	"vcmi.lobby.preview.error.full" : "Ця кімната вже повна.",
+	"vcmi.lobby.preview.error.busy" : "Кімната більше не приймає нових гравців.",
+	"vcmi.lobby.preview.error.invite" : "Ви не були запрошені до цієї кімнати.",
+	"vcmi.lobby.preview.error.mods" : "Ви використовуєте інший набір модифікацій.",
+	"vcmi.lobby.preview.error.version" : "Ви використовуєте іншу версію VCMI.",
 	"vcmi.lobby.room.new" : "Нова гра",
 	"vcmi.lobby.room.load" : "Завантажити гру",
 	"vcmi.lobby.room.type" : "Тип кімнати",
@@ -103,7 +120,14 @@
 	"vcmi.lobby.room.state.public" : "Публічна",
 	"vcmi.lobby.room.state.private" : "Приватна",
 	"vcmi.lobby.room.state.busy" : "У грі",
-
+	"vcmi.lobby.room.state.invited" : "Запрошено",
+	"vcmi.lobby.mod.state.compatible" : "Сумісна",
+	"vcmi.lobby.mod.state.disabled" : "Має бути увімкнена",
+	"vcmi.lobby.mod.state.version" : "Розбіжність версій",
+	"vcmi.lobby.mod.state.excessive" : "Має бути вимкнена",
+	"vcmi.lobby.mod.state.missing" : "Не встановлена",
+
+	"vcmi.client.errors.invalidMap" : "{Пошкоджена карта або кампанія}\n\nНе вдалося запустити гру! Вибрана карта або кампанія може бути невірною або пошкодженою. Причина:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{Не вистачає файлів даних}\n\nФайли даних кампаній не знайдено! Можливо, ви використовуєте неповні або пошкоджені файли даних Heroes 3. Будь ласка, перевстановіть дані гри.",
 	"vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його",
 	"vcmi.server.errors.modsToEnable"    : "{Потрібні модифікації для завантаження гри}",
@@ -205,6 +229,8 @@
 	"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Показувати вікно інформації героя}\n\nЗавжди показувати вікно статистики героїв, що відображає первинні параметри та очки заклинань.",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Пропускати вступну музику",
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Пропускати вступну музику}\n\n Пропускати коротку музику, яка грає на початку кожної битви перед початком дії. Також можна пропустити, натиснувши клавішу ESC.",
+	"vcmi.battleOptions.endWithAutocombat.hover": "Завершує бій",
+	"vcmi.battleOptions.endWithAutocombat.help": "{Завершує бій}\n\nАвто-бій миттєво завершує бій",
 
 	"vcmi.adventureMap.revisitObject.hover" : "Відвідати Об'єкт",
 	"vcmi.adventureMap.revisitObject.help" : "{Відвідати Об'єкт}\n\nЯкщо герой в даний момент стоїть на об'єкті мапи, він може знову відвідати цю локацію.",
@@ -224,8 +250,9 @@
 	"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s було вбито влучними пострілами!",
 	"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s було вбито влучним пострілом!",
 	"vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s було вбито влучними пострілами!",
+	"vcmi.battleWindow.endWithAutocombat" : "Ви впевнені, що хочете завершити бій автобоєм?",
 
-	"vcmi.battleResultsWindow.applyResultsLabel" : "Прийняти результат бою",
+	"vcmi.battleResultsWindow.applyResultsLabel" : "Прийняти результат бою?",
 
 	"vcmi.tutorialWindow.title" : "Використання Сенсорного Екрану",
 	"vcmi.tutorialWindow.decription.RightClick" : "Торкніться і утримуйте елемент, на якому ви хочете натиснути правою кнопкою миші. Торкніться вільної області, щоб закрити.",
@@ -266,6 +293,8 @@
 	"vcmi.heroWindow.openBackpack.hover" : "Відкрити вікно рюкзака з артефактами",
 	"vcmi.heroWindow.openBackpack.help"  : "Відкриває вікно, що дозволяє легше керувати рюкзаком артефактів",
 
+	"vcmi.tavernWindow.inviteHero"  : "Запросити героя",
+
 	"vcmi.commanderWindow.artifactMessage" : "Бажаєте передати цей артефакт герою?",
 
 	"vcmi.creatureWindow.showBonuses.hover"    : "Перейти до перегляду бонусів",
@@ -508,5 +537,5 @@
 	"vcmi.stackExperience.rank.7" :  "Експерт",
 	"vcmi.stackExperience.rank.8" :  "Еліта",
 	"vcmi.stackExperience.rank.9" : "Майстер",
-	"vcmi.stackExperience.rank.10" : "Профі",
+	"vcmi.stackExperience.rank.10" : "Профі"
 }

+ 2 - 0
client/CMT.cpp

@@ -375,6 +375,8 @@ int main(int argc, char * argv[])
 		while(!headlessQuit)
 			boost::this_thread::sleep_for(boost::chrono::milliseconds(200));
 
+		boost::this_thread::sleep_for(boost::chrono::milliseconds(500));
+
 		quitApplication();
 	}
 

+ 6 - 4
client/CMakeLists.txt

@@ -100,12 +100,13 @@ set(client_SRCS
 	globalLobby/GlobalLobbyClient.cpp
 	globalLobby/GlobalLobbyInviteWindow.cpp
 	globalLobby/GlobalLobbyLoginWindow.cpp
+	globalLobby/GlobalLobbyRoomWindow.cpp
 	globalLobby/GlobalLobbyServerSetup.cpp
 	globalLobby/GlobalLobbyWidget.cpp
 	globalLobby/GlobalLobbyWindow.cpp
 
 	widgets/Buttons.cpp
-	widgets/CArtifactHolder.cpp
+	widgets/CArtPlace.cpp
 	widgets/CComponent.cpp
 	widgets/CExchangeController.cpp
 	widgets/CGarrisonInt.cpp
@@ -124,7 +125,6 @@ set(client_SRCS
 	widgets/CArtifactsOfHeroAltar.cpp
 	widgets/CArtifactsOfHeroMarket.cpp
 	widgets/CArtifactsOfHeroBackpack.cpp
-	widgets/CWindowWithArtifacts.cpp
 	widgets/RadialMenu.cpp
 	widgets/markets/CAltarArtifacts.cpp
 	widgets/markets/CAltarCreatures.cpp
@@ -154,6 +154,7 @@ set(client_SRCS
 	windows/InfoWindows.cpp
 	windows/QuickRecruitmentWindow.cpp
 	windows/CHeroBackpackWindow.cpp
+	windows/CWindowWithArtifacts.cpp
 	windows/settings/GeneralOptionsTab.cpp
 	windows/settings/OtherOptionsTab.cpp
 	windows/settings/SettingsMainWindow.cpp
@@ -292,12 +293,13 @@ set(client_HEADERS
 	globalLobby/GlobalLobbyDefines.h
 	globalLobby/GlobalLobbyInviteWindow.h
 	globalLobby/GlobalLobbyLoginWindow.h
+	globalLobby/GlobalLobbyRoomWindow.h
 	globalLobby/GlobalLobbyServerSetup.h
 	globalLobby/GlobalLobbyWidget.h
 	globalLobby/GlobalLobbyWindow.h
 
 	widgets/Buttons.h
-	widgets/CArtifactHolder.h
+	widgets/CArtPlace.h
 	widgets/CComponent.h
 	widgets/CExchangeController.h
 	widgets/CGarrisonInt.h
@@ -316,7 +318,6 @@ set(client_HEADERS
 	widgets/CArtifactsOfHeroAltar.h
 	widgets/CArtifactsOfHeroMarket.h
 	widgets/CArtifactsOfHeroBackpack.h
-	widgets/CWindowWithArtifacts.h
 	widgets/RadialMenu.h
 	widgets/markets/CAltarArtifacts.h
 	widgets/markets/CAltarCreatures.h
@@ -346,6 +347,7 @@ set(client_HEADERS
 	windows/InfoWindows.h
 	windows/QuickRecruitmentWindow.h
 	windows/CHeroBackpackWindow.h
+	windows/CWindowWithArtifacts.h
 	windows/settings/GeneralOptionsTab.h
 	windows/settings/OtherOptionsTab.h
 	windows/settings/SettingsMainWindow.h

+ 11 - 5
client/CPlayerInterface.cpp

@@ -1024,7 +1024,7 @@ void CPlayerInterface::showYesNoDialog(const std::string &text, CFunctionList<vo
 	CInfoWindow::showYesNoDialog(text, components, onYes, onNo, playerID);
 }
 
-void CPlayerInterface::showBlockingDialog( const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel )
+void CPlayerInterface::showBlockingDialog(const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel, bool safeToAutoaccept)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	waitWhileDialog();
@@ -1034,6 +1034,12 @@ void CPlayerInterface::showBlockingDialog( const std::string &text, const std::v
 
 	if (!selection && cancel) //simple yes/no dialog
 	{
+		if(settings["general"]["enableUiEnhancements"].Bool() && safeToAutoaccept)
+		{
+			cb->selectionMade(1, askID); //as in HD mod, we try to skip dialogs that server considers visual fluff which does not affect gamestate
+			return;
+		}
+
 		std::vector<std::shared_ptr<CComponent>> intComps;
 		for (auto & component : components)
 			intComps.push_back(std::make_shared<CComponent>(component)); //will be deleted by close in window
@@ -1739,7 +1745,7 @@ void CPlayerInterface::artifactRemoved(const ArtifactLocation &al)
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	adventureInt->onHeroChanged(cb->getHero(al.artHolder));
 
-	for(auto artWin : GH.windows().findWindows<CArtifactHolder>())
+	for(auto artWin : GH.windows().findWindows<CWindowWithArtifacts>())
 		artWin->artifactRemoved(al);
 
 	waitWhileDialog();
@@ -1759,7 +1765,7 @@ void CPlayerInterface::artifactMoved(const ArtifactLocation &src, const Artifact
 			redraw = false;
 	}
 
-	for(auto artWin : GH.windows().findWindows<CArtifactHolder>())
+	for(auto artWin : GH.windows().findWindows<CWindowWithArtifacts>())
 		artWin->artifactMoved(src, dst, redraw);
 
 	waitWhileDialog();
@@ -1775,7 +1781,7 @@ void CPlayerInterface::artifactAssembled(const ArtifactLocation &al)
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	adventureInt->onHeroChanged(cb->getHero(al.artHolder));
 
-	for(auto artWin : GH.windows().findWindows<CArtifactHolder>())
+	for(auto artWin : GH.windows().findWindows<CWindowWithArtifacts>())
 		artWin->artifactAssembled(al);
 }
 
@@ -1784,7 +1790,7 @@ void CPlayerInterface::artifactDisassembled(const ArtifactLocation &al)
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	adventureInt->onHeroChanged(cb->getHero(al.artHolder));
 
-	for(auto artWin : GH.windows().findWindows<CArtifactHolder>())
+	for(auto artWin : GH.windows().findWindows<CWindowWithArtifacts>())
 		artWin->artifactDisassembled(al);
 }
 

+ 1 - 1
client/CPlayerInterface.h

@@ -119,7 +119,7 @@ protected: // Call-ins from server, should not be called directly, but only via
 	void receivedResource() override;
 	void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector<Component> & components, int soundID) override;
 	void showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level, QueryID queryID) override;
-	void showBlockingDialog(const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID.
+	void showBlockingDialog(const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel, bool safeToAutoaccept) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID.
 	void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override;
 	void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override;
 	void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects) override;

+ 4 - 4
client/Client.cpp

@@ -518,15 +518,15 @@ void CClient::handlePack(CPack * pack)
 	if(apply)
 	{
 		apply->applyOnClBefore(this, pack);
-		logNetwork->trace("\tMade first apply on cl: %s", typeid(pack).name());
+		logNetwork->trace("\tMade first apply on cl: %s", typeid(*pack).name());
 		gs->apply(pack);
-		logNetwork->trace("\tApplied on gs: %s", typeid(pack).name());
+		logNetwork->trace("\tApplied on gs: %s", typeid(*pack).name());
 		apply->applyOnClAfter(this, pack);
-		logNetwork->trace("\tMade second apply on cl: %s", typeid(pack).name());
+		logNetwork->trace("\tMade second apply on cl: %s", typeid(*pack).name());
 	}
 	else
 	{
-		logNetwork->error("Message %s cannot be applied, cannot find applier!", typeid(pack).name());
+		logNetwork->error("Message %s cannot be applied, cannot find applier!", typeid(*pack).name());
 	}
 	delete pack;
 }

+ 46 - 9
client/HeroMovementController.cpp

@@ -244,7 +244,7 @@ void HeroMovementController::onMoveHeroApplied()
 	}
 	else
 	{
-		moveOnce(hero, LOCPLINT->localState->getPath(hero));
+		sendMovementRequest(hero, LOCPLINT->localState->getPath(hero));
 	}
 }
 
@@ -335,28 +335,27 @@ bool HeroMovementController::canHeroStopAtNode(const CGPathNode & node) const
 void HeroMovementController::requestMovementStart(const CGHeroInstance * h, const CGPath & path)
 {
 	assert(duringMovement == false);
+
 	duringMovement = true;
 	currentlyMovingHero = h;
 
 	CCS->curh->hide();
-	moveOnce(h, path);
+	sendMovementRequest(h, path);
 }
 
-void HeroMovementController::moveOnce(const CGHeroInstance * h, const CGPath & path)
+void HeroMovementController::sendMovementRequest(const CGHeroInstance * h, const CGPath & path)
 {
-	// Moves hero once, sends request to server and immediately returns
-	// movement alongside paths will be done on receiving response from server
-
 	assert(duringMovement == true);
 
+	int heroMovementSpeed = settings["adventure"]["heroMoveTime"].Integer();
+	bool useMovementBatching = heroMovementSpeed == 0;
+
 	const auto & currNode = path.currNode();
 	const auto & nextNode = path.nextNode();
 
 	assert(nextNode.turns == 0);
 	assert(currNode.coord == h->visitablePos());
 
-	int3 nextCoord = h->convertFromVisitablePos(nextNode.coord);
-
 	if(nextNode.isTeleportAction())
 	{
 		stopMovementSound();
@@ -364,7 +363,8 @@ void HeroMovementController::moveOnce(const CGHeroInstance * h, const CGPath & p
 		LOCPLINT->cb->moveHero(h, h->pos, false);
 		return;
 	}
-	else
+
+	if (!useMovementBatching)
 	{
 		updateMovementSound(h, currNode.coord, nextNode.coord, nextNode.action);
 
@@ -373,7 +373,44 @@ void HeroMovementController::moveOnce(const CGHeroInstance * h, const CGPath & p
 		logGlobal->trace("Requesting hero movement to %s", nextNode.coord.toString());
 
 		bool useTransit = nextNode.layer == EPathfindingLayer::AIR || nextNode.layer == EPathfindingLayer::WATER;
+		int3 nextCoord = h->convertFromVisitablePos(nextNode.coord);
+
 		LOCPLINT->cb->moveHero(h, nextCoord, useTransit);
 		return;
 	}
+
+	bool useTransitAtStart = path.nextNode().layer == EPathfindingLayer::AIR || path.nextNode().layer == EPathfindingLayer::WATER;
+	std::vector<int3> pathToMove;
+
+	for (auto const & node : boost::adaptors::reverse(path.nodes))
+	{
+		if (node.coord == h->visitablePos())
+			continue; // first node, ignore - this is hero current position
+
+		if(node.isTeleportAction())
+			break; // pause after monolith / subterra gates
+
+		if (node.turns != 0)
+			break; // ran out of move points
+
+		bool useTransitHere = node.layer == EPathfindingLayer::AIR || node.layer == EPathfindingLayer::WATER;
+		if (useTransitHere != useTransitAtStart)
+			break;
+
+		int3 coord = h->convertFromVisitablePos(node.coord);
+		pathToMove.push_back(coord);
+
+		if (LOCPLINT->cb->guardingCreaturePosition(node.coord) != int3(-1, -1, -1))
+			break; // we reached zone-of-control of wandering monster
+
+		if (!LOCPLINT->cb->getVisitableObjs(node.coord).empty())
+			break; // we reached event, garrison or some other visitable object - end this movement batch
+	}
+
+	assert(!pathToMove.empty());
+	if (!pathToMove.empty())
+	{
+		updateMovementSound(h, currNode.coord, nextNode.coord, nextNode.action);
+		LOCPLINT->cb->moveHero(h, pathToMove, useTransitAtStart);
+	}
 }

+ 3 - 2
client/HeroMovementController.h

@@ -42,8 +42,9 @@ class HeroMovementController
 
 	void updatePath(const CGHeroInstance * hero, const TryMoveHero & details);
 
-	/// Moves hero 1 tile / path node
-	void moveOnce(const CGHeroInstance * h, const CGPath & path);
+	/// Sends one request to server to move selected hero alongside path.
+	/// Automatically selects between single-tile and multi-tile movement modes
+	void sendMovementRequest(const CGHeroInstance * h, const CGPath & path);
 
 	void endMove(const CGHeroInstance * h);
 

+ 1 - 1
client/NetPacksClient.cpp

@@ -738,7 +738,7 @@ void ApplyClientNetPackVisitor::visitBlockingDialog(BlockingDialog & pack)
 {
 	std::string str = pack.text.toString();
 
-	if(!callOnlyThatInterface(cl, pack.player, &CGameInterface::showBlockingDialog, str, pack.components, pack.queryID, (soundBase::soundID)pack.soundID, pack.selection(), pack.cancel()))
+	if(!callOnlyThatInterface(cl, pack.player, &CGameInterface::showBlockingDialog, str, pack.components, pack.queryID, (soundBase::soundID)pack.soundID, pack.selection(), pack.cancel(), pack.safeToAutoaccept()))
 		logNetwork->warn("We received YesNoDialog for not our player...");
 }
 

+ 6 - 4
client/adventureMap/AdventureMapInterface.cpp

@@ -171,18 +171,20 @@ void AdventureMapInterface::show(Canvas & to)
 
 void AdventureMapInterface::dim(Canvas & to)
 {
+	auto const isBigWindow = [&](std::shared_ptr<CIntObject> window) { return window->pos.w >= 800 && window->pos.h >= 600; }; // OH3 fullscreen
+
 	if(settings["adventure"]["hideBackground"].Bool())
-		for (auto window : GH.windows().findWindows<IShowActivatable>())
+		for (auto window : GH.windows().findWindows<CIntObject>())
 		{
-			if(!std::dynamic_pointer_cast<AdventureMapInterface>(window) && std::dynamic_pointer_cast<CIntObject>(window) && std::dynamic_pointer_cast<CIntObject>(window)->pos.w >= 800 && std::dynamic_pointer_cast<CIntObject>(window)->pos.w >= 600)
+			if(!std::dynamic_pointer_cast<AdventureMapInterface>(window) && std::dynamic_pointer_cast<CIntObject>(window) && isBigWindow(window))
 			{
 				to.fillTexture(GH.renderHandler().loadImage(ImagePath::builtin("DiBoxBck")));
 				return;
 			}
 		}
-	for (auto window : GH.windows().findWindows<IShowActivatable>())
+	for (auto window : GH.windows().findWindows<CIntObject>())
 	{
-		if (!std::dynamic_pointer_cast<AdventureMapInterface>(window) && !std::dynamic_pointer_cast<RadialMenu>(window) && !window->isPopupWindow())
+		if (!std::dynamic_pointer_cast<AdventureMapInterface>(window) && !std::dynamic_pointer_cast<RadialMenu>(window) && !window->isPopupWindow() && (settings["adventure"]["backgroundDimSmallWindows"].Bool() || isBigWindow(window)))
 		{
 			Rect targetRect(0, 0, GH.screenDimensions().x, GH.screenDimensions().y);
 			ColorRGBA colorToFill(0, 0, 0, std::clamp<int>(backgroundDimLevel, 0, 255));

+ 1 - 1
client/adventureMap/AdventureMapShortcuts.cpp

@@ -314,7 +314,7 @@ void AdventureMapShortcuts::visitObject()
 	const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero();
 
 	if(h)
-		LOCPLINT->cb->moveHero(h, h->pos);
+		LOCPLINT->cb->moveHero(h, h->pos, false);
 }
 
 void AdventureMapShortcuts::openObject()

+ 1 - 0
client/adventureMap/CInfoBar.cpp

@@ -386,6 +386,7 @@ void CInfoBar::pushComponents(const std::vector<Component> & components, std::st
 					reward_map.at(0).first.push_back(c);
 					reward_map.at(0).second = 8; //At most 8, cannot be more
 					break;
+				case ComponentType::NONE:
 				case ComponentType::SEC_SKILL:
 					reward_map.at(1).first.push_back(c);
 					reward_map.at(1).second = 4; //At most 4

+ 1 - 1
client/battle/BattleInterface.cpp

@@ -729,7 +729,7 @@ void BattleInterface::requestAutofightingAIToTakeAction()
 			// FIXME: unsafe
 			// Run task in separate thread to avoid UI lock while AI is making turn (which might take some time)
 			// HOWEVER this thread won't atttempt to lock game state, potentially leading to races
-			boost::thread aiThread([this, activeStack]()
+			boost::thread aiThread([battleID = this->battleID, curInt = this->curInt, activeStack]()
 			{
 				setThreadName("autofightingAI");
 				curInt->autofightingAI->activeStack(battleID, activeStack);

+ 14 - 1
client/globalLobby/GlobalLobbyClient.cpp

@@ -201,6 +201,8 @@ void GlobalLobbyClient::receiveActiveGameRooms(const JsonNode & json)
 		room.hostAccountDisplayName = jsonEntry["hostAccountDisplayName"].String();
 		room.description = jsonEntry["description"].String();
 		room.statusID = jsonEntry["status"].String();
+		room.gameVersion = jsonEntry["version"].String();
+		room.modList = ModVerificationInfo::jsonDeserializeList(jsonEntry["mods"]);
 		std::chrono::seconds ageSeconds (jsonEntry["ageSeconds"].Integer());
 		room.startDateFormatted = TextOperations::getCurrentFormattedDateTimeLocal(-ageSeconds);
 
@@ -277,7 +279,7 @@ void GlobalLobbyClient::receiveJoinRoomSuccess(const JsonNode & json)
 {
 	if (json["proxyMode"].Bool())
 	{
-		CSH->resetStateForLobby(EStartMode::NEW_GAME, ESelectionScreen::newGame, EServerMode::LOBBY_GUEST, {});
+		CSH->resetStateForLobby(EStartMode::NEW_GAME, ESelectionScreen::newGame, EServerMode::LOBBY_GUEST, { CSH->getGlobalLobby().getAccountDisplayName() });
 		CSH->loadMode = ELoadMode::MULTI;
 
 		std::string hostname = getServerHost();
@@ -430,6 +432,17 @@ const std::vector<GlobalLobbyRoom> & GlobalLobbyClient::getMatchesHistory() cons
 	return matchesHistory;
 }
 
+const GlobalLobbyRoom & GlobalLobbyClient::getActiveRoomByName(const std::string & roomUUID) const
+{
+	for (auto const & room : activeRooms)
+	{
+		if (room.gameRoomID == roomUUID)
+			return room;
+	}
+
+	throw std::out_of_range("Failed to find room with UUID of " + roomUUID);
+}
+
 const std::vector<GlobalLobbyChannelMessage> & GlobalLobbyClient::getChannelHistory(const std::string & channelType, const std::string & channelName) const
 {
 	static const std::vector<GlobalLobbyChannelMessage> emptyVector;

+ 3 - 0
client/globalLobby/GlobalLobbyClient.h

@@ -73,6 +73,9 @@ public:
 	const std::vector<GlobalLobbyRoom> & getMatchesHistory() const;
 	const std::vector<GlobalLobbyChannelMessage> & getChannelHistory(const std::string & channelType, const std::string & channelName) const;
 
+	/// Returns active room by ID. Throws out-of-range on failure
+	const GlobalLobbyRoom & getActiveRoomByName(const std::string & roomUUID) const;
+
 	const std::string & getAccountID() const;
 	const std::string & getAccountCookie() const;
 	const std::string & getAccountDisplayName() const;

+ 4 - 0
client/globalLobby/GlobalLobbyDefines.h

@@ -9,6 +9,8 @@
  */
 #pragma once
 
+#include "../../lib/modding/ModVerificationInfo.h"
+
 struct GlobalLobbyAccount
 {
 	std::string accountID;
@@ -22,8 +24,10 @@ struct GlobalLobbyRoom
 	std::string hostAccountID;
 	std::string hostAccountDisplayName;
 	std::string description;
+	std::string gameVersion;
 	std::string statusID;
 	std::string startDateFormatted;
+	ModCompatibilityInfo modList;
 	std::vector<GlobalLobbyAccount> participants;
 	int playerLimit;
 };

+ 200 - 0
client/globalLobby/GlobalLobbyRoomWindow.cpp

@@ -0,0 +1,200 @@
+/*
+ * GlobalLobbyRoomWindow.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#include "StdInc.h"
+#include "GlobalLobbyRoomWindow.h"
+
+#include "GlobalLobbyClient.h"
+#include "GlobalLobbyDefines.h"
+#include "GlobalLobbyWindow.h"
+
+#include "../CGameInfo.h"
+#include "../CServerHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../mainmenu/CMainMenu.h"
+#include "../widgets/Buttons.h"
+#include "../widgets/Images.h"
+#include "../widgets/TextControls.h"
+#include "../widgets/GraphicalPrimitiveCanvas.h"
+#include "../widgets/ObjectLists.h"
+
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/MetaString.h"
+#include "../../lib/VCMI_Lib.h"
+#include "../../lib/modding/CModHandler.h"
+#include "../../lib/modding/CModInfo.h"
+
+GlobalLobbyRoomAccountCard::GlobalLobbyRoomAccountCard(const GlobalLobbyAccount & accountDescription)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	pos.w = 130;
+	pos.h = 40;
+	backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1);
+	labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName);
+	labelStatus = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, accountDescription.status);
+}
+
+GlobalLobbyRoomModCard::GlobalLobbyRoomModCard(const GlobalLobbyRoomModInfo & modInfo)
+{
+	const std::map<ModVerificationStatus, std::string> statusToString = {
+		{ ModVerificationStatus::NOT_INSTALLED, "missing" },
+		{ ModVerificationStatus::DISABLED, "disabled" },
+		{ ModVerificationStatus::EXCESSIVE, "excessive" },
+		{ ModVerificationStatus::VERSION_MISMATCH, "version" },
+		{ ModVerificationStatus::FULL_MATCH, "compatible" }
+	};
+
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	pos.w = 200;
+	pos.h = 40;
+	backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1);
+
+	labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, modInfo.modName);
+	labelVersion = std::make_shared<CLabel>(195, 30, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, modInfo.version);
+	labelStatus = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.mod.state." + statusToString.at(modInfo.status)));
+}
+
+static const std::string getJoinRoomErrorMessage(const GlobalLobbyRoom & roomDescription, const std::vector<GlobalLobbyRoomModInfo> & modVerificationList)
+{
+	bool publicRoom = roomDescription.statusID == "public";
+	bool privateRoom = roomDescription.statusID == "private";
+	bool gameStarted = !publicRoom && !privateRoom;
+	bool hasInvite = CSH->getGlobalLobby().isInvitedToRoom(roomDescription.gameRoomID);
+	bool alreadyInRoom = CSH->inGame();
+
+	if (alreadyInRoom)
+		return "vcmi.lobby.preview.error.playing";
+
+	if (gameStarted)
+		return "vcmi.lobby.preview.error.busy";
+
+	if (VCMI_VERSION_STRING != roomDescription.gameVersion)
+		return "vcmi.lobby.preview.error.version";
+
+	if (roomDescription.playerLimit == roomDescription.participants.size())
+		return "vcmi.lobby.preview.error.full";
+
+	if (privateRoom && !hasInvite)
+		return "vcmi.lobby.preview.error.invite";
+
+	for(const auto & mod : modVerificationList)
+	{
+		switch (mod.status)
+		{
+			case ModVerificationStatus::NOT_INSTALLED:
+			case ModVerificationStatus::DISABLED:
+			case ModVerificationStatus::EXCESSIVE:
+				return "vcmi.lobby.preview.error.mods";
+				break;
+			case ModVerificationStatus::VERSION_MISMATCH:
+			case ModVerificationStatus::FULL_MATCH:
+				break;
+			default:
+				assert(0);
+		}
+	}
+	return "";
+}
+
+GlobalLobbyRoomWindow::GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const std::string & roomUUID)
+	: CWindowObject(BORDERED)
+	, roomUUID(roomUUID)
+	, window(window)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
+	pos.w = 400;
+	pos.h = 400;
+
+	GlobalLobbyRoom roomDescription = CSH->getGlobalLobby().getActiveRoomByName(roomUUID);
+	for(const auto & modEntry : ModVerificationInfo::verifyListAgainstLocalMods(roomDescription.modList))
+	{
+		GlobalLobbyRoomModInfo modInfo;
+		modInfo.status = modEntry.second;
+		if (modEntry.second == ModVerificationStatus::EXCESSIVE)
+			modInfo.version = CGI->modh->getModInfo(modEntry.first).getVerificationInfo().version.toString();
+		else
+			modInfo.version = roomDescription.modList.at(modEntry.first).version.toString();
+
+		if (modEntry.second == ModVerificationStatus::NOT_INSTALLED)
+			modInfo.modName = roomDescription.modList.at(modEntry.first).name;
+		else
+			modInfo.modName = CGI->modh->getModInfo(modEntry.first).getVerificationInfo().name;
+
+		modVerificationList.push_back(modInfo);
+	}
+
+	MetaString subtitleText;
+	subtitleText.appendTextID("vcmi.lobby.preview.subtitle");
+	subtitleText.replaceRawString(roomDescription.description);
+	subtitleText.replaceRawString(roomDescription.hostAccountDisplayName);
+
+	filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
+	labelTitle = std::make_shared<CLabel>( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.preview.title").toString());
+	labelSubtitle = std::make_shared<CLabel>( pos.w / 2, 40, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, subtitleText.toString());
+
+	labelVersionTitle = std::make_shared<CLabel>( 10, 60, FONT_MEDIUM, ETextAlignment::CENTERLEFT, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.preview.version").toString());
+	labelVersionValue = std::make_shared<CLabel>( 10, 80, FONT_MEDIUM, ETextAlignment::CENTERLEFT, Colors::WHITE, roomDescription.gameVersion);
+
+	buttonJoin = std::make_shared<CButton>(Point(10, 360), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onJoin(); });
+	buttonClose = std::make_shared<CButton>(Point(100, 360), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); });
+
+	MetaString joinStatusText;
+	std::string errorMessage = getJoinRoomErrorMessage(roomDescription, modVerificationList);
+	if (!errorMessage.empty())
+	{
+		joinStatusText.appendTextID("vcmi.lobby.preview.error.header");
+		joinStatusText.appendRawString("\n");
+		joinStatusText.appendTextID(errorMessage);
+	}
+	else
+		joinStatusText.appendTextID("vcmi.lobby.preview.allowed");
+
+	labelJoinStatus = std::make_shared<CTextBox>(joinStatusText.toString(), Rect(10, 280, 150, 70), 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
+
+	const auto & createAccountCardCallback = [participants = roomDescription.participants](size_t index) -> std::shared_ptr<CIntObject>
+	{
+		if(index < participants.size())
+			return std::make_shared<GlobalLobbyRoomAccountCard>(participants[index]);
+		return std::make_shared<CIntObject>();
+	};
+
+	accountListBackground = std::make_shared<TransparentFilledRectangle>(Rect(8, 98, 150, 180), ColorRGBA(0, 0, 0, 64), ColorRGBA(64, 80, 128, 255), 1);
+	accountList = std::make_shared<CListBox>(createAccountCardCallback, Point(10, 116), Point(0, 40), 4, roomDescription.participants.size(), 0, 1 | 4, Rect(130, 0, 160, 160));
+	accountList->setRedrawParent(true);
+	accountListTitle = std::make_shared<CLabel>( 12, 109, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.preview.players").toString());
+
+	const auto & createModCardCallback = [this](size_t index) -> std::shared_ptr<CIntObject>
+	{
+		if(index < modVerificationList.size())
+			return std::make_shared<GlobalLobbyRoomModCard>(modVerificationList[index]);
+		return std::make_shared<CIntObject>();
+	};
+
+	modListBackground = std::make_shared<TransparentFilledRectangle>(Rect(178, 48, 220, 340), ColorRGBA(0, 0, 0, 64), ColorRGBA(64, 80, 128, 255), 1);
+	modList = std::make_shared<CListBox>(createModCardCallback, Point(180, 66), Point(0, 40), 8, modVerificationList.size(), 0, 1 | 4, Rect(200, 0, 320, 320));
+	modList->setRedrawParent(true);
+	modListTitle = std::make_shared<CLabel>( 182, 59, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.preview.mods").toString());
+
+	buttonJoin->block(!errorMessage.empty());
+	filledBackground->playerColored(PlayerColor(1));
+	center();
+}
+
+void GlobalLobbyRoomWindow::onJoin()
+{
+	window->doJoinRoom(roomUUID);
+}
+
+void GlobalLobbyRoomWindow::onClose()
+{
+	close();
+}

+ 89 - 0
client/globalLobby/GlobalLobbyRoomWindow.h

@@ -0,0 +1,89 @@
+/*
+ * GlobalLobbyRoomWindow.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "../windows/CWindowObject.h"
+#include "../../lib/modding/ModVerificationInfo.h"
+
+class CLabel;
+class CTextBox;
+class FilledTexturePlayerColored;
+class CButton;
+class CToggleGroup;
+class GlobalLobbyWindow;
+class TransparentFilledRectangle;
+class CListBox;
+
+struct GlobalLobbyAccount;
+struct GlobalLobbyRoom;
+
+VCMI_LIB_NAMESPACE_BEGIN
+struct ModVerificationInfo;
+VCMI_LIB_NAMESPACE_END
+
+struct GlobalLobbyRoomModInfo
+{
+	std::string modName;
+	std::string version;
+	ModVerificationStatus status;
+};
+
+class GlobalLobbyRoomAccountCard : public CIntObject
+{
+	std::shared_ptr<TransparentFilledRectangle> backgroundOverlay;
+	std::shared_ptr<CLabel> labelName;
+	std::shared_ptr<CLabel> labelStatus;
+
+public:
+	GlobalLobbyRoomAccountCard(const GlobalLobbyAccount & accountDescription);
+};
+
+class GlobalLobbyRoomModCard : public CIntObject
+{
+	std::shared_ptr<TransparentFilledRectangle> backgroundOverlay;
+	std::shared_ptr<CLabel> labelName;
+	std::shared_ptr<CLabel> labelStatus;
+	std::shared_ptr<CLabel> labelVersion;
+
+public:
+	GlobalLobbyRoomModCard(const GlobalLobbyRoomModInfo & modInfo);
+};
+
+class GlobalLobbyRoomWindow : public CWindowObject
+{
+	std::vector<GlobalLobbyRoomModInfo> modVerificationList;
+	GlobalLobbyWindow * window;
+	std::string roomUUID;
+
+	std::shared_ptr<FilledTexturePlayerColored> filledBackground;
+	std::shared_ptr<CLabel> labelTitle;
+	std::shared_ptr<CLabel> labelSubtitle;
+	std::shared_ptr<CLabel> labelVersionTitle;
+	std::shared_ptr<CLabel> labelVersionValue;
+	std::shared_ptr<CTextBox> labelJoinStatus;
+
+	std::shared_ptr<CLabel> accountListTitle;
+	std::shared_ptr<CLabel> modListTitle;
+
+	std::shared_ptr<TransparentFilledRectangle> accountListBackground;
+	std::shared_ptr<TransparentFilledRectangle> modListBackground;
+
+	std::shared_ptr<CListBox> accountList;
+	std::shared_ptr<CListBox> modList;
+
+	std::shared_ptr<CButton> buttonJoin;
+	std::shared_ptr<CButton> buttonClose;
+
+	void onJoin();
+	void onClose();
+
+public:
+	GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const std::string & roomUUID);
+};

+ 4 - 2
client/globalLobby/GlobalLobbyServerSetup.cpp

@@ -11,6 +11,8 @@
 #include "StdInc.h"
 #include "GlobalLobbyServerSetup.h"
 
+#include "GlobalLobbyClient.h"
+
 #include "../CGameInfo.h"
 #include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
@@ -125,9 +127,9 @@ void GlobalLobbyServerSetup::onGameModeChanged(int value)
 void GlobalLobbyServerSetup::onCreate()
 {
 	if(toggleGameMode->getSelected() == 0)
-		CSH->resetStateForLobby(EStartMode::NEW_GAME, ESelectionScreen::newGame, EServerMode::LOBBY_HOST, {});
+		CSH->resetStateForLobby(EStartMode::NEW_GAME, ESelectionScreen::newGame, EServerMode::LOBBY_HOST, { CSH->getGlobalLobby().getAccountDisplayName() });
 	else
-		CSH->resetStateForLobby(EStartMode::LOAD_GAME, ESelectionScreen::loadGame, EServerMode::LOBBY_HOST, {});
+		CSH->resetStateForLobby(EStartMode::LOAD_GAME, ESelectionScreen::loadGame, EServerMode::LOBBY_HOST, { CSH->getGlobalLobby().getAccountDisplayName() });
 
 	CSH->loadMode = ELoadMode::MULTI;
 	CSH->startLocalServerAndConnect(true);

+ 11 - 15
client/globalLobby/GlobalLobbyWidget.cpp

@@ -13,6 +13,7 @@
 
 #include "GlobalLobbyClient.h"
 #include "GlobalLobbyWindow.h"
+#include "GlobalLobbyRoomWindow.h"
 
 #include "../CGameInfo.h"
 #include "../CMusicHandler.h"
@@ -209,17 +210,13 @@ GlobalLobbyAccountCard::GlobalLobbyAccountCard(GlobalLobbyWindow * window, const
 }
 
 GlobalLobbyRoomCard::GlobalLobbyRoomCard(GlobalLobbyWindow * window, const GlobalLobbyRoom & roomDescription)
+	: roomUUID(roomDescription.gameRoomID)
+	, window(window)
 {
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	addUsedEvents(LCLICK);
 
-	const auto & onJoinClicked = [window, roomID = roomDescription.gameRoomID]()
-	{
-		window->doJoinRoom(roomID);
-	};
-
-	bool publicRoom = roomDescription.statusID == "public";
 	bool hasInvite = CSH->getGlobalLobby().isInvitedToRoom(roomDescription.gameRoomID);
-	bool canJoin = publicRoom || hasInvite;
 
 	auto roomSizeText = MetaString::createFromRawString("%d/%d");
 	roomSizeText.replaceNumber(roomDescription.participants.size());
@@ -241,15 +238,14 @@ GlobalLobbyRoomCard::GlobalLobbyRoomCard(GlobalLobbyWindow * window, const Globa
 
 	labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, roomDescription.hostAccountDisplayName);
 	labelDescription = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, roomDescription.description);
-	labelRoomSize = std::make_shared<CLabel>(178, 10, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, roomSizeText.toString());
-	labelRoomStatus = std::make_shared<CLabel>(190, 30, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, roomStatusText.toString());
-	iconRoomSize = std::make_shared<CPicture>(ImagePath::builtin("lobby/iconPlayer"), Point(180, 5));
+	labelRoomSize = std::make_shared<CLabel>(212, 10, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, roomSizeText.toString());
+	labelRoomStatus = std::make_shared<CLabel>(225, 30, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, roomStatusText.toString());
+	iconRoomSize = std::make_shared<CPicture>(ImagePath::builtin("lobby/iconPlayer"), Point(214, 5));
+}
 
-	if(!CSH->inGame() && canJoin)
-	{
-		buttonJoin = std::make_shared<CButton>(Point(194, 4), AnimationPath::builtin("lobbyJoinRoom"), CButton::tooltip(), onJoinClicked);
-		buttonJoin->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("lobby/iconEnter")));
-	}
+void GlobalLobbyRoomCard::clickPressed(const Point & cursorPosition)
+{
+	GH.windows().createAndPushWindow<GlobalLobbyRoomWindow>(window, roomUUID);
 }
 
 GlobalLobbyChannelCard::GlobalLobbyChannelCard(GlobalLobbyWindow * window, const std::string & channelName)

+ 4 - 0
client/globalLobby/GlobalLobbyWidget.h

@@ -69,6 +69,9 @@ public:
 
 class GlobalLobbyRoomCard : public CIntObject
 {
+	GlobalLobbyWindow * window;
+	std::string roomUUID;
+
 	std::shared_ptr<TransparentFilledRectangle> backgroundOverlay;
 	std::shared_ptr<CLabel> labelName;
 	std::shared_ptr<CLabel> labelRoomSize;
@@ -77,6 +80,7 @@ class GlobalLobbyRoomCard : public CIntObject
 	std::shared_ptr<CButton> buttonJoin;
 	std::shared_ptr<CPicture> iconRoomSize;
 
+	void clickPressed(const Point & cursorPosition) override;
 public:
 	GlobalLobbyRoomCard(GlobalLobbyWindow * window, const GlobalLobbyRoom & roomDescription);
 };

+ 2 - 1
client/gui/Shortcut.h

@@ -59,7 +59,8 @@ enum class EShortcut
 	MAIN_MENU_CAMPAIGN_CUSTOM,
 
 	// Game lobby / scenario selection
-	LOBBY_BEGIN_GAME, // b, Return
+	LOBBY_BEGIN_STANDARD_GAME, // b
+	LOBBY_BEGIN_CAMPAIGN, // Return
 	LOBBY_LOAD_GAME,  // l, Return
 	LOBBY_SAVE_GAME,  // s, Return
 	LOBBY_RANDOM_MAP, // Open random map tab

+ 2 - 1
client/gui/ShortcutHandler.cpp

@@ -99,7 +99,8 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 		{"mainMenuCampaignRoe",      EShortcut::MAIN_MENU_CAMPAIGN_ROE    },
 		{"mainMenuCampaignAb",       EShortcut::MAIN_MENU_CAMPAIGN_AB     },
 		{"mainMenuCampaignCustom",   EShortcut::MAIN_MENU_CAMPAIGN_CUSTOM },
-		{"lobbyBeginGame",           EShortcut::LOBBY_BEGIN_GAME          },
+		{"lobbyBeginStandardGame",   EShortcut::LOBBY_BEGIN_STANDARD_GAME },
+		{"lobbyBeginCampaign",       EShortcut::LOBBY_BEGIN_CAMPAIGN      },
 		{"lobbyLoadGame",            EShortcut::LOBBY_LOAD_GAME           },
 		{"lobbySaveGame",            EShortcut::LOBBY_SAVE_GAME           },
 		{"lobbyRandomMap",           EShortcut::LOBBY_RANDOM_MAP          },

+ 1 - 1
client/lobby/CBonusSelection.cpp

@@ -240,7 +240,7 @@ void CBonusSelection::createBonusesIcons()
 		}
 		case CampaignBonusType::SECONDARY_SKILL:
 			desc.appendLocalString(EMetaText::GENERAL_TXT, 718);
-			desc.replaceTextID(TextIdentifier("core", "genrltxt", "levels", bonDescs[i].info3 - 1).get());
+			desc.replaceTextID(TextIdentifier("core", "skilllev", bonDescs[i].info3 - 1).get());
 			desc.replaceName(SecondarySkill(bonDescs[i].info2));
 			picNumber = bonDescs[i].info2 * 3 + bonDescs[i].info3 - 1;
 

+ 2 - 2
client/lobby/CLobbyScreen.cpp

@@ -82,7 +82,7 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType)
 
 		card->iconDifficulty->addCallback(std::bind(&IServerAPI::setDifficulty, CSH, _1));
 
-		buttonStart = std::make_shared<CButton>(Point(411, 535), AnimationPath::builtin("SCNRBEG.DEF"), CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, false), EShortcut::LOBBY_BEGIN_GAME);
+		buttonStart = std::make_shared<CButton>(Point(411, 535), AnimationPath::builtin("SCNRBEG.DEF"), CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, false), EShortcut::LOBBY_BEGIN_STANDARD_GAME);
 		initLobby();
 		break;
 	}
@@ -97,7 +97,7 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType)
 	}
 	case ESelectionScreen::campaignList:
 		tabSel->callOnSelect = std::bind(&IServerAPI::setMapInfo, CSH, _1, nullptr);
-		buttonStart = std::make_shared<CButton>(Point(411, 535), AnimationPath::builtin("SCNRLOD.DEF"), CButton::tooltip(), std::bind(&CLobbyScreen::startCampaign, this), EShortcut::LOBBY_BEGIN_GAME);
+		buttonStart = std::make_shared<CButton>(Point(411, 535), AnimationPath::builtin("SCNRLOD.DEF"), CButton::tooltip(), std::bind(&CLobbyScreen::startCampaign, this), EShortcut::LOBBY_BEGIN_CAMPAIGN);
 		break;
 	}
 

+ 3 - 0
client/lobby/SelectionTab.cpp

@@ -677,6 +677,9 @@ void SelectionTab::selectFileName(std::string fname)
 
 	filter(-1);
 	selectAbs(-1);
+
+	if(tabType == ESelectionScreen::saveGame && inputName->getText().empty())
+		inputName->setText("NEWGAME");
 }
 
 std::shared_ptr<ElementInfo> SelectionTab::getSelectedMapInfo() const

部分文件因为文件数量过多而无法显示