Преглед на файлове

Merge remote-tracking branch 'upstream/develop' into loading-bar

# Conflicts:
#	server/CGameHandler.h
nordsoft преди 2 години
родител
ревизия
dbc3a93013
променени са 61 файла, в които са добавени 6716 реда и са изтрити 5117 реда
  1. 11 17
      AI/BattleAI/BattleAI.cpp
  2. 1 1
      AI/BattleAI/BattleAI.h
  3. 6 0
      AI/EmptyAI/CEmptyAI.cpp
  4. 1 0
      AI/EmptyAI/CEmptyAI.h
  5. 1 0
      AI/StupidAI/StupidAI.cpp
  6. 4 1
      AI/VCAI/VCAI.cpp
  7. 1 0
      AI/VCAI/VCAI.h
  8. 1 1
      CCallback.cpp
  9. 7 8
      client/CPlayerInterface.cpp
  10. 1 1
      client/CPlayerInterface.h
  11. 2 2
      client/Client.h
  12. 3 3
      client/NetPacksClient.cpp
  13. 2 2
      client/battle/BattleActionsController.cpp
  14. 3 3
      client/battle/BattleEffectsController.cpp
  15. 1 1
      client/battle/BattleEffectsController.h
  16. 11 11
      client/battle/BattleInterface.cpp
  17. 3 3
      client/battle/BattleInterface.h
  18. 6 6
      client/battle/BattleInterfaceClasses.cpp
  19. 3 10
      client/battle/BattleStacksController.cpp
  20. 2 2
      client/battle/BattleStacksController.h
  21. 122 1
      client/lobby/SelectionTab.cpp
  22. 23 1
      client/lobby/SelectionTab.h
  23. 13 1
      config/schemas/settings.json
  24. 2 5
      lib/CGameInterface.h
  25. 0 1
      lib/GameConstants.cpp
  26. 16 8
      lib/GameConstants.h
  27. 0 1
      lib/NetPackVisitor.h
  28. 1 21
      lib/NetPacks.h
  29. 25 29
      lib/NetPacksLib.cpp
  30. 42 5
      lib/battle/BattleAction.cpp
  31. 5 2
      lib/battle/BattleAction.h
  32. 0 1
      lib/registerTypes/RegisterTypes.h
  33. 2125 3955
      server/CGameHandler.cpp
  34. 17 97
      server/CGameHandler.h
  35. 26 6
      server/CMakeLists.txt
  36. 0 584
      server/CQuery.cpp
  37. 0 242
      server/CQuery.h
  38. 1 1
      server/CVCMIServer.cpp
  39. 8 48
      server/NetPacksServer.cpp
  40. 1 2
      server/ServerNetPackVisitors.h
  41. 9 4
      server/ServerSpellCastEnvironment.cpp
  42. 1431 0
      server/battles/BattleActionProcessor.cpp
  43. 79 0
      server/battles/BattleActionProcessor.h
  44. 741 0
      server/battles/BattleFlowProcessor.cpp
  45. 55 0
      server/battles/BattleFlowProcessor.h
  46. 250 0
      server/battles/BattleProcessor.cpp
  47. 80 0
      server/battles/BattleProcessor.h
  48. 539 0
      server/battles/BattleResultProcessor.cpp
  49. 78 0
      server/battles/BattleResultProcessor.h
  50. 11 11
      server/processors/HeroPoolProcessor.cpp
  51. 0 0
      server/processors/HeroPoolProcessor.h
  52. 30 17
      server/processors/PlayerMessageProcessor.cpp
  53. 2 2
      server/processors/PlayerMessageProcessor.h
  54. 75 0
      server/queries/BattleQueries.cpp
  55. 40 0
      server/queries/BattleQueries.h
  56. 201 0
      server/queries/CQuery.cpp
  57. 99 0
      server/queries/CQuery.h
  58. 227 0
      server/queries/MapQueries.cpp
  59. 103 0
      server/queries/MapQueries.h
  60. 129 0
      server/queries/QueriesProcessor.cpp
  61. 40 0
      server/queries/QueriesProcessor.h

+ 11 - 17
AI/BattleAI/BattleAI.cpp

@@ -18,6 +18,7 @@
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/spells/CSpellHandler.h"
 #include "../../lib/spells/ISpellMechanics.h"
+#include "../../lib/battle/BattleAction.h"
 #include "../../lib/battle/BattleStateInfoForRetreat.h"
 #include "../../lib/battle/CObstacleInstance.h"
 #include "../../lib/CStack.h" // TODO: remove
@@ -283,20 +284,11 @@ void CBattleAI::activeStack( const CStack * stack )
 			return;
 		}
 
-		attemptCastingSpell();
+		if (attemptCastingSpell())
+			return;
 
 		logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start));
 
-		if(cb->battleIsFinished() || !stack->alive())
-		{
-			//spellcast may finish battle or kill active stack
-			//send special preudo-action
-			BattleAction cancel;
-			cancel.actionType = EActionType::CANCEL;
-			cb->battleMakeUnitAction(cancel);
-			return;
-		}
-
 		if(auto action = considerFleeingOrSurrendering())
 		{
 			cb->battleMakeUnitAction(*action);
@@ -476,14 +468,14 @@ BattleAction CBattleAI::useCatapult(const CStack * stack)
 	return attack;
 }
 
-void CBattleAI::attemptCastingSpell()
+bool CBattleAI::attemptCastingSpell()
 {
 	auto hero = cb->battleGetMyHero();
 	if(!hero)
-		return;
+		return false;
 
 	if(cb->battleCanCastSpell(hero, spells::Mode::HERO) != ESpellCastProblem::OK)
-		return;
+		return false;
 
 	LOGL("Casting spells sounds like fun. Let's see...");
 	//Get all spells we can cast
@@ -522,7 +514,7 @@ void CBattleAI::attemptCastingSpell()
 	}
 	LOGFL("Found %d spell-target combinations.", possibleCasts.size());
 	if(possibleCasts.empty())
-		return;
+		return false;
 
 	using ValueMap = PossibleSpellcast::ValueMap;
 
@@ -657,7 +649,7 @@ void CBattleAI::attemptCastingSpell()
 			if(battleIsFinishedOpt)
 			{
 				print("No need to cast a spell. Battle will finish soon.");
-				return;
+				return false;
 			}
 		}
 	}
@@ -780,16 +772,18 @@ void CBattleAI::attemptCastingSpell()
 		LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->getNameTranslated() % castToPerform.value);
 		BattleAction spellcast;
 		spellcast.actionType = EActionType::HERO_SPELL;
-		spellcast.actionSubtype = castToPerform.spell->id;
+		spellcast.spell = castToPerform.spell->getId();
 		spellcast.setTarget(castToPerform.dest);
 		spellcast.side = side;
 		spellcast.stackNumber = (!side) ? -1 : -2;
 		cb->battleMakeSpellAction(spellcast);
 		movesSkippedByDefense = 0;
+		return true;
 	}
 	else
 	{
 		LOGFL("Best spell is %s. But it is actually useless (value %d).", castToPerform.spell->getNameTranslated() % castToPerform.value);
+		return false;
 	}
 }
 

+ 1 - 1
AI/BattleAI/BattleAI.h

@@ -68,7 +68,7 @@ public:
 	~CBattleAI();
 
 	void initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB) override;
-	void attemptCastingSpell();
+	bool attemptCastingSpell();
 
 	void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only
 

+ 6 - 0
AI/EmptyAI/CEmptyAI.cpp

@@ -12,6 +12,7 @@
 
 #include "../../lib/CRandomGenerator.h"
 #include "../../lib/CStack.h"
+#include "../../lib/battle/BattleAction.h"
 
 void CEmptyAI::saveGame(BinarySerializer & h, const int version)
 {
@@ -73,3 +74,8 @@ void CEmptyAI::showMapObjectSelectDialog(QueryID askID, const Component & icon,
 {
 	cb->selectionMade(0, askID);
 }
+
+std::optional<BattleAction> CEmptyAI::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState)
+{
+	return std::nullopt;
+}

+ 1 - 0
AI/EmptyAI/CEmptyAI.h

@@ -32,6 +32,7 @@ public:
 	void showTeleportDialog(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;
+	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override;
 };
 
 #define NAME "EmptyAI 0.1"

+ 1 - 0
AI/StupidAI/StupidAI.cpp

@@ -13,6 +13,7 @@
 #include "../../lib/CStack.h"
 #include "../../CCallback.h"
 #include "../../lib/CCreatureHandler.h"
+#include "../../lib/battle/BattleAction.h"
 
 static std::shared_ptr<CBattleCallback> cbc;
 

+ 4 - 1
AI/VCAI/VCAI.cpp

@@ -2890,4 +2890,7 @@ bool shouldVisit(HeroPtr h, const CGObjectInstance * obj)
 	return true;
 }
 
-
+std::optional<BattleAction> VCAI::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState)
+{
+	return std::nullopt;
+}

+ 1 - 0
AI/VCAI/VCAI.h

@@ -203,6 +203,7 @@ public:
 
 	void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override;
 	void battleEnd(const BattleResult * br, QueryID queryID) override;
+	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override;
 
 	void makeTurn();
 	void mainLoop();

+ 1 - 1
CCallback.cpp

@@ -206,7 +206,7 @@ bool CCallback::buildBuilding(const CGTownInstance *town, BuildingID buildingID)
 void CBattleCallback::battleMakeSpellAction(const BattleAction & action)
 {
 	assert(action.actionType == EActionType::HERO_SPELL);
-	MakeCustomAction mca(action);
+	MakeAction mca(action);
 	sendRequest(&mca);
 }
 

+ 7 - 8
client/CPlayerInterface.cpp

@@ -128,7 +128,6 @@ CPlayerInterface::CPlayerInterface(PlayerColor Player):
 	destinationTeleportPos = int3(-1);
 	GH.defActionsDef = 0;
 	LOCPLINT = this;
-	curAction = nullptr;
 	playerID=Player;
 	human=true;
 	battleInt = nullptr;
@@ -769,8 +768,7 @@ void CPlayerInterface::actionStarted(const BattleAction &action)
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
 
-	curAction = new BattleAction(action);
-	battleInt->startAction(curAction);
+	battleInt->startAction(action);
 }
 
 void CPlayerInterface::actionFinished(const BattleAction &action)
@@ -778,9 +776,7 @@ void CPlayerInterface::actionFinished(const BattleAction &action)
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
 
-	battleInt->endAction(curAction);
-	delete curAction;
-	curAction = nullptr;
+	battleInt->endAction(action);
 }
 
 void CPlayerInterface::activeStack(const CStack * stack) //called when it's turn of that stack
@@ -935,8 +931,6 @@ void CPlayerInterface::battleAttack(const BattleAttack * ba)
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
 
-	assert(curAction);
-
 	StackAttackInfo info;
 	info.attacker = cb->battleGetStackByID(ba->stackAttacking);
 	info.defender = nullptr;
@@ -2110,3 +2104,8 @@ void CPlayerInterface::showWorldViewEx(const std::vector<ObjectPosInfo>& objectP
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	adventureInt->openWorldView(objectPositions, showTerrain );
 }
+
+std::optional<BattleAction> CPlayerInterface::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState)
+{
+	return std::nullopt;
+}

+ 1 - 1
client/CPlayerInterface.h

@@ -66,7 +66,6 @@ class CPlayerInterface : public CGameInterface, public IUpdateable
 	int autosaveCount;
 
 	std::list<std::shared_ptr<CInfoWindow>> dialogs; //queue of dialogs awaiting to be shown (not currently shown!)
-	const BattleAction *curAction; //during the battle - action currently performed by active stack (or nullptr)
 
 	ObjectInstanceID destinationTeleport; //contain -1 or object id if teleportation
 	int3 destinationTeleportPos;
@@ -173,6 +172,7 @@ protected: // Call-ins from server, should not be called directly, but only via
 	void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack
 	void battleGateStateChanged(const EGateState state) override;
 	void yourTacticPhase(int distance) override;
+	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override;
 
 public: // public interface for use by client via LOCPLINT access
 

+ 2 - 2
client/Client.h

@@ -13,7 +13,6 @@
 #include <vcmi/Environment.h>
 
 #include "../lib/IGameCallback.h"
-#include "../lib/battle/BattleAction.h"
 #include "../lib/battle/CBattleInfoCallback.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -25,6 +24,7 @@ class CBattleGameInterface;
 class CGameInterface;
 class BinaryDeserializer;
 class BinarySerializer;
+class BattleAction;
 
 template<typename T> class CApplier;
 
@@ -118,7 +118,7 @@ public:
 
 	std::map<PlayerColor, std::vector<std::shared_ptr<IBattleEventsReceiver>>> additionalBattleInts;
 
-	std::optional<BattleAction> curbaction;
+	std::unique_ptr<BattleAction> currentBattleAction;
 
 	CClient();
 	~CClient();

+ 3 - 3
client/NetPacksClient.cpp

@@ -784,7 +784,7 @@ void ApplyClientNetPackVisitor::visitBattleAttack(BattleAttack & pack)
 
 void ApplyFirstClientNetPackVisitor::visitStartAction(StartAction & pack)
 {
-	cl.curbaction = std::make_optional(pack.ba);
+	cl.currentBattleAction = std::make_unique<BattleAction>(pack.ba);
 	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::actionStarted, pack.ba);
 }
 
@@ -830,8 +830,8 @@ void ApplyClientNetPackVisitor::visitCatapultAttack(CatapultAttack & pack)
 
 void ApplyClientNetPackVisitor::visitEndAction(EndAction & pack)
 {
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::actionFinished, *cl.curbaction);
-	cl.curbaction.reset();
+	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::actionFinished, *cl.currentBattleAction);
+	cl.currentBattleAction.reset();
 }
 
 void ApplyClientNetPackVisitor::visitPackageApplied(PackageApplied & pack)

+ 2 - 2
client/battle/BattleActionsController.cpp

@@ -286,7 +286,7 @@ void BattleActionsController::castThisSpell(SpellID spellID)
 {
 	heroSpellToCast = std::make_shared<BattleAction>();
 	heroSpellToCast->actionType = EActionType::HERO_SPELL;
-	heroSpellToCast->actionSubtype = spellID; //spell number
+	heroSpellToCast->spell = spellID;
 	heroSpellToCast->stackNumber = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? -1 : -2;
 	heroSpellToCast->side = owner.defendingHeroInstance ? (owner.curInt->playerID == owner.defendingHeroInstance->tempOwner) : false;
 
@@ -314,7 +314,7 @@ void BattleActionsController::castThisSpell(SpellID spellID)
 const CSpell * BattleActionsController::getHeroSpellToCast( ) const
 {
 	if (heroSpellToCast)
-		return SpellID(heroSpellToCast->actionSubtype).toSpell();
+		return heroSpellToCast->spell.toSpell();
 	return nullptr;
 }
 

+ 3 - 3
client/battle/BattleEffectsController.cpp

@@ -94,13 +94,13 @@ void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bt
 	owner.waitForAnimations();
 }
 
-void BattleEffectsController::startAction(const BattleAction* action)
+void BattleEffectsController::startAction(const BattleAction & action)
 {
 	owner.checkForAnimations();
 
-	const CStack *stack = owner.curInt->cb->battleGetStackByID(action->stackNumber);
+	const CStack *stack = owner.curInt->cb->battleGetStackByID(action.stackNumber);
 
-	switch(action->actionType)
+	switch(action.actionType)
 	{
 	case EActionType::WAIT:
 		owner.appendBattleLog(stack->formatGeneralMessage(136));

+ 1 - 1
client/battle/BattleEffectsController.h

@@ -60,7 +60,7 @@ public:
 
 	BattleEffectsController(BattleInterface & owner);
 
-	void startAction(const BattleAction* action);
+	void startAction(const BattleAction & action);
 
 	//displays custom effect on the battlefield
 	void displayEffect(EBattleEffect effect, const BattleHex & destTile);

+ 11 - 11
client/battle/BattleInterface.cpp

@@ -234,7 +234,7 @@ void BattleInterface::newRound(int number)
 	console->addText(CGI->generaltexth->allTexts[412]);
 }
 
-void BattleInterface::giveCommand(EActionType action, BattleHex tile, si32 additional)
+void BattleInterface::giveCommand(EActionType action, BattleHex tile, SpellID spell)
 {
 	const CStack * actor = nullptr;
 	if(action != EActionType::HERO_SPELL && action != EActionType::RETREAT && action != EActionType::SURRENDER)
@@ -253,7 +253,7 @@ void BattleInterface::giveCommand(EActionType action, BattleHex tile, si32 addit
 	ba.side = side.value();
 	ba.actionType = action;
 	ba.aimToHex(tile);
-	ba.actionSubtype = additional;
+	ba.spell = spell;
 
 	sendCommand(ba, actor);
 }
@@ -567,12 +567,12 @@ bool BattleInterface::makingTurn() const
 	return stacksController->getActiveStack() != nullptr;
 }
 
-void BattleInterface::endAction(const BattleAction* action)
+void BattleInterface::endAction(const BattleAction &action)
 {
 	// it is possible that tactics mode ended while opening music is still playing
 	waitForAnimations();
 
-	const CStack *stack = curInt->cb->battleGetStackByID(action->stackNumber);
+	const CStack *stack = curInt->cb->battleGetStackByID(action.stackNumber);
 
 	// Activate stack from stackToActivate because this might have been temporary disabled, e.g., during spell cast
 	activateStack();
@@ -585,7 +585,7 @@ void BattleInterface::endAction(const BattleAction* action)
 		tacticNextStack(stack);
 
 	//we have activated next stack after sending request that has been just realized -> blockmap due to movement has changed
-	if(action->actionType == EActionType::HERO_SPELL)
+	if(action.actionType == EActionType::HERO_SPELL)
 		fieldController->redrawBackgroundWithHexes();
 }
 
@@ -594,15 +594,15 @@ void BattleInterface::appendBattleLog(const std::string & newEntry)
 	console->addText(newEntry);
 }
 
-void BattleInterface::startAction(const BattleAction* action)
+void BattleInterface::startAction(const BattleAction & action)
 {
-	if(action->actionType == EActionType::END_TACTIC_PHASE)
+	if(action.actionType == EActionType::END_TACTIC_PHASE)
 	{
 		windowObject->tacticPhaseEnded();
 		return;
 	}
 
-	const CStack *stack = curInt->cb->battleGetStackByID(action->stackNumber);
+	const CStack *stack = curInt->cb->battleGetStackByID(action.stackNumber);
 
 	if (stack)
 	{
@@ -610,17 +610,17 @@ void BattleInterface::startAction(const BattleAction* action)
 	}
 	else
 	{
-		assert(action->actionType == EActionType::HERO_SPELL); //only cast spell is valid action without acting stack number
+		assert(action.actionType == EActionType::HERO_SPELL); //only cast spell is valid action without acting stack number
 	}
 
 	stacksController->startAction(action);
 
-	if(action->actionType == EActionType::HERO_SPELL) //when hero casts spell
+	if(action.actionType == EActionType::HERO_SPELL) //when hero casts spell
 		return;
 
 	if (!stack)
 	{
-		logGlobal->error("Something wrong with stackNumber in actionStarted. Stack number: %d", action->stackNumber);
+		logGlobal->error("Something wrong with stackNumber in actionStarted. Stack number: %d", action.stackNumber);
 		return;
 	}
 

+ 3 - 3
client/battle/BattleInterface.h

@@ -156,7 +156,7 @@ public:
 	void activateStack(); //sets activeStack to stackToActivate etc. //FIXME: No, it's not clear at all
 	void requestAutofightingAIToTakeAction();
 
-	void giveCommand(EActionType action, BattleHex tile = BattleHex(), si32 additional = -1);
+	void giveCommand(EActionType action, BattleHex tile = BattleHex(), SpellID spell = SpellID::NONE);
 	void sendCommand(BattleAction command, const CStack * actor = nullptr);
 
 	const CGHeroInstance *getActiveHero(); //returns hero that can currently cast a spell
@@ -188,7 +188,7 @@ public:
 	void addToAnimationStage( EAnimationEvents event, const AwaitingAnimationAction & action);
 
 	//call-ins
-	void startAction(const BattleAction* action);
+	void startAction(const BattleAction & action);
 	void stackReset(const CStack * stack);
 	void stackAdded(const CStack * stack); //new stack appeared on battlefield
 	void stackRemoved(uint32_t stackID); //stack disappeared from batlefiled
@@ -211,7 +211,7 @@ public:
 	void displaySpellEffect(const CSpell * spell, BattleHex destinationTile); //displays spell`s affected animation
 	void displaySpellHit(const CSpell * spell, BattleHex destinationTile); //displays spell`s affected animation
 
-	void endAction(const BattleAction* action);
+	void endAction(const BattleAction & action);
 
 	void obstaclePlaced(const std::vector<std::shared_ptr<const CObstacleInstance>> oi);
 	void obstacleRemoved(const std::vector<ObstacleChanges> & obstacles);

+ 6 - 6
client/battle/BattleInterfaceClasses.cpp

@@ -567,12 +567,12 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface
 		int text = 304;
 		switch(br.result)
 		{
-		case BattleResult::NORMAL:
+		case EBattleResult::NORMAL:
 			break;
-		case BattleResult::ESCAPE:
+		case EBattleResult::ESCAPE:
 			text = 303;
 			break;
-		case BattleResult::SURRENDER:
+		case EBattleResult::SURRENDER:
 			text = 302;
 			break;
 		default:
@@ -601,14 +601,14 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface
 		std::string videoName = "LBSTART.BIK";
 		switch(br.result)
 		{
-		case BattleResult::NORMAL:
+		case EBattleResult::NORMAL:
 			break;
-		case BattleResult::ESCAPE:
+		case EBattleResult::ESCAPE:
 			musicName = "Music/Retreat Battle";
 			videoName = "RTSTART.BIK";
 			text = 310;
 			break;
-		case BattleResult::SURRENDER:
+		case EBattleResult::SURRENDER:
 			musicName = "Music/Surrender Battle";
 			videoName = "SURRENDER.BIK";
 			text = 309;

+ 3 - 10
client/battle/BattleStacksController.cpp

@@ -31,6 +31,7 @@
 
 #include "../../CCallback.h"
 #include "../../lib/spells/ISpellMechanics.h"
+#include "../../lib/battle/BattleAction.h"
 #include "../../lib/battle/BattleHex.h"
 #include "../../lib/CStack.h"
 #include "../../lib/CondSh.h"
@@ -398,15 +399,7 @@ void BattleStacksController::addNewAnim(BattleAnimation *anim)
 void BattleStacksController::stackRemoved(uint32_t stackID)
 {
 	if (getActiveStack() && getActiveStack()->unitId() == stackID)
-	{
-		BattleAction action;
-		action.side = owner.defendingHeroInstance ? (owner.curInt->playerID == owner.defendingHeroInstance->tempOwner) : false;
-		action.actionType = EActionType::CANCEL;
-		action.stackNumber = getActiveStack()->unitId();
-
-		LOCPLINT->cb->battleMakeUnitAction(action);
 		setActiveStack(nullptr);
-	}
 }
 
 void BattleStacksController::stacksAreAttacked(std::vector<StackAttackedInfo> attackedInfos)
@@ -663,7 +656,7 @@ bool BattleStacksController::shouldRotate(const CStack * stack, const BattleHex
 	return false;
 }
 
-void BattleStacksController::endAction(const BattleAction* action)
+void BattleStacksController::endAction(const BattleAction & action)
 {
 	owner.checkForAnimations();
 
@@ -688,7 +681,7 @@ void BattleStacksController::endAction(const BattleAction* action)
 	removeExpiredColorFilters();
 }
 
-void BattleStacksController::startAction(const BattleAction* action)
+void BattleStacksController::startAction(const BattleAction & action)
 {
 	removeExpiredColorFilters();
 }

+ 2 - 2
client/battle/BattleStacksController.h

@@ -115,8 +115,8 @@ public:
 	void stacksAreAttacked(std::vector<StackAttackedInfo> attackedInfos); //called when a certain amount of stacks has been attacked
 	void stackAttacking(const StackAttackInfo & info); //called when stack with id ID is attacking something on hex dest
 
-	void startAction(const BattleAction* action);
-	void endAction(const BattleAction* action);
+	void startAction(const BattleAction & action);
+	void endAction(const BattleAction & action);
 
 	void deactivateStack(); //copy activeStack to stackToActivate, then set activeStack to nullptr to temporary disable current stack
 

+ 122 - 1
client/lobby/SelectionTab.cpp

@@ -18,6 +18,7 @@
 #include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/Shortcut.h"
+#include "../gui/WindowHandler.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/MiscWidgets.h"
@@ -27,7 +28,9 @@
 #include "../windows/GUIClasses.h"
 #include "../windows/InfoWindows.h"
 #include "../render/CAnimation.h"
+#include "../render/Canvas.h"
 #include "../render/IImage.h"
+#include "../render/Graphics.h"
 
 #include "../../CCallback.h"
 
@@ -37,9 +40,12 @@
 #include "../../lib/GameSettings.h"
 #include "../../lib/filesystem/Filesystem.h"
 #include "../../lib/campaign/CampaignState.h"
+#include "../../lib/mapping/CMap.h"
+#include "../../lib/mapping/CMapService.h"
 #include "../../lib/mapping/CMapInfo.h"
 #include "../../lib/mapping/CMapHeader.h"
 #include "../../lib/mapping/MapFormat.h"
+#include "../../lib/TerrainHandler.h"
 #include "../../lib/serializer/Connection.h"
 
 bool mapSorter::operator()(const std::shared_ptr<ElementInfo> aaa, const std::shared_ptr<ElementInfo> bbb)
@@ -357,7 +363,7 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition)
 		if(curItems[py]->date != "")
 			text += boost::str(boost::format("\r\n\r\n%1%:\r\n%2%") % CGI->generaltexth->translate("vcmi.lobby.creationDate") % curItems[py]->date);
 
-		CRClickPopup::createAndPush(text);
+		GH.windows().createAndPushWindow<CMapInfoTooltipBox>(text, ResourceID(curItems[py]->fileURI), tabType);
 	}
 }
 
@@ -806,6 +812,121 @@ std::unordered_set<ResourceID> SelectionTab::getFiles(std::string dirURI, int re
 	return ret;
 }
 
+SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourceID resource, ESelectionScreen tabType)
+	: CWindowObject(BORDERED | RCLICK_POPUP)
+{
+	drawPlayerElements = tabType == ESelectionScreen::newGame;
+	renderImage = tabType == ESelectionScreen::newGame && settings["lobby"]["mapPreview"].Bool();
+
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
+	std::vector<std::shared_ptr<IImage>> mapLayerImages;
+	if(renderImage)
+		mapLayerImages = createMinimaps(ResourceID(resource.getName(), EResType::MAP), IMAGE_SIZE);
+
+	if(mapLayerImages.size() == 0)
+		renderImage = false;
+
+	pos = Rect(0, 0, 3 * BORDER + 2 * IMAGE_SIZE, 2000);
+
+	auto drawLabel = [&]() {
+		label = std::make_shared<CTextBox>(text, Rect(BORDER, BORDER, BORDER + 2 * IMAGE_SIZE, 350), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE);
+		if(!label->slider)
+			label->resize(Point(BORDER + 2 * IMAGE_SIZE, label->label->textSize.y));
+	};
+	drawLabel();
+
+	int textHeight = std::min(350, label->label->textSize.y);
+	pos.h = BORDER + textHeight + BORDER;
+	if(renderImage)
+		pos.h += IMAGE_SIZE + BORDER;
+	backgroundTexture = std::make_shared<CFilledTexture>("DIBOXBCK", pos);
+	updateShadow();
+
+	drawLabel();
+
+	if(renderImage)
+	{
+		if(mapLayerImages.size() == 1)
+			image1 = std::make_shared<CPicture>(mapLayerImages[0], Point(BORDER + (BORDER + IMAGE_SIZE) / 2, textHeight + 2 * BORDER));
+		else
+		{
+			image1 = std::make_shared<CPicture>(mapLayerImages[0], Point(BORDER, textHeight + 2 * BORDER));
+			image2 = std::make_shared<CPicture>(mapLayerImages[1], Point(BORDER + IMAGE_SIZE + BORDER, textHeight + 2 * BORDER));
+		}
+	}
+
+	center(GH.getCursorPosition()); //center on mouse
+#ifdef VCMI_MOBILE
+	moveBy({0, -pos.h / 2});
+#endif
+	fitToScreen(10);
+}
+
+Canvas SelectionTab::CMapInfoTooltipBox::createMinimapForLayer(std::unique_ptr<CMap> & map, int layer)
+{
+	Canvas canvas = Canvas(Point(map->width, map->height));
+
+	for (int y = 0; y < map->height; ++y)
+		for (int x = 0; x < map->width; ++x)
+		{
+			TerrainTile & tile = map->getTile(int3(x, y, layer));
+
+			ColorRGBA color = tile.terType->minimapUnblocked;
+			if (tile.blocked && (!tile.visitable))
+				color = tile.terType->minimapBlocked;
+
+			if(drawPlayerElements)
+				// if object at tile is owned - it will be colored as its owner
+				for (const CGObjectInstance *obj : tile.blockingObjects)
+				{
+					PlayerColor player = obj->getOwner();
+					if(player == PlayerColor::NEUTRAL)
+					{
+						color = graphics->neutralColor;
+						break;
+					}
+					if (player < PlayerColor::PLAYER_LIMIT)
+					{
+						color = graphics->playerColors[player.getNum()];
+						break;
+					}
+				}
+
+			canvas.drawPoint(Point(x, y), color);
+		}
+	
+	return canvas;
+}
+
+std::vector<std::shared_ptr<IImage>> SelectionTab::CMapInfoTooltipBox::createMinimaps(ResourceID resource, int size)
+{
+	std::vector<std::shared_ptr<IImage>> ret = std::vector<std::shared_ptr<IImage>>();
+
+	CMapService mapService;
+	std::unique_ptr<CMap> map;
+	try
+	{
+		map = mapService.loadMap(resource);
+	}
+	catch (...)
+	{
+		return ret;
+	}
+
+	for(int i = 0; i < (map->twoLevel ? 2 : 1); i++)
+	{
+		Canvas canvas = createMinimapForLayer(map, i);
+		Canvas canvasScaled = Canvas(Point(size, size));
+		canvasScaled.drawScaled(canvas, Point(0, 0), Point(size, size));
+		std::shared_ptr<IImage> img = IImage::createFromSurface(canvasScaled.getInternalSurface());
+		
+		ret.push_back(img);
+	}
+
+	return ret;
+}
+
 SelectionTab::ListItem::ListItem(Point position, std::shared_ptr<CAnimation> iconsFormats, std::shared_ptr<CAnimation> iconsVictory, std::shared_ptr<CAnimation> iconsLoss)
 	: CIntObject(LCLICK, position)
 {

+ 23 - 1
client/lobby/SelectionTab.h

@@ -10,10 +10,15 @@
 #pragma once
 
 #include "CSelectionBase.h"
+VCMI_LIB_NAMESPACE_BEGIN
+class CMap;
+VCMI_LIB_NAMESPACE_END
 #include "../../lib/mapping/CMapInfo.h"
 
 class CSlider;
 class CLabel;
+class CPicture;
+class IImage;
 
 enum ESortBy
 {
@@ -62,6 +67,24 @@ class SelectionTab : public CIntObject
 	std::shared_ptr<CAnimation> iconsVictoryCondition;
 	std::shared_ptr<CAnimation> iconsLossCondition;
 
+	class CMapInfoTooltipBox : public CWindowObject
+	{
+		const int IMAGE_SIZE = 169;
+		const int BORDER = 30;
+
+		bool drawPlayerElements;
+		bool renderImage;
+
+		std::shared_ptr<CFilledTexture> backgroundTexture;
+		std::shared_ptr<CTextBox> label;
+		std::shared_ptr<CPicture> image1;
+		std::shared_ptr<CPicture> image2;
+
+		Canvas createMinimapForLayer(std::unique_ptr<CMap> & map, int layer);
+		std::vector<std::shared_ptr<IImage>> createMinimaps(ResourceID resource, int size);
+	public:
+		CMapInfoTooltipBox(std::string text, ResourceID resource, ESelectionScreen tabType);
+	};
 public:
 	std::vector<std::shared_ptr<ElementInfo>> allItems;
 	std::vector<std::shared_ptr<ElementInfo>> curItems;
@@ -100,7 +123,6 @@ public:
 	void restoreLastSelection();
 
 private:
-
 	std::shared_ptr<CPicture> background;
 	std::shared_ptr<CSlider> slider;
 	std::vector<std::shared_ptr<CButton>> buttonsSortBy;

+ 13 - 1
config/schemas/settings.json

@@ -3,7 +3,7 @@
 {
 	"type" : "object",
 	"$schema" : "http://json-schema.org/draft-04/schema",
-	"required" : [ "general", "video", "adventure", "battle", "input", "server", "logging", "launcher", "gameTweaks" ],
+	"required" : [ "general", "video", "adventure", "battle", "input", "server", "logging", "launcher", "lobby", "gameTweaks" ],
 	"definitions" : {
 		"logLevelEnum" : {
 			"type" : "string",
@@ -526,6 +526,18 @@
 				}
 			}
 		},
+		"lobby" : {
+			"type" : "object",
+			"additionalProperties" : false,
+			"default" : {},
+			"required" : [ "mapPreview" ],
+			"properties" : {
+				"mapPreview" : {
+					"type" : "boolean",
+					"default" : true
+				}
+			}
+		},
 		"gameTweaks" : {
 			"type" : "object",
 			"default" : {},

+ 2 - 5
lib/CGameInterface.h

@@ -9,7 +9,6 @@
  */
 #pragma once
 
-#include "battle/BattleAction.h"
 #include "IGameEventsReceiver.h"
 
 #include "spells/ViewSpellInt.h"
@@ -36,6 +35,7 @@ class CCreatureSet;
 class CArmedInstance;
 class IShipyard;
 class IMarket;
+class BattleAction;
 struct BattleResult;
 struct BattleAttack;
 struct BattleStackAttacked;
@@ -107,10 +107,7 @@ public:
 
 	virtual void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain){};
 
-	virtual std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState)
-	{
-		return std::nullopt;
-	}
+	virtual std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) = 0;
 
 	virtual void saveGame(BinarySerializer & h, const int version) = 0;
 	virtual void loadGame(BinaryDeserializer & h, const int version) = 0;

+ 0 - 1
lib/GameConstants.cpp

@@ -248,7 +248,6 @@ std::ostream & operator<<(std::ostream & os, const EActionType actionType)
 	static const std::map<EActionType, std::string> actionTypeToString =
 	{
 		{EActionType::END_TACTIC_PHASE, "End tactic phase"},
-		{EActionType::INVALID, "Invalid"},
 		{EActionType::NO_ACTION, "No action"},
 		{EActionType::HERO_SPELL, "Hero spell"},
 		{EActionType::WALK, "Walk"},

+ 16 - 8
lib/GameConstants.h

@@ -998,20 +998,21 @@ namespace Date
 	};
 }
 
-enum class EActionType : int32_t
+enum class EActionType : int8_t
 {
-	CANCEL = -3,
-	END_TACTIC_PHASE = -2,
-	INVALID = -1,
-	NO_ACTION = 0,
+	NO_ACTION,
+
+	END_TACTIC_PHASE,
+	RETREAT,
+	SURRENDER,
+
 	HERO_SPELL,
+
 	WALK,
+	WAIT,
 	DEFEND,
-	RETREAT,
-	SURRENDER,
 	WALK_AND_ATTACK,
 	SHOOT,
-	WAIT,
 	CATAPULT,
 	MONSTER_SPELL,
 	BAD_MORALE,
@@ -1379,6 +1380,13 @@ enum class EHealPower : ui8
 	PERMANENT
 };
 
+enum class EBattleResult : int8_t
+{
+	NORMAL = 0,
+	ESCAPE = 1,
+	SURRENDER = 2
+};
+
 // Typedef declarations
 using TExpType = si64;
 using TQuantity = si32;

+ 0 - 1
lib/NetPackVisitor.h

@@ -134,7 +134,6 @@ public:
 	virtual void visitBuildBoat(BuildBoat & pack) {}
 	virtual void visitQueryReply(QueryReply & pack) {}
 	virtual void visitMakeAction(MakeAction & pack) {}
-	virtual void visitMakeCustomAction(MakeCustomAction & pack) {}
 	virtual void visitDigWithHero(DigWithHero & pack) {}
 	virtual void visitCastAdvSpell(CastAdvSpell & pack) {}
 	virtual void visitSaveGame(SaveGame & pack) {}

+ 1 - 21
lib/NetPacks.h

@@ -1524,11 +1524,9 @@ struct DLL_LINKAGE BattleResultAccepted : public CPackForClient
 
 struct DLL_LINKAGE BattleResult : public Query
 {
-	enum EResult { NORMAL = 0, ESCAPE = 1, SURRENDER = 2 };
-
 	void applyFirstCl(CClient * cl);
 
-	EResult result = NORMAL;
+	EBattleResult result = EBattleResult::NORMAL;
 	ui8 winner = 2; //0 - attacker, 1 - defender, [2 - draw (should be possible?)]
 	std::map<ui32, si32> casualties[2]; //first => casualties of attackers - map crid => number
 	TExpType exp[2] = {0, 0}; //exp for attacker and defender
@@ -2515,24 +2513,6 @@ struct DLL_LINKAGE MakeAction : public CPackForServer
 	}
 };
 
-struct DLL_LINKAGE MakeCustomAction : public CPackForServer
-{
-	MakeCustomAction() = default;
-	MakeCustomAction(BattleAction BA)
-		: ba(std::move(BA))
-	{
-	}
-	BattleAction ba;
-
-	virtual void visitTyped(ICPackVisitor & visitor) override;
-
-	template <typename Handler> void serialize(Handler & h, const int version)
-	{
-		h & static_cast<CPackForServer &>(*this);
-		h & ba;
-	}
-};
-
 struct DLL_LINKAGE DigWithHero : public CPackForServer
 {
 	ObjectInstanceID id; //digging hero id

+ 25 - 29
lib/NetPacksLib.cpp

@@ -638,11 +638,6 @@ void MakeAction::visitTyped(ICPackVisitor & visitor)
 	visitor.visitMakeAction(*this);
 }
 
-void MakeCustomAction::visitTyped(ICPackVisitor & visitor)
-{
-	visitor.visitMakeCustomAction(*this);
-}
-
 void DigWithHero::visitTyped(ICPackVisitor & visitor)
 {
 	visitor.visitDigWithHero(*this);
@@ -2294,34 +2289,35 @@ void StartAction::applyGs(CGameState *gs)
 		return;
 	}
 
-	if(ba.actionType != EActionType::HERO_SPELL) //don't check for stack if it's custom action by hero
+	if (ba.isUnitAction())
 	{
-		assert(st);
+		assert(st); // stack must exists for all non-hero actions
+
+		switch(ba.actionType)
+		{
+			case EActionType::DEFEND:
+				st->waiting = false;
+				st->defending = true;
+				st->defendingAnim = true;
+				break;
+			case EActionType::WAIT:
+				st->defendingAnim = false;
+				st->waiting = true;
+				st->waitedThisTurn = true;
+				break;
+			case EActionType::HERO_SPELL: //no change in current stack state
+				break;
+			default: //any active stack action - attack, catapult, heal, spell...
+				st->waiting = false;
+				st->defendingAnim = false;
+				st->movedThisRound = true;
+				break;
+		}
 	}
 	else
 	{
-		gs->curB->sides[ba.side].usedSpellsHistory.emplace_back(ba.actionSubtype);
-	}
-
-	switch(ba.actionType)
-	{
-	case EActionType::DEFEND:
-		st->waiting = false;
-		st->defending = true;
-		st->defendingAnim = true;
-		break;
-	case EActionType::WAIT:
-		st->defendingAnim = false;
-		st->waiting = true;
-		st->waitedThisTurn = true;
-		break;
-	case EActionType::HERO_SPELL: //no change in current stack state
-		break;
-	default: //any active stack action - attack, catapult, heal, spell...
-		st->waiting = false;
-		st->defendingAnim = false;
-		st->movedThisRound = true;
-		break;
+		if(ba.actionType == EActionType::HERO_SPELL)
+			gs->curB->sides[ba.side].usedSpellsHistory.push_back(ba.spell);
 	}
 }
 

+ 42 - 5
lib/battle/BattleAction.cpp

@@ -20,8 +20,7 @@ static const int32_t INVALID_UNIT_ID = -1000;
 BattleAction::BattleAction():
 	side(-1),
 	stackNumber(-1),
-	actionType(EActionType::INVALID),
-	actionSubtype(-1)
+	actionType(EActionType::NO_ACTION)
 {
 }
 
@@ -80,7 +79,7 @@ BattleAction BattleAction::makeCreatureSpellcast(const battle::Unit * stack, con
 {
 	BattleAction ba;
 	ba.actionType = EActionType::MONSTER_SPELL;
-	ba.actionSubtype = spellID;
+	ba.spell = spellID;
 	ba.setTarget(target);
 	ba.side = stack->unitSide();
 	ba.stackNumber = stack->unitId();
@@ -144,7 +143,7 @@ std::string BattleAction::toString() const
 	}
 
 	boost::format fmt("{BattleAction: side '%d', stackNumber '%d', actionType '%s', actionSubtype '%d', target {%s}}");
-	fmt % static_cast<int>(side) % stackNumber % actionTypeStream.str() % actionSubtype % targetStream.str();
+	fmt % static_cast<int>(side) % stackNumber % actionTypeStream.str() % spell.getNum() % targetStream.str();
 	return fmt.str();
 }
 
@@ -183,7 +182,7 @@ battle::Target BattleAction::getTarget(const CBattleInfoCallback * cb) const
 
 void BattleAction::setTarget(const battle::Target & target_)
 {
-    target.clear();
+	target.clear();
 	for(const auto & destination : target_)
 	{
 		if(destination.unitValue == nullptr)
@@ -193,6 +192,44 @@ void BattleAction::setTarget(const battle::Target & target_)
 	}
 }
 
+bool BattleAction::isUnitAction() const
+{
+	static const std::array<EActionType, 9> actions = {
+		EActionType::WALK,
+		EActionType::WAIT,
+		EActionType::DEFEND,
+		EActionType::WALK_AND_ATTACK,
+		EActionType::SHOOT,
+		EActionType::CATAPULT,
+		EActionType::MONSTER_SPELL,
+		EActionType::BAD_MORALE,
+		EActionType::STACK_HEAL
+	};
+
+	return vstd::contains(actions, actionType);
+}
+
+bool BattleAction::isSpellAction() const
+{
+	static const std::array<EActionType, 2> actions = {
+		EActionType::HERO_SPELL,
+		EActionType::MONSTER_SPELL
+	};
+
+	return vstd::contains(actions, actionType);
+}
+
+bool BattleAction::isTacticsAction() const
+{
+	static const std::array<EActionType, 9> actions = {
+		EActionType::WALK,
+		EActionType::END_TACTIC_PHASE,
+		EActionType::RETREAT,
+		EActionType::SURRENDER
+	};
+
+	return vstd::contains(actions, actionType);
+}
 
 std::ostream & operator<<(std::ostream & os, const BattleAction & ba)
 {

+ 5 - 2
lib/battle/BattleAction.h

@@ -28,7 +28,7 @@ public:
 	ui32 stackNumber; //stack ID, -1 left hero, -2 right hero,
 	EActionType actionType; //use ActionType enum for values
 
-	si32 actionSubtype;
+	SpellID spell;
 
 	BattleAction();
 
@@ -43,6 +43,9 @@ public:
 	static BattleAction makeRetreat(ui8 side);
 	static BattleAction makeSurrender(ui8 side);
 
+	bool isTacticsAction() const;
+	bool isUnitAction() const;
+	bool isSpellAction() const;
 	std::string toString() const;
 
 	void aimToHex(const BattleHex & destination);
@@ -56,7 +59,7 @@ public:
 		h & side;
 		h & stackNumber;
 		h & actionType;
-		h & actionSubtype;
+		h & spell;
 		h & target;
 	}
 private:

+ 0 - 1
lib/registerTypes/RegisterTypes.h

@@ -352,7 +352,6 @@ void registerTypesServerPacks(Serializer &s)
 	s.template registerType<CPackForServer, BuildBoat>();
 	s.template registerType<CPackForServer, QueryReply>();
 	s.template registerType<CPackForServer, MakeAction>();
-	s.template registerType<CPackForServer, MakeCustomAction>();
 	s.template registerType<CPackForServer, DigWithHero>();
 	s.template registerType<CPackForServer, CastAdvSpell>();
 	s.template registerType<CPackForServer, CastleTeleportHero>();

Файловите разлики са ограничени, защото са твърде много
+ 2125 - 3955
server/CGameHandler.cpp


+ 17 - 97
server/CGameHandler.h

@@ -11,30 +11,26 @@
 
 #include <vcmi/Environment.h>
 
-#include "../lib/FunctionList.h"
 #include "../lib/IGameCallback.h"
 #include "../lib/battle/CBattleInfoCallback.h"
-#include "../lib/battle/BattleAction.h"
 #include "../lib/LoadProgress.h"
 #include "../lib/ScriptHandler.h"
-#include "CQuery.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-class CGameState;
-struct StartInfo;
-struct BattleResult;
 struct SideInBattle;
-struct BattleAttack;
-struct BattleStackAttacked;
+class IMarket;
+class SpellCastEnvironment;
+class CConnection;
+class CCommanderInstance;
+class EVictoryLossCheckResult;
+
 struct CPack;
-struct Query;
+struct CPackForServer;
+struct NewTurn;
+struct CGarrisonOperationPack;
 struct SetResources;
 struct NewStructures;
-class CGHeroInstance;
-class IMarket;
-
-class SpellCastEnvironment;
 
 #if SCRIPTING_ENABLED
 namespace scripting
@@ -43,16 +39,17 @@ namespace scripting
 }
 #endif
 
-
 template<typename T> class CApplier;
 
 VCMI_LIB_NAMESPACE_END
 
 class HeroPoolProcessor;
-class CGameHandler;
 class CVCMIServer;
 class CBaseForGHApply;
 class PlayerMessageProcessor;
+class BattleProcessor;
+class QueriesProcessor;
+class CObjectVisitQuery;
 
 struct PlayerStatus
 {
@@ -81,33 +78,18 @@ public:
 	}
 };
 
-struct CasualtiesAfterBattle
-{
-	using TStackAndItsNewCount = std::pair<StackLocation, int>;
-	using TSummoned = std::map<CreatureID, TQuantity>;
-	enum {ERASE = -1};
-	const CArmedInstance * army;
-	std::vector<TStackAndItsNewCount> newStackCounts;
-	std::vector<ArtifactLocation> removedWarMachines;
-	TSummoned summoned;
-	ObjectInstanceID heroWithDeadCommander; //TODO: unify stack locations
-
-	CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat);
-	void updateArmy(CGameHandler *gh);
-};
-
 class CGameHandler : public IGameCallback, public CBattleInfoCallback, public Environment
 {
 	CVCMIServer * lobby;
 	std::shared_ptr<CApplier<CBaseForGHApply>> applier;
-	std::unique_ptr<boost::thread> battleThread;
 
 public:
-	boost::recursive_mutex battleActionMutex;
+	using CCallbackBase::setBattle;
 
 	std::unique_ptr<HeroPoolProcessor> heroPool;
+	std::unique_ptr<BattleProcessor> battles;
+	std::unique_ptr<QueriesProcessor> queries;
 
-	using FireShieldInfo = std::vector<std::pair<const CStack *, int64_t>>;
 	//use enums as parameters, because doMove(sth, true, false, true) is not readable
 	enum EGuardLook {CHECK_FOR_GUARDS, IGNORE_GUARDS};
 	enum EVisitDest {VISIT_DEST, DONT_VISIT_DEST};
@@ -121,7 +103,7 @@ public:
 	//queries stuff
 	boost::recursive_mutex gsm;
 	ui32 QID;
-	Queries queries;
+
 
 	SpellCastEnvironment * spellEnv;
 
@@ -136,26 +118,6 @@ public:
 	bool isBlockedByQueries(const CPack *pack, PlayerColor player);
 	bool isAllowedExchange(ObjectInstanceID id1, ObjectInstanceID id2);
 	void giveSpells(const CGTownInstance *t, const CGHeroInstance *h);
-	int moveStack(int stack, BattleHex dest); //returned value - travelled distance
-	void runBattle();
-
-	////used only in endBattle - don't touch elsewhere
-	bool visitObjectAfterVictory;
-	//
-	void endBattle(int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2); //ends battle
-	void endBattleConfirm(const BattleInfo * battleInfo);
-
-	void makeAttack(const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter);
-
-	// damage, drain life & fire shield; returns amount of drained life
-	int64_t applyBattleEffects(BattleAttack & bat, std::shared_ptr<battle::CUnitState> attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary);
-
-	void sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple);
-	void addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple);
-
-	void checkBattleStateChanges();
-	void setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town);
-	void setBattleResult(BattleResult::EResult resultType, int victoriusSide);
 
 	CGameHandler() = default;
 	CGameHandler(CVCMIServer * lobby);
@@ -243,14 +205,6 @@ public:
 	PlayerColor getPlayerAt(std::shared_ptr<CConnection> c) const;
 	bool hasPlayerAt(PlayerColor player, std::shared_ptr<CConnection> c) const;
 
-	void updateGateState();
-	bool makeBattleAction(BattleAction &ba);
-	bool makeAutomaticAction(const CStack *stack, BattleAction &ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack)
-	bool makeCustomAction(BattleAction &ba);
-	void stackEnchantedTrigger(const CStack * stack);
-	void stackTurnTrigger(const CStack *stack);
-
-	void removeObstacle(const CObstacleInstance &obstacle);
 	bool queryReply( QueryID qid, const JsonNode & answer, PlayerColor player );
 	bool buildBoat( ObjectInstanceID objid, PlayerColor player );
 	bool setFormation( ObjectInstanceID hid, ui8 formation );
@@ -285,7 +239,6 @@ public:
 	bool complain(const std::string &problem); //sends message to all clients, prints on the logs and return true
 	void objectVisited( const CGObjectInstance * obj, const CGHeroInstance * h );
 	void objectVisitEnded(const CObjectVisitQuery &query);
-	void engageIntoBattle( PlayerColor player );
 	bool dig(const CGHeroInstance *h);
 	void moveArmy(const CArmedInstance *src, const CArmedInstance *dst, bool allowMerging);
 
@@ -293,7 +246,7 @@ public:
 	{
 		h & QID;
 		h & states;
-		h & finishingBattle;
+		h & battles;
 		h & heroPool;
 		h & getRandomGenerator();
 		h & playerMessages;
@@ -325,39 +278,8 @@ public:
 	void throwAndComplain(CPackForServer * pack, std::string txt);
 	bool isPlayerOwns(CPackForServer * pack, ObjectInstanceID id);
 
-	struct FinishingBattleHelper
-	{
-		FinishingBattleHelper();
-		FinishingBattleHelper(std::shared_ptr<const CBattleQuery> Query, int RemainingBattleQueriesCount);
-		
-		inline bool isDraw() const {return winnerSide == 2;}
-
-		const CGHeroInstance *winnerHero, *loserHero;
-		PlayerColor victor, loser;
-		ui8 winnerSide;
-
-		int remainingBattleQueriesCount;
-
-		template <typename Handler> void serialize(Handler &h, const int version)
-		{
-			h & winnerHero;
-			h & loserHero;
-			h & victor;
-			h & loser;
-			h & winnerSide;
-			h & remainingBattleQueriesCount;
-		}
-	};
-
-	std::unique_ptr<FinishingBattleHelper> finishingBattle;
-
-	void battleAfterLevelUp(const BattleResult &result);
-
 	void run(bool resume);
 	void newTurn();
-	void handleAttackBeforeCasting(bool ranged, const CStack * attacker, const CStack * defender);
-	void handleAfterAttackCasting(bool ranged, const CStack * attacker, const CStack * defender);
-	void attackCasting(bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender);
 	bool sacrificeArtifact(const IMarket * m, const CGHeroInstance * hero, const std::vector<ArtifactPosition> & slot);
 	void spawnWanderingMonsters(CreatureID creatureID);
 
@@ -385,8 +307,6 @@ private:
 	void reinitScripting();
 	void deserializationFix();
 
-
-	void makeStackDoNothing(const CStack * next);
 	void getVictoryLossMessage(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult, InfoWindow & out) const;
 
 	const std::string complainNoCreatures;

+ 26 - 6
server/CMakeLists.txt

@@ -1,11 +1,21 @@
 set(server_SRCS
 		StdInc.cpp
 
+		battles/BattleActionProcessor.cpp
+		battles/BattleFlowProcessor.cpp
+		battles/BattleProcessor.cpp
+		battles/BattleResultProcessor.cpp
+
+		queries/BattleQueries.cpp
+		queries/CQuery.cpp
+		queries/MapQueries.cpp
+		queries/QueriesProcessor.cpp
+
+		processors/HeroPoolProcessor.cpp
+		processors/PlayerMessageProcessor.cpp
+
 		CGameHandler.cpp
-		HeroPoolProcessor.cpp
-		PlayerMessageProcessor.cpp
 		ServerSpellCastEnvironment.cpp
-		CQuery.cpp
 		CVCMIServer.cpp
 		NetPacksServer.cpp
 		NetPacksLobbyServer.cpp
@@ -14,11 +24,21 @@ set(server_SRCS
 set(server_HEADERS
 		StdInc.h
 
+		battles/BattleActionProcessor.h
+		battles/BattleFlowProcessor.h
+		battles/BattleProcessor.h
+		battles/BattleResultProcessor.h
+
+		queries/BattleQueries.h
+		queries/CQuery.h
+		queries/MapQueries.h
+		queries/QueriesProcessor.h
+
+		processors/HeroPoolProcessor.h
+		processors/PlayerMessageProcessor.h
+
 		CGameHandler.h
-		HeroPoolProcessor.h
-		PlayerMessageProcessor.h
 		ServerSpellCastEnvironment.h
-		CQuery.h
 		CVCMIServer.h
 		LobbyNetPackVisitors.h
 		ServerNetPackVisitors.h

+ 0 - 584
server/CQuery.cpp

@@ -1,584 +0,0 @@
-/*
- * CQuery.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 "CQuery.h"
-#include "CGameHandler.h"
-#include "../lib/battle/BattleInfo.h"
-#include "../lib/mapObjects/MiscObjects.h"
-#include "../lib/serializer/Cast.h"
-
-boost::mutex Queries::mx;
-
-template <typename Container>
-std::string formatContainer(const Container & c, std::string delimeter = ", ", std::string opener = "(", std::string closer=")")
-{
-	std::string ret = opener;
-	auto itr = std::begin(c);
-	if(itr != std::end(c))
-	{
-		ret += std::to_string(*itr);
-		while(++itr != std::end(c))
-		{
-			ret += delimeter;
-			ret += std::to_string(*itr);
-		}
-	}
-	ret += closer;
-	return ret;
-}
-
-std::ostream & operator<<(std::ostream & out, const CQuery & query)
-{
-	return out << query.toString();
-}
-
-std::ostream & operator<<(std::ostream & out, QueryPtr query)
-{
-	return out << "[" << query.get() << "] " << query->toString();
-}
-
-CQuery::CQuery(Queries * Owner):
-	owner(Owner)
-{
-	boost::unique_lock<boost::mutex> l(Queries::mx);
-
-	static QueryID QID = QueryID(0);
-
-	queryID = ++QID;
-	logGlobal->trace("Created a new query with id %d", queryID);
-}
-
-
-CQuery::~CQuery()
-{
-	logGlobal->trace("Destructed the query with id %d", queryID);
-}
-
-void CQuery::addPlayer(PlayerColor color)
-{
-	if(color.isValidPlayer())
-		players.push_back(color);
-}
-
-std::string CQuery::toString() const
-{
-	const auto size = players.size();
-	const std::string plural = size > 1 ? "s" : "";
-	std::string names;
-
-	for(size_t i = 0; i < size; i++)
-	{
-		names += boost::to_upper_copy<std::string>(players[i].getStr());
-
-		if(i < size - 2)
-			names += ", ";
-		else if(size > 1 && i == size - 2)
-			names += " and ";
-	}
-	std::string ret = boost::str(boost::format("A query of type '%s' and qid = %d affecting player%s %s")
-		% typeid(*this).name()
-		% queryID 
-		% plural
-		% names
-	);
-	return ret;
-}
-
-bool CQuery::endsByPlayerAnswer() const
-{
-	return false;
-}
-
-void CQuery::onRemoval(PlayerColor color)
-{
-
-}
-
-bool CQuery::blocksPack(const CPack * pack) const
-{
-	return false;
-}
-
-void CQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const
-{
-
-}
-
-void CQuery::onExposure(QueryPtr topQuery)
-{
-	logGlobal->trace("Exposed query with id %d", queryID);
-	owner->popQuery(*this);
-}
-
-void CQuery::onAdding(PlayerColor color)
-{
-
-}
-
-void CQuery::onAdded(PlayerColor color)
-{
-
-}
-
-void CQuery::setReply(const JsonNode & reply)
-{
-
-}
-
-bool CQuery::blockAllButReply(const CPack * pack) const
-{
-	//We accept only query replies from correct player
-	if(auto reply = dynamic_ptr_cast<QueryReply>(pack))
-		return !vstd::contains(players, reply->player);
-
-	return true;
-}
-
-CGhQuery::CGhQuery(CGameHandler * owner):
-	CQuery(&owner->queries), gh(owner)
-{
-
-}
-
-CObjectVisitQuery::CObjectVisitQuery(CGameHandler * owner, const CGObjectInstance * Obj, const CGHeroInstance * Hero, int3 Tile):
-	CGhQuery(owner), visitedObject(Obj), visitingHero(Hero), tile(Tile), removeObjectAfterVisit(false)
-{
-	addPlayer(Hero->tempOwner);
-}
-
-bool CObjectVisitQuery::blocksPack(const CPack *pack) const
-{
-	//During the visit itself ALL actions are blocked.
-	//(However, the visit may trigger a query above that'll pass some.)
-	return true;
-}
-
-void CObjectVisitQuery::onRemoval(PlayerColor color)
-{
-	gh->objectVisitEnded(*this);
-
-	//TODO or should it be destructor?
-	//Can object visit affect 2 players and what would be desired behavior?
-	if(removeObjectAfterVisit)
-		gh->removeObject(visitedObject);
-}
-
-void CObjectVisitQuery::onExposure(QueryPtr topQuery)
-{
-	//Object may have been removed and deleted.
-	if(gh->isValidObject(visitedObject))
-		topQuery->notifyObjectAboutRemoval(*this);
-
-	owner->popIfTop(*this);
-}
-
-void Queries::popQuery(PlayerColor player, QueryPtr query)
-{
-	LOG_TRACE_PARAMS(logGlobal, "player='%s', query='%s'", player % query);
-	if(topQuery(player) != query)
-	{
-		logGlobal->trace("Cannot remove, not a top!");
-		return;
-	}
-
-	queries[player] -= query;
-	auto nextQuery = topQuery(player);
-
-	query->onRemoval(player);
-
-	//Exposure on query below happens only if removal didn't trigger any new query
-	if(nextQuery && nextQuery == topQuery(player))
-		nextQuery->onExposure(query);
-}
-
-void Queries::popQuery(const CQuery &query)
-{
-	LOG_TRACE_PARAMS(logGlobal, "query='%s'", query);
-
-	assert(query.players.size());
-	for(auto player : query.players)
-	{
-		auto top = topQuery(player);
-		if(top.get() == &query)
-			popQuery(top);
-		else
-		{
-			logGlobal->trace("Cannot remove query %s", query.toString());
-			logGlobal->trace("Queries found:");
-			for(auto q : queries[player])
-			{
-				logGlobal->trace(q->toString());
-			}
-		}
-	}
-}
-
-void Queries::popQuery(QueryPtr query)
-{
-	for(auto player : query->players)
-		popQuery(player, query);
-}
-
-void Queries::addQuery(QueryPtr query)
-{
-	for(auto player : query->players)
-		addQuery(player, query);
-
-	for(auto player : query->players)
-		query->onAdded(player);
-}
-
-void Queries::addQuery(PlayerColor player, QueryPtr query)
-{
-	LOG_TRACE_PARAMS(logGlobal, "player='%d', query='%s'", player.getNum() % query);
-	query->onAdding(player);
-	queries[player].push_back(query);
-}
-
-QueryPtr Queries::topQuery(PlayerColor player)
-{
-	return vstd::backOrNull(queries[player]);
-}
-
-void Queries::popIfTop(QueryPtr query)
-{
-	LOG_TRACE_PARAMS(logGlobal, "query='%d'", query);
-	if(!query)
-		logGlobal->error("The query is nullptr! Ignoring.");
-
-	popIfTop(*query);
-}
-
-void Queries::popIfTop(const CQuery & query)
-{
-	for(PlayerColor color : query.players)
-		if(topQuery(color).get() == &query)
-			popQuery(color, topQuery(color));
-}
-
-std::vector<std::shared_ptr<const CQuery>> Queries::allQueries() const
-{
-	std::vector<std::shared_ptr<const CQuery>> ret;
-	for(auto & playerQueries : queries)
-		for(auto & query : playerQueries.second)
-			ret.push_back(query);
-
-	return ret;
-}
-
-std::vector<QueryPtr> Queries::allQueries()
-{
-	//TODO code duplication with const function :(
-	std::vector<QueryPtr> ret;
-	for(auto & playerQueries : queries)
-		for(auto & query : playerQueries.second)
-			ret.push_back(query);
-
-	return ret;
-}
-
-QueryPtr Queries::getQuery(QueryID queryID)
-{
-	for(auto & playerQueries : queries)
-		for(auto & query : playerQueries.second)
-			if(query->queryID == queryID)
-				return query;
-	return nullptr;
-}
-
-void CBattleQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const
-{
-	if(result)
-		objectVisit.visitedObject->battleFinished(objectVisit.visitingHero, *result);
-}
-
-CBattleQuery::CBattleQuery(CGameHandler * owner, const BattleInfo * Bi):
-	CGhQuery(owner)
-{
-	belligerents[0] = Bi->sides[0].armyObject;
-	belligerents[1] = Bi->sides[1].armyObject;
-
-	bi = Bi;
-
-	for(auto & side : bi->sides)
-		addPlayer(side.color);
-}
-
-CBattleQuery::CBattleQuery(CGameHandler * owner):
-	CGhQuery(owner), bi(nullptr)
-{
-	belligerents[0] = belligerents[1] = nullptr;
-}
-
-bool CBattleQuery::blocksPack(const CPack * pack) const
-{
-	const char * name = typeid(*pack).name();
-	return strcmp(name, typeid(MakeAction).name()) && strcmp(name, typeid(MakeCustomAction).name());
-}
-
-void CBattleQuery::onRemoval(PlayerColor color)
-{
-	if(result)
-		gh->battleAfterLevelUp(*result);
-}
-
-void CGarrisonDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const
-{
-	objectVisit.visitedObject->garrisonDialogClosed(objectVisit.visitingHero);
-}
-
-CGarrisonDialogQuery::CGarrisonDialogQuery(CGameHandler * owner, const CArmedInstance * up, const CArmedInstance * down):
-	CDialogQuery(owner)
-{
-	exchangingArmies[0] = up;
-	exchangingArmies[1] = down;
-
-	addPlayer(up->tempOwner);
-	addPlayer(down->tempOwner);
-}
-
-bool CGarrisonDialogQuery::blocksPack(const CPack * pack) const
-{
-	std::set<ObjectInstanceID> ourIds;
-	ourIds.insert(this->exchangingArmies[0]->id);
-	ourIds.insert(this->exchangingArmies[1]->id);
-
-	if(auto stacks = dynamic_ptr_cast<ArrangeStacks>(pack))
-		return !vstd::contains(ourIds, stacks->id1) || !vstd::contains(ourIds, stacks->id2);
-
-	if(auto stacks = dynamic_ptr_cast<BulkSplitStack>(pack))
-		return !vstd::contains(ourIds, stacks->srcOwner);
-
-	if(auto stacks = dynamic_ptr_cast<BulkMergeStacks>(pack))
-		return !vstd::contains(ourIds, stacks->srcOwner);
-
-	if(auto stacks = dynamic_ptr_cast<BulkSmartSplitStack>(pack))
-		return !vstd::contains(ourIds, stacks->srcOwner);
-
-	if(auto stacks = dynamic_ptr_cast<BulkMoveArmy>(pack))
-		return !vstd::contains(ourIds, stacks->srcArmy) || !vstd::contains(ourIds, stacks->destArmy);
-
-	if(auto arts = dynamic_ptr_cast<ExchangeArtifacts>(pack))
-	{
-		if(auto id1 = std::visit(GetEngagedHeroIds(), arts->src.artHolder))
-			if(!vstd::contains(ourIds, *id1))
-				return true;
-
-		if(auto id2 = std::visit(GetEngagedHeroIds(), arts->dst.artHolder))
-			if(!vstd::contains(ourIds, *id2))
-				return true;
-		return false;
-	}
-	if(auto dismiss = dynamic_ptr_cast<DisbandCreature>(pack))
-		return !vstd::contains(ourIds, dismiss->id);
-	
-	if(auto arts = dynamic_ptr_cast<BulkExchangeArtifacts>(pack))
-		return !vstd::contains(ourIds, arts->srcHero) || !vstd::contains(ourIds, arts->dstHero);
-
-	if(auto art = dynamic_ptr_cast<EraseArtifactByClient>(pack))
-	{
-		if (auto id = std::visit(GetEngagedHeroIds(), art->al.artHolder))
-			return !vstd::contains(ourIds, *id);
-	}
-
-	if(auto dismiss = dynamic_ptr_cast<AssembleArtifacts>(pack))
-		return !vstd::contains(ourIds, dismiss->heroID);
-
-	if(auto upgrade = dynamic_ptr_cast<UpgradeCreature>(pack))
-		return !vstd::contains(ourIds, upgrade->id);
-
-	if(auto formation = dynamic_ptr_cast<SetFormation>(pack))
-		return !vstd::contains(ourIds, formation->hid);
-
-	return CDialogQuery::blocksPack(pack);
-}
-	
-CBattleDialogQuery::CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi):
-	CDialogQuery(owner)
-{
-	bi = Bi;
-	
-	for(auto & side : bi->sides)
-		addPlayer(side.color);
-}
-
-void CBattleDialogQuery::onRemoval(PlayerColor color)
-{
-	assert(answer);
-	if(*answer == 1)
-	{
-		gh->startBattlePrimary(bi->sides[0].armyObject, bi->sides[1].armyObject, bi->tile, bi->sides[0].hero, bi->sides[1].hero, bi->creatureBank, bi->town);
-	}
-	else
-	{
-		gh->endBattleConfirm(bi);
-	}
-}
-
-void CBlockingDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const
-{
-	assert(answer);
-	objectVisit.visitedObject->blockingDialogAnswered(objectVisit.visitingHero, *answer);
-}
-
-CBlockingDialogQuery::CBlockingDialogQuery(CGameHandler * owner, const BlockingDialog & bd):
-	CDialogQuery(owner)
-{
-	this->bd = bd;
-	addPlayer(bd.player);
-}
-
-void CTeleportDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const
-{
-	// do not change to dynamic_ptr_cast - SIGSEGV!
-	auto obj = dynamic_cast<const CGTeleport*>(objectVisit.visitedObject);
-	if(obj)
-		obj->teleportDialogAnswered(objectVisit.visitingHero, *answer, td.exits);
-	else
-		logGlobal->error("Invalid instance in teleport query");
-}
-
-CTeleportDialogQuery::CTeleportDialogQuery(CGameHandler * owner, const TeleportDialog & td):
-	CDialogQuery(owner)
-{
-	this->td = td;
-	addPlayer(td.player);
-}
-
-CHeroLevelUpDialogQuery::CHeroLevelUpDialogQuery(CGameHandler * owner, const HeroLevelUp & Hlu, const CGHeroInstance * Hero):
-	CDialogQuery(owner), hero(Hero)
-{
-	hlu = Hlu;
-	addPlayer(hero->tempOwner);
-}
-
-void CHeroLevelUpDialogQuery::onRemoval(PlayerColor color)
-{
-	assert(answer);
-	logGlobal->trace("Completing hero level-up query. %s gains skill %d", hero->getObjectName(), answer.value());
-	gh->levelUpHero(hero, hlu.skills[*answer]);
-}
-
-void CHeroLevelUpDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const
-{
-	objectVisit.visitedObject->heroLevelUpDone(objectVisit.visitingHero);
-}
-
-CCommanderLevelUpDialogQuery::CCommanderLevelUpDialogQuery(CGameHandler * owner, const CommanderLevelUp & Clu, const CGHeroInstance * Hero):
-	CDialogQuery(owner), hero(Hero)
-{
-	clu = Clu;
-	addPlayer(hero->tempOwner);
-}
-
-void CCommanderLevelUpDialogQuery::onRemoval(PlayerColor color)
-{
-	assert(answer);
-	logGlobal->trace("Completing commander level-up query. Commander of hero %s gains skill %s", hero->getObjectName(), answer.value());
-	gh->levelUpCommander(hero->commander, clu.skills[*answer]);
-}
-
-void CCommanderLevelUpDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const
-{
-	objectVisit.visitedObject->heroLevelUpDone(objectVisit.visitingHero);
-}
-
-CDialogQuery::CDialogQuery(CGameHandler * owner):
-	CGhQuery(owner)
-{
-
-}
-
-bool CDialogQuery::endsByPlayerAnswer() const
-{
-	return true;
-}
-
-bool CDialogQuery::blocksPack(const CPack * pack) const
-{
-	return blockAllButReply(pack);
-}
-
-void CDialogQuery::setReply(const JsonNode & reply)
-{
-	if(reply.getType() == JsonNode::JsonType::DATA_INTEGER)
-		answer = reply.Integer();
-}
-
-CHeroMovementQuery::CHeroMovementQuery(CGameHandler * owner, const TryMoveHero & Tmh, const CGHeroInstance * Hero, bool VisitDestAfterVictory):
-	CGhQuery(owner), tmh(Tmh), visitDestAfterVictory(VisitDestAfterVictory), hero(Hero)
-{
-	players.push_back(hero->tempOwner);
-}
-
-void CHeroMovementQuery::onExposure(QueryPtr topQuery)
-{
-	assert(players.size() == 1);
-
-	if(visitDestAfterVictory && hero->tempOwner == players[0]) //hero still alive, so he won with the guard
-		//TODO what if there were H4-like escape? we should also check pos
-	{
-		logGlobal->trace("Hero %s after victory over guard finishes visit to %s", hero->getNameTranslated(), tmh.end.toString());
-		//finish movement
-		visitDestAfterVictory = false;
-		gh->visitObjectOnTile(*gh->getTile(hero->convertToVisitablePos(tmh.end)), hero);
-	}
-
-	owner->popIfTop(*this);
-}
-
-void CHeroMovementQuery::onRemoval(PlayerColor color)
-{
-	PlayerBlocked pb;
-	pb.player = color;
-	pb.reason = PlayerBlocked::ONGOING_MOVEMENT;
-	pb.startOrEnd = PlayerBlocked::BLOCKADE_ENDED;
-	gh->sendAndApply(&pb);
-}
-
-void CHeroMovementQuery::onAdding(PlayerColor color)
-{
-	PlayerBlocked pb;
-	pb.player = color;
-	pb.reason = PlayerBlocked::ONGOING_MOVEMENT;
-	pb.startOrEnd = PlayerBlocked::BLOCKADE_STARTED;
-	gh->sendAndApply(&pb);
-}
-
-CGenericQuery::CGenericQuery(Queries * Owner, PlayerColor color, std::function<void(const JsonNode &)> Callback):
-	CQuery(Owner), callback(Callback)
-{
-	addPlayer(color);
-}
-
-bool CGenericQuery::blocksPack(const CPack * pack) const
-{
-	return blockAllButReply(pack);
-}
-
-bool CGenericQuery::endsByPlayerAnswer() const
-{
-	return true;
-}
-
-void CGenericQuery::onExposure(QueryPtr topQuery)
-{
-	//do nothing
-}
-
-void CGenericQuery::setReply(const JsonNode & reply)
-{
-	this->reply = reply;
-}
-
-void CGenericQuery::onRemoval(PlayerColor color)
-{
-	callback(reply);
-}

+ 0 - 242
server/CQuery.h

@@ -1,242 +0,0 @@
-/*
- * CQuery.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 "../lib/GameConstants.h"
-#include "../lib/int3.h"
-#include "../lib/NetPacks.h"
-#include "JsonNode.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-class CGObjectInstance;
-class CGHeroInstance;
-class CArmedInstance;
-
-VCMI_LIB_NAMESPACE_END
-
-class CGameHandler;
-class CObjectVisitQuery;
-class CQuery;
-class Queries;
-
-using QueryPtr = std::shared_ptr<CQuery>;
-
-// This class represents any kind of prolonged interaction that may need to do something special after it is over.
-// It does not necessarily has to be "query" requiring player action, it can be also used internally within server.
-// Examples:
-// - all kinds of blocking dialog windows
-// - battle
-// - object visit
-// - hero movement
-// Queries can cause another queries, forming a stack of queries for each player. Eg: hero movement -> object visit -> dialog.
-class CQuery
-{
-public:
-	std::vector<PlayerColor> players; //players that are affected (often "blocked") by query
-	QueryID queryID;
-
-	CQuery(Queries * Owner);
-
-
-	virtual bool blocksPack(const CPack *pack) const; //query can block attempting actions by player. Eg. he can't move hero during the battle.
-
-	virtual bool endsByPlayerAnswer() const; //query is removed after player gives answer (like dialogs)
-	virtual void onAdding(PlayerColor color); //called just before query is pushed on stack
-	virtual void onAdded(PlayerColor color); //called right after query is pushed on stack
-	virtual void onRemoval(PlayerColor color); //called after query is removed from stack
-	virtual void onExposure(QueryPtr topQuery);//called when query immediately above is removed and this is exposed (becomes top)
-	virtual std::string toString() const;
-
-	virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const;
-
-	virtual void setReply(const JsonNode & reply);
-
-	virtual ~CQuery();
-protected:
-	Queries * owner;
-	void addPlayer(PlayerColor color);
-	bool blockAllButReply(const CPack * pack) const;
-};
-
-std::ostream &operator<<(std::ostream &out, const CQuery &query);
-std::ostream &operator<<(std::ostream &out, QueryPtr query);
-
-class CGhQuery : public CQuery
-{
-public:
-	CGhQuery(CGameHandler * owner);
-protected:
-	CGameHandler * gh;
-};
-
-//Created when hero visits object.
-//Removed when query above is resolved (or immediately after visit if no queries were created)
-class CObjectVisitQuery : public CGhQuery
-{
-public:
-	const CGObjectInstance *visitedObject;
-	const CGHeroInstance *visitingHero;
-	int3 tile; //may be different than hero pos -> eg. visit via teleport
-	bool removeObjectAfterVisit;
-
-	CObjectVisitQuery(CGameHandler * owner, const CGObjectInstance *Obj, const CGHeroInstance *Hero, int3 Tile);
-
-	virtual bool blocksPack(const CPack *pack) const override;
-	virtual void onRemoval(PlayerColor color) override;
-	virtual void onExposure(QueryPtr topQuery) override;
-};
-
-class CBattleQuery : public CGhQuery
-{
-public:
-	std::array<const CArmedInstance *,2> belligerents;
-	std::array<int, 2> initialHeroMana;
-
-	const BattleInfo *bi;
-	std::optional<BattleResult> result;
-
-	CBattleQuery(CGameHandler * owner);
-	CBattleQuery(CGameHandler * owner, const BattleInfo * Bi); //TODO
-	virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override;
-	virtual bool blocksPack(const CPack *pack) const override;
-	virtual void onRemoval(PlayerColor color) override;
-};
-
-//Created when hero attempts move and something happens
-//(not necessarily position change, could be just an object interaction).
-class CHeroMovementQuery : public CGhQuery
-{
-public:
-	TryMoveHero tmh;
-	bool visitDestAfterVictory; //if hero moved to guarded tile and it should be visited once guard is defeated
-	const CGHeroInstance *hero;
-
-	virtual void onExposure(QueryPtr topQuery) override;
-
-	CHeroMovementQuery(CGameHandler * owner, const TryMoveHero & Tmh, const CGHeroInstance * Hero, bool VisitDestAfterVictory = false);
-	virtual void onAdding(PlayerColor color) override;
-	virtual void onRemoval(PlayerColor color) override;
-};
-
-class CDialogQuery : public CGhQuery
-{
-public:
-	CDialogQuery(CGameHandler * owner);
-	virtual bool endsByPlayerAnswer() const override;
-	virtual bool blocksPack(const CPack *pack) const override;
-	void setReply(const JsonNode & reply) override;
-protected:
-	std::optional<ui32> answer;
-};
-
-class CGarrisonDialogQuery : public CDialogQuery //used also for hero exchange dialogs
-{
-public:
-	std::array<const CArmedInstance *,2> exchangingArmies;
-
-	CGarrisonDialogQuery(CGameHandler * owner, const CArmedInstance *up, const CArmedInstance *down);
-	virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override;
-	virtual bool blocksPack(const CPack *pack) const override;
-};
-
-class CBattleDialogQuery : public CDialogQuery
-{
-public:
-	CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi);
-
-	const BattleInfo * bi;
-
-	virtual void onRemoval(PlayerColor color) override;
-};
-
-//yes/no and component selection dialogs
-class CBlockingDialogQuery : public CDialogQuery
-{
-public:
-	BlockingDialog bd; //copy of pack... debug purposes
-
-	CBlockingDialogQuery(CGameHandler * owner, const BlockingDialog &bd);
-
-	virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override;
-};
-
-class CTeleportDialogQuery : public CDialogQuery
-{
-public:
-	TeleportDialog td; //copy of pack... debug purposes
-
-	CTeleportDialogQuery(CGameHandler * owner, const TeleportDialog &td);
-
-	virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override;
-};
-
-class CHeroLevelUpDialogQuery : public CDialogQuery
-{
-public:
-	CHeroLevelUpDialogQuery(CGameHandler * owner, const HeroLevelUp &Hlu, const CGHeroInstance * Hero);
-
-	virtual void onRemoval(PlayerColor color) override;
-	virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override;
-
-	HeroLevelUp hlu;
-	const CGHeroInstance * hero;
-};
-
-class CCommanderLevelUpDialogQuery : public CDialogQuery
-{
-public:
-	CCommanderLevelUpDialogQuery(CGameHandler * owner, const CommanderLevelUp &Clu, const CGHeroInstance * Hero);
-
-	virtual void onRemoval(PlayerColor color) override;
-	virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override;
-
-	CommanderLevelUp clu;
-	const CGHeroInstance * hero;
-};
-
-class CGenericQuery : public CQuery
-{
-public:
-	CGenericQuery(Queries * Owner, PlayerColor color, std::function<void(const JsonNode &)> Callback);
-
-	bool blocksPack(const CPack * pack) const override;
-	bool endsByPlayerAnswer() const override;
-	void onExposure(QueryPtr topQuery) override;
-	void setReply(const JsonNode & reply) override;
-	void onRemoval(PlayerColor color) override;
-private:
-	std::function<void(const JsonNode &)> callback;
-	JsonNode reply;
-};
-
-class Queries
-{
-private:
-	void addQuery(PlayerColor player, QueryPtr query);
-	void popQuery(PlayerColor player, QueryPtr query);
-
-	std::map<PlayerColor, std::vector<QueryPtr>> queries; //player => stack of queries
-
-public:
-	static boost::mutex mx;
-
-	void addQuery(QueryPtr query);
-	void popQuery(const CQuery &query);
-	void popQuery(QueryPtr query);
-	void popIfTop(const CQuery &query); //removes this query if it is at the top (otherwise, do nothing)
-	void popIfTop(QueryPtr query); //removes this query if it is at the top (otherwise, do nothing)
-
-	QueryPtr topQuery(PlayerColor player);
-
-	std::vector<std::shared_ptr<const CQuery>> allQueries() const;
-	std::vector<QueryPtr> allQueries();
-	QueryPtr getQuery(QueryID queryID);
-	//void removeQuery
-};

+ 1 - 1
server/CVCMIServer.cpp

@@ -36,7 +36,7 @@
 #include "../lib/VCMI_Lib.h"
 #include "../lib/VCMIDirs.h"
 #include "CGameHandler.h"
-#include "PlayerMessageProcessor.h"
+#include "processors/PlayerMessageProcessor.h"
 #include "../lib/mapping/CMapInfo.h"
 #include "../lib/GameConstants.h"
 #include "../lib/logging/CBasicLogConfigurator.h"

+ 8 - 48
server/NetPacksServer.cpp

@@ -11,8 +11,10 @@
 #include "ServerNetPackVisitors.h"
 
 #include "CGameHandler.h"
-#include "HeroPoolProcessor.h"
-#include "PlayerMessageProcessor.h"
+#include "battles/BattleProcessor.h"
+#include "processors/HeroPoolProcessor.h"
+#include "processors/PlayerMessageProcessor.h"
+#include "queries/QueriesProcessor.h"
 
 #include "../lib/IGameCallback.h"
 #include "../lib/mapObjects/CGTownInstance.h"
@@ -47,7 +49,7 @@ void ApplyGhNetPackVisitor::visitEndTurn(EndTurn & pack)
 	}
 
 	gh.throwOnWrongPlayer(&pack, pack.player);
-	if(gh.queries.topQuery(pack.player))
+	if(gh.queries->topQuery(pack.player))
 		gh.throwAndComplain(&pack, "Cannot end turn before resolving queries!");
 
 	gh.states.setFlag(gs.currentPlayer, &PlayerStatus::makingTurn, false);
@@ -280,52 +282,10 @@ void ApplyGhNetPackVisitor::visitQueryReply(QueryReply & pack)
 
 void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack)
 {
-	boost::unique_lock lock(gh.battleActionMutex);
-
-	const BattleInfo * b = gs.curB;
-	if(!b)
-		gh.throwAndComplain(&pack, "Can not make action - there is no battle ongoing!");
-
-	if(b->tacticDistance)
-	{
-		if(pack.ba.actionType != EActionType::WALK && pack.ba.actionType != EActionType::END_TACTIC_PHASE
-			&& pack.ba.actionType != EActionType::RETREAT && pack.ba.actionType != EActionType::SURRENDER)
-			gh.throwAndComplain(&pack, "Can not make actions while in tactics mode!");
-		if(!vstd::contains(gh.connections[b->sides[b->tacticsSide].color], pack.c))
-			gh.throwAndComplain(&pack, "Can not make actions in battles you are not part of!");
-	}
-	else
-	{
-		auto active = b->battleActiveUnit();
-		if(!active)
-			gh.throwAndComplain(&pack, "No active unit in battle!");
-		auto unitOwner = b->battleGetOwner(active);
-		if(!vstd::contains(gh.connections[unitOwner], pack.c))
-			gh.throwAndComplain(&pack, "Can not make actions in battles you are not part of!");
-	}
-
-	result = gh.makeBattleAction(pack.ba);
-}
-
-void ApplyGhNetPackVisitor::visitMakeCustomAction(MakeCustomAction & pack)
-{
-	boost::unique_lock lock(gh.battleActionMutex);
-
-	const BattleInfo * b = gs.curB;
-	if(!b)
-		gh.throwNotAllowedAction(&pack);
-	if(b->tacticDistance)
-		gh.throwNotAllowedAction(&pack);
-	auto active = b->battleActiveUnit();
-	if(!active)
-		gh.throwNotAllowedAction(&pack);
-	auto unitOwner = b->battleGetOwner(active);
-	if(!vstd::contains(gh.connections[unitOwner], pack.c))
-		gh.throwNotAllowedAction(&pack);
-	if(pack.ba.actionType != EActionType::HERO_SPELL)
-		gh.throwNotAllowedAction(&pack);
+	if (!gh.hasPlayerAt(pack.player, pack.c))
+		gh.throwAndComplain(&pack, "No such pack.player!");
 
-	result = gh.makeCustomAction(pack.ba);
+	result = gh.battles->makePlayerBattleAction(pack.player, pack.ba);
 }
 
 void ApplyGhNetPackVisitor::visitDigWithHero(DigWithHero & pack)

+ 1 - 2
server/ServerNetPackVisitors.h

@@ -55,8 +55,7 @@ public:
 	virtual void visitBuildBoat(BuildBoat & pack) override;
 	virtual void visitQueryReply(QueryReply & pack) override;
 	virtual void visitMakeAction(MakeAction & pack) override;
-	virtual void visitMakeCustomAction(MakeCustomAction & pack) override;
 	virtual void visitDigWithHero(DigWithHero & pack) override;
 	virtual void visitCastAdvSpell(CastAdvSpell & pack) override;
 	virtual void visitPlayerMessage(PlayerMessage & pack) override;
-};
+};

+ 9 - 4
server/ServerSpellCastEnvironment.cpp

@@ -8,10 +8,15 @@
  *
  */
 #include "StdInc.h"
-#include "../lib/gameState/CGameState.h"
-#include "CGameHandler.h"
 #include "ServerSpellCastEnvironment.h"
 
+#include "CGameHandler.h"
+#include "queries/QueriesProcessor.h"
+#include "queries/CQuery.h"
+
+#include "../lib/gameState/CGameState.h"
+#include "../lib/NetPacks.h"
+
 ///ServerSpellCastEnvironment
 ServerSpellCastEnvironment::ServerSpellCastEnvironment(CGameHandler * gh)
 	: gh(gh)
@@ -90,8 +95,8 @@ bool ServerSpellCastEnvironment::moveHero(ObjectInstanceID hid, int3 dst, bool t
 
 void ServerSpellCastEnvironment::genericQuery(Query * request, PlayerColor color, std::function<void(const JsonNode&)> callback)
 {
-	auto query = std::make_shared<CGenericQuery>(&gh->queries, color, callback);
+	auto query = std::make_shared<CGenericQuery>(gh->queries.get(), color, callback);
 	request->queryID = query->queryID;
-	gh->queries.addQuery(query);
+	gh->queries->addQuery(query);
 	gh->sendAndApply(request);
 }

+ 1431 - 0
server/battles/BattleActionProcessor.cpp

@@ -0,0 +1,1431 @@
+/*
+ * BattleActionProcessor.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 "BattleActionProcessor.h"
+
+#include "BattleProcessor.h"
+
+#include "../CGameHandler.h"
+
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/CStack.h"
+#include "../../lib/GameSettings.h"
+#include "../../lib/battle/BattleInfo.h"
+#include "../../lib/battle/BattleAction.h"
+#include "../../lib/gameState/CGameState.h"
+#include "../../lib/NetPacks.h"
+#include "../../lib/spells/AbilityCaster.h"
+#include "../../lib/spells/CSpellHandler.h"
+#include "../../lib/spells/ISpellMechanics.h"
+#include "../../lib/spells/Problem.h"
+
+BattleActionProcessor::BattleActionProcessor(BattleProcessor * owner)
+	: owner(owner)
+	, gameHandler(nullptr)
+{
+}
+
+void BattleActionProcessor::setGameHandler(CGameHandler * newGameHandler)
+{
+	gameHandler = newGameHandler;
+}
+
+bool BattleActionProcessor::doEmptyAction(const BattleAction & ba)
+{
+	return true;
+}
+
+bool BattleActionProcessor::doEndTacticsAction(const BattleAction & ba)
+{
+	return true;
+}
+
+bool BattleActionProcessor::doWaitAction(const BattleAction & ba)
+{
+	const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber);
+
+	if (!canStackAct(stack))
+		return false;
+
+	return true;
+}
+
+bool BattleActionProcessor::doRetreatAction(const BattleAction & ba)
+{
+	if (!gameHandler->gameState()->curB->battleCanFlee(gameHandler->gameState()->curB->sides.at(ba.side).color))
+	{
+		gameHandler->complain("Cannot retreat!");
+		return false;
+	}
+
+	owner->setBattleResult(EBattleResult::ESCAPE, !ba.side);
+	return true;
+}
+
+bool BattleActionProcessor::doSurrenderAction(const BattleAction & ba)
+{
+	PlayerColor player = gameHandler->gameState()->curB->sides.at(ba.side).color;
+	int cost = gameHandler->gameState()->curB->battleGetSurrenderCost(player);
+	if (cost < 0)
+	{
+		gameHandler->complain("Cannot surrender!");
+		return false;
+	}
+
+	if (gameHandler->getResource(player, EGameResID::GOLD) < cost)
+	{
+		gameHandler->complain("Not enough gold to surrender!");
+		return false;
+	}
+
+	gameHandler->giveResource(player, EGameResID::GOLD, -cost);
+	owner->setBattleResult(EBattleResult::SURRENDER, !ba.side);
+	return true;
+}
+
+bool BattleActionProcessor::doHeroSpellAction(const BattleAction & ba)
+{
+	const CGHeroInstance *h = gameHandler->gameState()->curB->battleGetFightingHero(ba.side);
+	if (!h)
+	{
+		logGlobal->error("Wrong caster!");
+		return false;
+	}
+
+	const CSpell * s = ba.spell.toSpell();
+	if (!s)
+	{
+		logGlobal->error("Wrong spell id (%d)!", ba.spell.getNum());
+		return false;
+	}
+
+	spells::BattleCast parameters(gameHandler->gameState()->curB, h, spells::Mode::HERO, s);
+
+	spells::detail::ProblemImpl problem;
+
+	auto m = s->battleMechanics(&parameters);
+
+	if(!m->canBeCast(problem))//todo: should we check aimed cast?
+	{
+		logGlobal->warn("Spell cannot be cast!");
+		std::vector<std::string> texts;
+		problem.getAll(texts);
+		for(auto s : texts)
+			logGlobal->warn(s);
+		return false;
+	}
+
+	parameters.cast(gameHandler->spellEnv, ba.getTarget(gameHandler->gameState()->curB));
+
+	return true;
+}
+
+bool BattleActionProcessor::doWalkAction(const BattleAction & ba)
+{
+	const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber);
+	battle::Target target = ba.getTarget(gameHandler->gameState()->curB);
+
+	if (!canStackAct(stack))
+		return false;
+
+	if(target.size() < 1)
+	{
+		gameHandler->complain("Destination required for move action.");
+		return false;
+	}
+
+	int walkedTiles = moveStack(ba.stackNumber, target.at(0).hexValue); //move
+	if (!walkedTiles)
+	{
+		gameHandler->complain("Stack failed movement!");
+		return false;
+	}
+	return true;
+}
+
+bool BattleActionProcessor::doDefendAction(const BattleAction & ba)
+{
+	const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber);
+
+	if (!canStackAct(stack))
+		return false;
+
+	//defensive stance, TODO: filter out spell boosts from bonus (stone skin etc.)
+	SetStackEffect sse;
+	Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, -1, PrimarySkill::DEFENSE, BonusValueType::PERCENT_TO_ALL);
+	Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), -1, PrimarySkill::DEFENSE, BonusValueType::ADDITIVE_VALUE);
+	Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, -1, PrimarySkill::DEFENSE, BonusValueType::ADDITIVE_VALUE);
+
+	BonusList defence = *stack->getBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE));
+	int oldDefenceValue = defence.totalValue();
+
+	defence.push_back(std::make_shared<Bonus>(defenseBonusToAdd));
+	defence.push_back(std::make_shared<Bonus>(bonus2));
+
+	int difference = defence.totalValue() - oldDefenceValue;
+	std::vector<Bonus> buffer;
+	if(difference == 0) //give replacement bonus for creatures not reaching 5 defense points (20% of def becomes 0)
+	{
+		difference = 1;
+		buffer.push_back(alternativeWeakCreatureBonus);
+	}
+	else
+	{
+		buffer.push_back(defenseBonusToAdd);
+	}
+
+	buffer.push_back(bonus2);
+
+	sse.toUpdate.push_back(std::make_pair(ba.stackNumber, buffer));
+	gameHandler->sendAndApply(&sse);
+
+	BattleLogMessage message;
+
+	MetaString text;
+	stack->addText(text, EMetaText::GENERAL_TXT, 120);
+	stack->addNameReplacement(text);
+	text.replaceNumber(difference);
+
+	message.lines.push_back(text);
+
+	gameHandler->sendAndApply(&message);
+	return true;
+}
+
+bool BattleActionProcessor::doAttackAction(const BattleAction & ba)
+{
+	const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber);
+	battle::Target target = ba.getTarget(gameHandler->gameState()->curB);
+
+	if (!canStackAct(stack))
+		return false;
+
+	if(target.size() < 2)
+	{
+		gameHandler->complain("Two destinations required for attack action.");
+		return false;
+	}
+
+	BattleHex attackPos = target.at(0).hexValue;
+	BattleHex destinationTile = target.at(1).hexValue;
+	const CStack * destinationStack = gameHandler->gameState()->curB->battleGetStackByPos(destinationTile, true);
+
+	if(!destinationStack)
+	{
+		gameHandler->complain("Invalid target to attack");
+		return false;
+	}
+
+	BattleHex startingPos = stack->getPosition();
+	int distance = moveStack(ba.stackNumber, attackPos);
+
+	logGlobal->trace("%s will attack %s", stack->nodeName(), destinationStack->nodeName());
+
+	if(stack->getPosition() != attackPos && !(stack->doubleWide() && (stack->getPosition() == attackPos.cloneInDirection(stack->destShiftDir(), false))) )
+	{
+		// we were not able to reach destination tile, nor occupy specified hex
+		// abort attack attempt, but treat this case as legal - we may have stepped onto a quicksands/mine
+		return true;
+	}
+
+	if(destinationStack && stack->unitId() == destinationStack->unitId()) //we should just move, it will be handled by following check
+	{
+		destinationStack = nullptr;
+	}
+
+	if(!destinationStack)
+	{
+		gameHandler->complain("Unit can not attack itself");
+		return false;
+	}
+
+	if(!CStack::isMeleeAttackPossible(stack, destinationStack))
+	{
+		gameHandler->complain("Attack cannot be performed!");
+		return false;
+	}
+
+	//attack
+	int totalAttacks = stack->totalAttacks.getMeleeValue();
+
+	//TODO: move to CUnitState
+	const auto * attackingHero = gameHandler->gameState()->curB->battleGetFightingHero(ba.side);
+	if(attackingHero)
+	{
+		totalAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex());
+	}
+
+	const bool firstStrike = destinationStack->hasBonusOfType(BonusType::FIRST_STRIKE);
+	const bool retaliation = destinationStack->ableToRetaliate();
+	for (int i = 0; i < totalAttacks; ++i)
+	{
+		//first strike
+		if(i == 0 && firstStrike && retaliation)
+		{
+			makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true);
+		}
+
+		//move can cause death, eg. by walking into the moat, first strike can cause death or paralysis/petrification
+		if(stack->alive() && !stack->hasBonusOfType(BonusType::NOT_ACTIVE) && destinationStack->alive())
+		{
+			makeAttack(stack, destinationStack, (i ? 0 : distance), destinationTile, i==0, false, false);//no distance travelled on second attack
+		}
+
+		//counterattack
+		//we check retaliation twice, so if it unblocked during attack it will work only on next attack
+		if(stack->alive()
+			&& !stack->hasBonusOfType(BonusType::BLOCKS_RETALIATION)
+			&& (i == 0 && !firstStrike)
+			&& retaliation && destinationStack->ableToRetaliate())
+		{
+			makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true);
+		}
+	}
+
+	//return
+	if(stack->hasBonusOfType(BonusType::RETURN_AFTER_STRIKE)
+		&& target.size() == 3
+		&& startingPos != stack->getPosition()
+		&& startingPos == target.at(2).hexValue
+		&& stack->alive())
+	{
+		moveStack(ba.stackNumber, startingPos);
+		//NOTE: curStack->unitId() == ba.stackNumber (rev 1431)
+	}
+	return true;
+}
+
+bool BattleActionProcessor::doShootAction(const BattleAction & ba)
+{
+	const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber);
+	battle::Target target = ba.getTarget(gameHandler->gameState()->curB);
+
+	if (!canStackAct(stack))
+		return false;
+
+	if(target.size() < 1)
+	{
+		gameHandler->complain("Destination required for shot action.");
+		return false;
+	}
+
+	auto destination = target.at(0).hexValue;
+
+	const CStack * destinationStack = gameHandler->gameState()->curB->battleGetStackByPos(destination);
+
+	if (!gameHandler->gameState()->curB->battleCanShoot(stack, destination))
+	{
+		gameHandler->complain("Cannot shoot!");
+		return false;
+	}
+
+	if (!destinationStack)
+	{
+		gameHandler->complain("No target to shoot!");
+		return false;
+	}
+
+	makeAttack(stack, destinationStack, 0, destination, true, true, false);
+
+	//ranged counterattack
+	if (destinationStack->hasBonusOfType(BonusType::RANGED_RETALIATION)
+		&& !stack->hasBonusOfType(BonusType::BLOCKS_RANGED_RETALIATION)
+		&& destinationStack->ableToRetaliate()
+		&& gameHandler->gameState()->curB->battleCanShoot(destinationStack, stack->getPosition())
+		&& stack->alive()) //attacker may have died (fire shield)
+	{
+		makeAttack(destinationStack, stack, 0, stack->getPosition(), true, true, true);
+	}
+	//allow more than one additional attack
+
+	int totalRangedAttacks = stack->totalAttacks.getRangedValue();
+
+	//TODO: move to CUnitState
+	const auto * attackingHero = gameHandler->gameState()->curB->battleGetFightingHero(ba.side);
+	if(attackingHero)
+	{
+		totalRangedAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex());
+	}
+
+	for(int i = 1; i < totalRangedAttacks; ++i)
+	{
+		if(
+			stack->alive()
+			&& destinationStack->alive()
+			&& stack->shots.canUse()
+			)
+		{
+			makeAttack(stack, destinationStack, 0, destination, false, true, false);
+		}
+	}
+
+	return true;
+}
+
+bool BattleActionProcessor::doCatapultAction(const BattleAction & ba)
+{
+	const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber);
+	battle::Target target = ba.getTarget(gameHandler->gameState()->curB);
+
+	if (!canStackAct(stack))
+		return false;
+
+	std::shared_ptr<const Bonus> catapultAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::CATAPULT));
+	if(!catapultAbility || catapultAbility->subtype < 0)
+	{
+		gameHandler->complain("We do not know how to shoot :P");
+	}
+	else
+	{
+		const CSpell * spell = SpellID(catapultAbility->subtype).toSpell();
+		spells::BattleCast parameters(gameHandler->gameState()->curB, stack, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can shot infinitely by catapult
+		auto shotLevel = stack->valOfBonuses(Selector::typeSubtype(BonusType::CATAPULT_EXTRA_SHOTS, catapultAbility->subtype));
+		parameters.setSpellLevel(shotLevel);
+		parameters.cast(gameHandler->spellEnv, target);
+	}
+	return true;
+}
+
+bool BattleActionProcessor::doUnitSpellAction(const BattleAction & ba)
+{
+	const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber);
+	battle::Target target = ba.getTarget(gameHandler->gameState()->curB);
+	SpellID spellID = ba.spell;
+
+	if (!canStackAct(stack))
+		return false;
+
+	std::shared_ptr<const Bonus> randSpellcaster = stack->getBonus(Selector::type()(BonusType::RANDOM_SPELLCASTER));
+	std::shared_ptr<const Bonus> spellcaster = stack->getBonus(Selector::typeSubtype(BonusType::SPELLCASTER, spellID));
+
+	//TODO special bonus for genies ability
+	if (randSpellcaster && gameHandler->battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_AIMED) < 0)
+		spellID = gameHandler->battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_GENIE);
+
+	if (spellID < 0)
+		gameHandler->complain("That stack can't cast spells!");
+	else
+	{
+		const CSpell * spell = SpellID(spellID).toSpell();
+		spells::BattleCast parameters(gameHandler->gameState()->curB, stack, spells::Mode::CREATURE_ACTIVE, spell);
+		int32_t spellLvl = 0;
+		if(spellcaster)
+			vstd::amax(spellLvl, spellcaster->val);
+		if(randSpellcaster)
+			vstd::amax(spellLvl, randSpellcaster->val);
+		parameters.setSpellLevel(spellLvl);
+		parameters.cast(gameHandler->spellEnv, target);
+	}
+	return true;
+}
+
+bool BattleActionProcessor::doHealAction(const BattleAction & ba)
+{
+	const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber);
+	battle::Target target = ba.getTarget(gameHandler->gameState()->curB);
+
+	if (!canStackAct(stack))
+		return false;
+
+	if(target.size() < 1)
+	{
+		gameHandler->complain("Destination required for heal action.");
+		return false;
+	}
+
+	const battle::Unit * destStack = nullptr;
+	std::shared_ptr<const Bonus> healerAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::HEALER));
+
+	if(target.at(0).unitValue)
+		destStack = target.at(0).unitValue;
+	else
+		destStack = gameHandler->gameState()->curB->battleGetUnitByPos(target.at(0).hexValue);
+
+	if(stack == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype < 0)
+	{
+		gameHandler->complain("There is either no healer, no destination, or healer cannot heal :P");
+	}
+	else
+	{
+		const CSpell * spell = SpellID(healerAbility->subtype).toSpell();
+		spells::BattleCast parameters(gameHandler->gameState()->curB, stack, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can heal infinitely by first aid tent
+		auto dest = battle::Destination(destStack, target.at(0).hexValue);
+		parameters.setSpellLevel(0);
+		parameters.cast(gameHandler->spellEnv, {dest});
+	}
+	return true;
+}
+
+bool BattleActionProcessor::canStackAct(const CStack * stack)
+{
+	if (!stack)
+	{
+		gameHandler->complain("No such stack!");
+		return false;
+	}
+	if (!stack->alive())
+	{
+		gameHandler->complain("This stack is dead: " + stack->nodeName());
+		return false;
+	}
+
+	if (gameHandler->battleTacticDist())
+	{
+		if (stack && stack->unitSide() != gameHandler->battleGetTacticsSide())
+		{
+			gameHandler->complain("This is not a stack of side that has tactics!");
+			return false;
+		}
+	}
+	else
+	{
+		if (stack->unitId() != gameHandler->gameState()->curB->getActiveStackID())
+		{
+			gameHandler->complain("Action has to be about active stack!");
+			return false;
+		}
+	}
+	return true;
+}
+
+bool BattleActionProcessor::dispatchBattleAction(const BattleAction & ba)
+{
+	switch(ba.actionType)
+	{
+		case EActionType::NO_ACTION:
+			return doEmptyAction(ba);
+		case EActionType::END_TACTIC_PHASE:
+			return doEndTacticsAction(ba);
+		case EActionType::RETREAT:
+			return doRetreatAction(ba);
+		case EActionType::SURRENDER:
+			return doSurrenderAction(ba);
+		case EActionType::HERO_SPELL:
+			return doHeroSpellAction(ba);
+		case EActionType::WALK:
+			return doWalkAction(ba);
+		case EActionType::WAIT:
+			return doWaitAction(ba);
+		case EActionType::DEFEND:
+			return doDefendAction(ba);
+		case EActionType::WALK_AND_ATTACK:
+			return doAttackAction(ba);
+		case EActionType::SHOOT:
+			return doShootAction(ba);
+		case EActionType::CATAPULT:
+			return doCatapultAction(ba);
+		case EActionType::MONSTER_SPELL:
+			return doUnitSpellAction(ba);
+		case EActionType::STACK_HEAL:
+			return doHealAction(ba);
+	}
+	gameHandler->complain("Unrecognized action type received!!");
+	return false;
+}
+
+bool BattleActionProcessor::makeBattleActionImpl(const BattleAction &ba)
+{
+	logGlobal->trace("Making action: %s", ba.toString());
+	const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber);
+
+	StartAction startAction(ba);
+	gameHandler->sendAndApply(&startAction);
+
+	bool result = dispatchBattleAction(ba);
+
+	EndAction endAction;
+	gameHandler->sendAndApply(&endAction);
+
+	if(ba.actionType == EActionType::WAIT || ba.actionType == EActionType::DEFEND || ba.actionType == EActionType::SHOOT || ba.actionType == EActionType::MONSTER_SPELL)
+		gameHandler->handleObstacleTriggersForUnit(*gameHandler->spellEnv, *stack);
+
+	return result;
+}
+
+int BattleActionProcessor::moveStack(int stack, BattleHex dest)
+{
+	int ret = 0;
+
+	const CStack *curStack = gameHandler->battleGetStackByID(stack);
+	const CStack *stackAtEnd = gameHandler->gameState()->curB->battleGetStackByPos(dest);
+
+	assert(curStack);
+	assert(dest < GameConstants::BFIELD_SIZE);
+
+	if (gameHandler->gameState()->curB->tacticDistance)
+	{
+		assert(gameHandler->gameState()->curB->isInTacticRange(dest));
+	}
+
+	auto start = curStack->getPosition();
+	if (start == dest)
+		return 0;
+
+	//initing necessary tables
+	auto accessibility = gameHandler->getAccesibility(curStack);
+	std::set<BattleHex> passed;
+	//Ignore obstacles on starting position
+	passed.insert(curStack->getPosition());
+	if(curStack->doubleWide())
+		passed.insert(curStack->occupiedHex());
+
+	//shifting destination (if we have double wide stack and we can occupy dest but not be exactly there)
+	if(!stackAtEnd && curStack->doubleWide() && !accessibility.accessible(dest, curStack))
+	{
+		BattleHex shifted = dest.cloneInDirection(curStack->destShiftDir(), false);
+
+		if(accessibility.accessible(shifted, curStack))
+			dest = shifted;
+	}
+
+	if((stackAtEnd && stackAtEnd!=curStack && stackAtEnd->alive()) || !accessibility.accessible(dest, curStack))
+	{
+		gameHandler->complain("Given destination is not accessible!");
+		return 0;
+	}
+
+	bool canUseGate = false;
+	auto dbState = gameHandler->gameState()->curB->si.gateState;
+	if(gameHandler->battleGetSiegeLevel() > 0 && curStack->unitSide() == BattleSide::DEFENDER &&
+		dbState != EGateState::DESTROYED &&
+		dbState != EGateState::BLOCKED)
+	{
+		canUseGate = true;
+	}
+
+	std::pair< std::vector<BattleHex>, int > path = gameHandler->gameState()->curB->getPath(start, dest, curStack);
+
+	ret = path.second;
+
+	int creSpeed = curStack->speed(0, true);
+
+	if (gameHandler->gameState()->curB->tacticDistance > 0 && creSpeed > 0)
+		creSpeed = GameConstants::BFIELD_SIZE;
+
+	bool hasWideMoat = vstd::contains_if(gameHandler->battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), false), [](const std::shared_ptr<const CObstacleInstance> & obst)
+	{
+		return obst->obstacleType == CObstacleInstance::MOAT;
+	});
+
+	auto isGateDrawbridgeHex = [&](BattleHex hex) -> bool
+	{
+		if (hasWideMoat && hex == ESiegeHex::GATE_BRIDGE)
+			return true;
+		if (hex == ESiegeHex::GATE_OUTER)
+			return true;
+		if (hex == ESiegeHex::GATE_INNER)
+			return true;
+
+		return false;
+	};
+
+	auto occupyGateDrawbridgeHex = [&](BattleHex hex) -> bool
+	{
+		if (isGateDrawbridgeHex(hex))
+			return true;
+
+		if (curStack->doubleWide())
+		{
+			BattleHex otherHex = curStack->occupiedHex(hex);
+			if (otherHex.isValid() && isGateDrawbridgeHex(otherHex))
+				return true;
+		}
+
+		return false;
+	};
+
+	if (curStack->hasBonusOfType(BonusType::FLYING))
+	{
+		if (path.second <= creSpeed && path.first.size() > 0)
+		{
+			if (canUseGate && dbState != EGateState::OPENED &&
+				occupyGateDrawbridgeHex(dest))
+			{
+				BattleUpdateGateState db;
+				db.state = EGateState::OPENED;
+				gameHandler->sendAndApply(&db);
+			}
+
+			//inform clients about move
+			BattleStackMoved sm;
+			sm.stack = curStack->unitId();
+			std::vector<BattleHex> tiles;
+			tiles.push_back(path.first[0]);
+			sm.tilesToMove = tiles;
+			sm.distance = path.second;
+			sm.teleporting = false;
+			gameHandler->sendAndApply(&sm);
+		}
+	}
+	else //for non-flying creatures
+	{
+		std::vector<BattleHex> tiles;
+		const int tilesToMove = std::max((int)(path.first.size() - creSpeed), 0);
+		int v = (int)path.first.size()-1;
+		path.first.push_back(start);
+
+		// check if gate need to be open or closed at some point
+		BattleHex openGateAtHex, gateMayCloseAtHex;
+		if (canUseGate)
+		{
+			for (int i = (int)path.first.size()-1; i >= 0; i--)
+			{
+				auto needOpenGates = [&](BattleHex hex) -> bool
+				{
+					if (hasWideMoat && hex == ESiegeHex::GATE_BRIDGE)
+						return true;
+					if (hex == ESiegeHex::GATE_BRIDGE && i-1 >= 0 && path.first[i-1] == ESiegeHex::GATE_OUTER)
+						return true;
+					else if (hex == ESiegeHex::GATE_OUTER || hex == ESiegeHex::GATE_INNER)
+						return true;
+
+					return false;
+				};
+
+				auto hex = path.first[i];
+				if (!openGateAtHex.isValid() && dbState != EGateState::OPENED)
+				{
+					if (needOpenGates(hex))
+						openGateAtHex = path.first[i+1];
+
+					//TODO we need find batter way to handle double-wide stacks
+					//currently if only second occupied stack part is standing on gate / bridge hex then stack will start to wait for bridge to lower before it's needed. Though this is just a visual bug.
+					if (curStack->doubleWide())
+					{
+						BattleHex otherHex = curStack->occupiedHex(hex);
+						if (otherHex.isValid() && needOpenGates(otherHex))
+							openGateAtHex = path.first[i+2];
+					}
+
+					//gate may be opened and then closed during stack movement, but not other way around
+					if (openGateAtHex.isValid())
+						dbState = EGateState::OPENED;
+				}
+
+				if (!gateMayCloseAtHex.isValid() && dbState != EGateState::CLOSED)
+				{
+					if (hex == ESiegeHex::GATE_INNER && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_OUTER)
+					{
+						gateMayCloseAtHex = path.first[i-1];
+					}
+					if (hasWideMoat)
+					{
+						if (hex == ESiegeHex::GATE_BRIDGE && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_OUTER)
+						{
+							gateMayCloseAtHex = path.first[i-1];
+						}
+						else if (hex == ESiegeHex::GATE_OUTER && i-1 >= 0 &&
+							path.first[i-1] != ESiegeHex::GATE_INNER &&
+							path.first[i-1] != ESiegeHex::GATE_BRIDGE)
+						{
+							gateMayCloseAtHex = path.first[i-1];
+						}
+					}
+					else if (hex == ESiegeHex::GATE_OUTER && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_INNER)
+					{
+						gateMayCloseAtHex = path.first[i-1];
+					}
+				}
+			}
+		}
+
+		bool stackIsMoving = true;
+
+		while(stackIsMoving)
+		{
+			if (v<tilesToMove)
+			{
+				logGlobal->error("Movement terminated abnormally");
+				break;
+			}
+
+			bool gateStateChanging = false;
+			//special handling for opening gate on from starting hex
+			if (openGateAtHex.isValid() && openGateAtHex == start)
+				gateStateChanging = true;
+			else
+			{
+				for (bool obstacleHit = false; (!obstacleHit) && (!gateStateChanging) && (v >= tilesToMove); --v)
+				{
+					BattleHex hex = path.first[v];
+					tiles.push_back(hex);
+
+					if ((openGateAtHex.isValid() && openGateAtHex == hex) ||
+						(gateMayCloseAtHex.isValid() && gateMayCloseAtHex == hex))
+					{
+						gateStateChanging = true;
+					}
+
+					//if we walked onto something, finalize this portion of stack movement check into obstacle
+					if(!gameHandler->battleGetAllObstaclesOnPos(hex, false).empty())
+						obstacleHit = true;
+
+					if (curStack->doubleWide())
+					{
+						BattleHex otherHex = curStack->occupiedHex(hex);
+						//two hex creature hit obstacle by backside
+						auto obstacle2 = gameHandler->battleGetAllObstaclesOnPos(otherHex, false);
+						if(otherHex.isValid() && !obstacle2.empty())
+							obstacleHit = true;
+					}
+					if(!obstacleHit)
+						passed.insert(hex);
+				}
+			}
+
+			if (!tiles.empty())
+			{
+				//commit movement
+				BattleStackMoved sm;
+				sm.stack = curStack->unitId();
+				sm.distance = path.second;
+				sm.teleporting = false;
+				sm.tilesToMove = tiles;
+				gameHandler->sendAndApply(&sm);
+				tiles.clear();
+			}
+
+			//we don't handle obstacle at the destination tile -> it's handled separately in the if at the end
+			if (curStack->getPosition() != dest)
+			{
+				if(stackIsMoving && start != curStack->getPosition())
+				{
+					stackIsMoving = gameHandler->handleObstacleTriggersForUnit(*gameHandler->spellEnv, *curStack, passed);
+					passed.insert(curStack->getPosition());
+					if(curStack->doubleWide())
+						passed.insert(curStack->occupiedHex());
+				}
+				if (gateStateChanging)
+				{
+					if (curStack->getPosition() == openGateAtHex)
+					{
+						openGateAtHex = BattleHex();
+						//only open gate if stack is still alive
+						if (curStack->alive())
+						{
+							BattleUpdateGateState db;
+							db.state = EGateState::OPENED;
+							gameHandler->sendAndApply(&db);
+						}
+					}
+					else if (curStack->getPosition() == gateMayCloseAtHex)
+					{
+						gateMayCloseAtHex = BattleHex();
+						owner->updateGateState();
+					}
+				}
+			}
+			else
+				//movement finished normally: we reached destination
+				stackIsMoving = false;
+		}
+	}
+	//handle last hex separately for deviation
+	if (VLC->settings()->getBoolean(EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES))
+	{
+		if (dest == battle::Unit::occupiedHex(start, curStack->doubleWide(), curStack->unitSide())
+			|| start == battle::Unit::occupiedHex(dest, curStack->doubleWide(), curStack->unitSide()))
+			passed.clear(); //Just empty passed, obstacles will handled automatically
+	}
+	//handling obstacle on the final field (separate, because it affects both flying and walking stacks)
+	gameHandler->handleObstacleTriggersForUnit(*gameHandler->spellEnv, *curStack, passed);
+
+	return ret;
+}
+
+void BattleActionProcessor::makeAttack(const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter)
+{
+	if(first && !counter)
+		handleAttackBeforeCasting(ranged, attacker, defender);
+
+	FireShieldInfo fireShield;
+	BattleAttack bat;
+	BattleLogMessage blm;
+	bat.stackAttacking = attacker->unitId();
+	bat.tile = targetHex;
+
+	std::shared_ptr<battle::CUnitState> attackerState = attacker->acquireState();
+
+	if(ranged)
+		bat.flags |= BattleAttack::SHOT;
+	if(counter)
+		bat.flags |= BattleAttack::COUNTER;
+
+	const int attackerLuck = attacker->luckVal();
+
+	if(attackerLuck > 0)
+	{
+		auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_LUCK_DICE);
+		size_t diceIndex = std::min<size_t>(diceSize.size() - 1, attackerLuck);
+
+		if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1)
+			bat.flags |= BattleAttack::LUCKY;
+	}
+
+	if(attackerLuck < 0)
+	{
+		auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_BAD_LUCK_DICE);
+		size_t diceIndex = std::min<size_t>(diceSize.size() - 1, -attackerLuck);
+
+		if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1)
+			bat.flags |= BattleAttack::UNLUCKY;
+	}
+
+	if (gameHandler->getRandomGenerator().nextInt(99) < attacker->valOfBonuses(BonusType::DOUBLE_DAMAGE_CHANCE))
+	{
+		bat.flags |= BattleAttack::DEATH_BLOW;
+	}
+
+	const auto * owner = gameHandler->gameState()->curB->getHero(attacker->unitOwner());
+	if(owner)
+	{
+		int chance = owner->valOfBonuses(BonusType::BONUS_DAMAGE_CHANCE, attacker->creatureIndex());
+		if (chance > gameHandler->getRandomGenerator().nextInt(99))
+			bat.flags |= BattleAttack::BALLISTA_DOUBLE_DMG;
+	}
+
+	int64_t drainedLife = 0;
+
+	// only primary target
+	if(defender->alive())
+		drainedLife += applyBattleEffects(bat, attackerState, fireShield, defender, distance, false);
+
+	//multiple-hex normal attack
+	std::set<const CStack*> attackedCreatures = gameHandler->gameState()->curB->getAttackedCreatures(attacker, targetHex, bat.shot()); //creatures other than primary target
+	for(const CStack * stack : attackedCreatures)
+	{
+		if(stack != defender && stack->alive()) //do not hit same stack twice
+			drainedLife += applyBattleEffects(bat, attackerState, fireShield, stack, distance, true);
+	}
+
+	std::shared_ptr<const Bonus> bonus = attacker->getBonusLocalFirst(Selector::type()(BonusType::SPELL_LIKE_ATTACK));
+	if(bonus && ranged) //TODO: make it work in melee?
+	{
+		//this is need for displaying hit animation
+		bat.flags |= BattleAttack::SPELL_LIKE;
+		bat.spellID = SpellID(bonus->subtype);
+
+		//TODO: should spell override creature`s projectile?
+
+		auto spell = bat.spellID.toSpell();
+
+		battle::Target target;
+		target.emplace_back(defender, targetHex);
+
+		spells::BattleCast event(gameHandler->gameState()->curB, attacker, spells::Mode::SPELL_LIKE_ATTACK, spell);
+		event.setSpellLevel(bonus->val);
+
+		auto attackedCreatures = spell->battleMechanics(&event)->getAffectedStacks(target);
+
+		//TODO: get exact attacked hex for defender
+
+		for(const CStack * stack : attackedCreatures)
+		{
+			if(stack != defender && stack->alive()) //do not hit same stack twice
+			{
+				drainedLife += applyBattleEffects(bat, attackerState, fireShield, stack, distance, true);
+			}
+		}
+
+		//now add effect info for all attacked stacks
+		for (BattleStackAttacked & bsa : bat.bsa)
+		{
+			if (bsa.attackerID == attacker->unitId()) //this is our attack and not f.e. fire shield
+			{
+				//this is need for displaying affect animation
+				bsa.flags |= BattleStackAttacked::SPELL_EFFECT;
+				bsa.spellID = SpellID(bonus->subtype);
+			}
+		}
+	}
+
+	attackerState->afterAttack(ranged, counter);
+
+	{
+		UnitChanges info(attackerState->unitId(), UnitChanges::EOperation::RESET_STATE);
+		attackerState->save(info.data);
+		bat.attackerChanges.changedStacks.push_back(info);
+	}
+
+	if (drainedLife > 0)
+		bat.flags |= BattleAttack::LIFE_DRAIN;
+
+	gameHandler->sendAndApply(&bat);
+
+	{
+		const bool multipleTargets = bat.bsa.size() > 1;
+
+		int64_t totalDamage = 0;
+		int32_t totalKills = 0;
+
+		for(const BattleStackAttacked & bsa : bat.bsa)
+		{
+			totalDamage += bsa.damageAmount;
+			totalKills += bsa.killedAmount;
+		}
+
+		{
+			MetaString text;
+			attacker->addText(text, EMetaText::GENERAL_TXT, 376);
+			attacker->addNameReplacement(text);
+			text.replaceNumber(totalDamage);
+			blm.lines.push_back(text);
+		}
+
+		addGenericKilledLog(blm, defender, totalKills, multipleTargets);
+	}
+
+	// drain life effect (as well as log entry) must be applied after the attack
+	if(drainedLife > 0)
+	{
+		MetaString text;
+		attackerState->addText(text, EMetaText::GENERAL_TXT, 361);
+		attackerState->addNameReplacement(text, false);
+		text.replaceNumber(drainedLife);
+		defender->addNameReplacement(text, true);
+		blm.lines.push_back(std::move(text));
+	}
+
+	if(!fireShield.empty())
+	{
+		//todo: this should be "virtual" spell instead, we only need fire spell school bonus here
+		const CSpell * fireShieldSpell = SpellID(SpellID::FIRE_SHIELD).toSpell();
+		int64_t totalDamage = 0;
+
+		for(const auto & item : fireShield)
+		{
+			const CStack * actor = item.first;
+			int64_t rawDamage = item.second;
+
+			const CGHeroInstance * actorOwner = gameHandler->gameState()->curB->getHero(actor->unitOwner());
+
+			if(actorOwner)
+			{
+				rawDamage = fireShieldSpell->adjustRawDamage(actorOwner, attacker, rawDamage);
+			}
+			else
+			{
+				rawDamage = fireShieldSpell->adjustRawDamage(actor, attacker, rawDamage);
+			}
+
+			totalDamage+=rawDamage;
+			//FIXME: add custom effect on actor
+		}
+
+		if (totalDamage > 0)
+		{
+			BattleStackAttacked bsa;
+
+			bsa.flags |= BattleStackAttacked::FIRE_SHIELD;
+			bsa.stackAttacked = attacker->unitId(); //invert
+			bsa.attackerID = defender->unitId();
+			bsa.damageAmount = totalDamage;
+			attacker->prepareAttacked(bsa, gameHandler->getRandomGenerator());
+
+			StacksInjured pack;
+			pack.stacks.push_back(bsa);
+			gameHandler->sendAndApply(&pack);
+
+			// TODO: this is already implemented in Damage::describeEffect()
+			{
+				MetaString text;
+				text.appendLocalString(EMetaText::GENERAL_TXT, 376);
+				text.replaceLocalString(EMetaText::SPELL_NAME, SpellID::FIRE_SHIELD);
+				text.replaceNumber(totalDamage);
+				blm.lines.push_back(std::move(text));
+			}
+			addGenericKilledLog(blm, attacker, bsa.killedAmount, false);
+		}
+	}
+
+	gameHandler->sendAndApply(&blm);
+
+	handleAfterAttackCasting(ranged, attacker, defender);
+}
+
+void BattleActionProcessor::attackCasting(bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender)
+{
+	if(attacker->hasBonusOfType(attackMode))
+	{
+		std::set<SpellID> spellsToCast;
+		TConstBonusListPtr spells = attacker->getBonuses(Selector::type()(attackMode));
+		for(const auto & sf : *spells)
+		{
+			spellsToCast.insert(SpellID(sf->subtype));
+		}
+		for(SpellID spellID : spellsToCast)
+		{
+			bool castMe = false;
+			if(!defender->alive())
+			{
+				logGlobal->debug("attackCasting: all attacked creatures have been killed");
+				return;
+			}
+			int32_t spellLevel = 0;
+			TConstBonusListPtr spellsByType = attacker->getBonuses(Selector::typeSubtype(attackMode, spellID));
+			for(const auto & sf : *spellsByType)
+			{
+				int meleeRanged;
+				if(sf->additionalInfo.size() < 2)
+				{
+					// legacy format
+					vstd::amax(spellLevel, sf->additionalInfo[0] % 1000);
+					meleeRanged = sf->additionalInfo[0] / 1000;
+				}
+				else
+				{
+					vstd::amax(spellLevel, sf->additionalInfo[0]);
+					meleeRanged = sf->additionalInfo[1];
+				}
+				if (meleeRanged == 0 || (meleeRanged == 1 && ranged) || (meleeRanged == 2 && !ranged))
+					castMe = true;
+			}
+			int chance = attacker->valOfBonuses((Selector::typeSubtype(attackMode, spellID)));
+			vstd::amin(chance, 100);
+
+			const CSpell * spell = SpellID(spellID).toSpell();
+			spells::AbilityCaster caster(attacker, spellLevel);
+
+			spells::Target target;
+			target.emplace_back(defender);
+
+			spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell);
+
+			auto m = spell->battleMechanics(&parameters);
+
+			spells::detail::ProblemImpl ignored;
+
+			if(!m->canBeCastAt(target, ignored))
+				continue;
+
+			//check if spell should be cast (probability handling)
+			if(gameHandler->getRandomGenerator().nextInt(99) >= chance)
+				continue;
+
+			//casting
+			if(castMe)
+			{
+				parameters.cast(gameHandler->spellEnv, target);
+			}
+		}
+	}
+}
+
+void BattleActionProcessor::handleAttackBeforeCasting(bool ranged, const CStack * attacker, const CStack * defender)
+{
+	attackCasting(ranged, BonusType::SPELL_BEFORE_ATTACK, attacker, defender); //no death stare / acid breath needed?
+}
+
+void BattleActionProcessor::handleAfterAttackCasting(bool ranged, const CStack * attacker, const CStack * defender)
+{
+	if(!attacker->alive() || !defender->alive()) // can be already dead
+		return;
+
+	attackCasting(ranged, BonusType::SPELL_AFTER_ATTACK, attacker, defender);
+
+	if(!defender->alive())
+	{
+		//don't try death stare or acid breath on dead stack (crash!)
+		return;
+	}
+
+	if(attacker->hasBonusOfType(BonusType::DEATH_STARE))
+	{
+		// mechanics of Death Stare as in H3:
+		// each gorgon have 10% chance to kill (counted separately in H3) -> binomial distribution
+		//original formula x = min(x, (gorgons_count + 9)/10);
+
+		double chanceToKill = attacker->valOfBonuses(BonusType::DEATH_STARE, 0) / 100.0f;
+		vstd::amin(chanceToKill, 1); //cap at 100%
+
+		std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill);
+
+		int staredCreatures = distribution(gameHandler->getRandomGenerator().getStdGenerator());
+
+		double cap = 1 / std::max(chanceToKill, (double)(0.01));//don't divide by 0
+		int maxToKill = static_cast<int>((attacker->getCount() + cap - 1) / cap); //not much more than chance * count
+		vstd::amin(staredCreatures, maxToKill);
+
+		staredCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, 1)) / defender->level();
+		if(staredCreatures)
+		{
+			//TODO: death stare was not originally available for multiple-hex attacks, but...
+			const CSpell * spell = SpellID(SpellID::DEATH_STARE).toSpell();
+
+			spells::AbilityCaster caster(attacker, 0);
+
+			spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell);
+			spells::Target target;
+			target.emplace_back(defender);
+			parameters.setEffectValue(staredCreatures);
+			parameters.cast(gameHandler->spellEnv, target);
+		}
+	}
+
+	if(!defender->alive())
+		return;
+
+	int64_t acidDamage = 0;
+	TConstBonusListPtr acidBreath = attacker->getBonuses(Selector::type()(BonusType::ACID_BREATH));
+	for(const auto & b : *acidBreath)
+	{
+		if(b->additionalInfo[0] > gameHandler->getRandomGenerator().nextInt(99))
+			acidDamage += b->val;
+	}
+
+	if(acidDamage > 0)
+	{
+		const CSpell * spell = SpellID(SpellID::ACID_BREATH_DAMAGE).toSpell();
+
+		spells::AbilityCaster caster(attacker, 0);
+
+		spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell);
+		spells::Target target;
+		target.emplace_back(defender);
+
+		parameters.setEffectValue(acidDamage * attacker->getCount());
+		parameters.cast(gameHandler->spellEnv, target);
+	}
+
+
+	if(!defender->alive())
+		return;
+
+	if(attacker->hasBonusOfType(BonusType::TRANSMUTATION) && defender->isLiving()) //transmutation mechanics, similar to WoG werewolf ability
+	{
+		double chanceToTrigger = attacker->valOfBonuses(BonusType::TRANSMUTATION) / 100.0f;
+		vstd::amin(chanceToTrigger, 1); //cap at 100%
+
+		if(gameHandler->getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger)
+			return;
+
+		int bonusAdditionalInfo = attacker->getBonus(Selector::type()(BonusType::TRANSMUTATION))->additionalInfo[0];
+
+		if(defender->unitType()->getId() == bonusAdditionalInfo ||
+			(bonusAdditionalInfo == CAddInfo::NONE && defender->unitType()->getId() == attacker->unitType()->getId()))
+			return;
+
+		battle::UnitInfo resurrectInfo;
+		resurrectInfo.id = gameHandler->gameState()->curB->battleNextUnitId();
+		resurrectInfo.summoned = false;
+		resurrectInfo.position = defender->getPosition();
+		resurrectInfo.side = defender->unitSide();
+
+		if(bonusAdditionalInfo != CAddInfo::NONE)
+			resurrectInfo.type = CreatureID(bonusAdditionalInfo);
+		else
+			resurrectInfo.type = attacker->creatureId();
+
+		if(attacker->hasBonusOfType((BonusType::TRANSMUTATION), 0))
+			resurrectInfo.count = std::max((defender->getCount() * defender->getMaxHealth()) / resurrectInfo.type.toCreature()->getMaxHealth(), 1u);
+		else if (attacker->hasBonusOfType((BonusType::TRANSMUTATION), 1))
+			resurrectInfo.count = defender->getCount();
+		else
+			return; //wrong subtype
+
+		BattleUnitsChanged addUnits;
+		addUnits.changedStacks.emplace_back(resurrectInfo.id, UnitChanges::EOperation::ADD);
+		resurrectInfo.save(addUnits.changedStacks.back().data);
+
+		BattleUnitsChanged removeUnits;
+		removeUnits.changedStacks.emplace_back(defender->unitId(), UnitChanges::EOperation::REMOVE);
+		gameHandler->sendAndApply(&removeUnits);
+		gameHandler->sendAndApply(&addUnits);
+	}
+
+	if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 0) || attacker->hasBonusOfType(BonusType::DESTRUCTION, 1))
+	{
+		double chanceToTrigger = 0;
+		int amountToDie = 0;
+
+		if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 0)) //killing by percentage
+		{
+			chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, 0) / 100.0f;
+			int percentageToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(0)))->additionalInfo[0];
+			amountToDie = static_cast<int>(defender->getCount() * percentageToDie * 0.01f);
+		}
+		else if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 1)) //killing by count
+		{
+			chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, 1) / 100.0f;
+			amountToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(1)))->additionalInfo[0];
+		}
+
+		vstd::amin(chanceToTrigger, 1); //cap trigger chance at 100%
+
+		if(gameHandler->getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger)
+			return;
+
+		BattleStackAttacked bsa;
+		bsa.attackerID = -1;
+		bsa.stackAttacked = defender->unitId();
+		bsa.damageAmount = amountToDie * defender->getMaxHealth();
+		bsa.flags = BattleStackAttacked::SPELL_EFFECT;
+		bsa.spellID = SpellID::SLAYER;
+		defender->prepareAttacked(bsa, gameHandler->getRandomGenerator());
+
+		StacksInjured si;
+		si.stacks.push_back(bsa);
+
+		gameHandler->sendAndApply(&si);
+		sendGenericKilledLog(defender, bsa.killedAmount, false);
+	}
+}
+
+int64_t BattleActionProcessor::applyBattleEffects(BattleAttack & bat, std::shared_ptr<battle::CUnitState> attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary)
+{
+	BattleStackAttacked bsa;
+	if(secondary)
+		bsa.flags |= BattleStackAttacked::SECONDARY; //all other targets do not suffer from spells & spell-like abilities
+
+	bsa.attackerID = attackerState->unitId();
+	bsa.stackAttacked = def->unitId();
+	{
+		BattleAttackInfo bai(attackerState.get(), def, distance, bat.shot());
+
+		bai.deathBlow = bat.deathBlow();
+		bai.doubleDamage = bat.ballistaDoubleDmg();
+		bai.luckyStrike  = bat.lucky();
+		bai.unluckyStrike  = bat.unlucky();
+
+		auto range = gameHandler->gameState()->curB->calculateDmgRange(bai);
+		bsa.damageAmount = gameHandler->gameState()->curB->getActualDamage(range.damage, attackerState->getCount(), gameHandler->getRandomGenerator());
+		CStack::prepareAttacked(bsa, gameHandler->getRandomGenerator(), bai.defender->acquireState()); //calculate casualties
+	}
+
+	int64_t drainedLife = 0;
+
+	//life drain handling
+	if(attackerState->hasBonusOfType(BonusType::LIFE_DRAIN) && def->isLiving())
+	{
+		int64_t toHeal = bsa.damageAmount * attackerState->valOfBonuses(BonusType::LIFE_DRAIN) / 100;
+		attackerState->heal(toHeal, EHealLevel::RESURRECT, EHealPower::PERMANENT);
+		drainedLife += toHeal;
+	}
+
+	//soul steal handling
+	if(attackerState->hasBonusOfType(BonusType::SOUL_STEAL) && def->isLiving())
+	{
+		//we can have two bonuses - one with subtype 0 and another with subtype 1
+		//try to use permanent first, use only one of two
+		for(si32 subtype = 1; subtype >= 0; subtype--)
+		{
+			if(attackerState->hasBonusOfType(BonusType::SOUL_STEAL, subtype))
+			{
+				int64_t toHeal = bsa.killedAmount * attackerState->valOfBonuses(BonusType::SOUL_STEAL, subtype) * attackerState->getMaxHealth();
+				attackerState->heal(toHeal, EHealLevel::OVERHEAL, ((subtype == 0) ? EHealPower::ONE_BATTLE : EHealPower::PERMANENT));
+				drainedLife += toHeal;
+				break;
+			}
+		}
+	}
+	bat.bsa.push_back(bsa); //add this stack to the list of victims after drain life has been calculated
+
+	//fire shield handling
+	if(!bat.shot() &&
+		!def->isClone() &&
+		def->hasBonusOfType(BonusType::FIRE_SHIELD) &&
+		!attackerState->hasBonusOfType(BonusType::FIRE_IMMUNITY) &&
+		CStack::isMeleeAttackPossible(attackerState.get(), def) // attacked needs to be adjacent to defender for fire shield to trigger (e.g. Dragon Breath attack)
+			)
+	{
+		//TODO: use damage with bonus but without penalties
+		auto fireShieldDamage = (std::min<int64_t>(def->getAvailableHealth(), bsa.damageAmount) * def->valOfBonuses(BonusType::FIRE_SHIELD)) / 100;
+		fireShield.push_back(std::make_pair(def, fireShieldDamage));
+	}
+
+	return drainedLife;
+}
+
+void BattleActionProcessor::sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple)
+{
+	if(killed > 0)
+	{
+		BattleLogMessage blm;
+		addGenericKilledLog(blm, defender, killed, multiple);
+		gameHandler->sendAndApply(&blm);
+	}
+}
+
+void BattleActionProcessor::addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple)
+{
+	if(killed > 0)
+	{
+		const int32_t txtIndex = (killed > 1) ? 379 : 378;
+		std::string formatString = VLC->generaltexth->allTexts[txtIndex];
+
+		// these default h3 texts have unnecessary new lines, so get rid of them before displaying (and trim just in case, trimming newlines does not works for some reason)
+		formatString.erase(std::remove(formatString.begin(), formatString.end(), '\n'), formatString.end());
+		formatString.erase(std::remove(formatString.begin(), formatString.end(), '\r'), formatString.end());
+		boost::algorithm::trim(formatString);
+
+		boost::format txt(formatString);
+		if(killed > 1)
+		{
+			txt % killed % (multiple ? VLC->generaltexth->allTexts[43] : defender->unitType()->getNamePluralTranslated()); // creatures perish
+		}
+		else //killed == 1
+		{
+			txt % (multiple ? VLC->generaltexth->allTexts[42] : defender->unitType()->getNameSingularTranslated()); // creature perishes
+		}
+		MetaString line;
+		line.appendRawString(txt.str());
+		blm.lines.push_back(std::move(line));
+	}
+}
+
+bool BattleActionProcessor::makeAutomaticBattleAction(const BattleAction & ba)
+{
+	return makeBattleActionImpl(ba);
+}
+
+bool BattleActionProcessor::makePlayerBattleAction(PlayerColor player, const BattleAction &ba)
+{
+	const BattleInfo * battle = gameHandler->gameState()->curB;
+
+	if(!battle && gameHandler->complain("Can not make action - there is no battle ongoing!"))
+		return false;
+
+	if (ba.side != 0 && ba.side != 1 && gameHandler->complain("Can not make action - invalid battle side!"))
+		return false;
+
+	if(battle->tacticDistance != 0)
+	{
+		if(!ba.isTacticsAction())
+		{
+			gameHandler->complain("Can not make actions while in tactics mode!");
+			return false;
+		}
+
+		if(player != battle->sides[ba.side].color)
+		{
+			gameHandler->complain("Can not make actions in battles you are not part of!");
+			return false;
+		}
+	}
+	else
+	{
+		if (ba.isUnitAction() && ba.stackNumber != battle->getActiveStackID())
+		{
+			gameHandler->complain("Can not make actions - stack is not active!");
+			return false;
+		}
+
+		auto active = battle->battleActiveUnit();
+		if(!active && gameHandler->complain("No active unit in battle!"))
+			return false;
+
+		auto unitOwner = battle->battleGetOwner(active);
+
+		if(player != unitOwner && gameHandler->complain("Can not make actions in battles you are not part of!"))
+			return false;
+	}
+
+	return makeBattleActionImpl(ba);
+}

+ 79 - 0
server/battles/BattleActionProcessor.h

@@ -0,0 +1,79 @@
+/*
+ * BattleActionProcessor.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
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+struct BattleLogMessage;
+struct BattleAttack;
+class BattleAction;
+struct BattleHex;
+class CStack;
+class PlayerColor;
+enum class BonusType;
+
+namespace battle
+{
+class Unit;
+class CUnitState;
+}
+
+VCMI_LIB_NAMESPACE_END
+
+class CGameHandler;
+class BattleProcessor;
+
+/// Processes incoming battle action queries and applies requested action(s)
+class BattleActionProcessor : boost::noncopyable
+{
+	using FireShieldInfo = std::vector<std::pair<const CStack *, int64_t>>;
+
+	BattleProcessor * owner;
+	CGameHandler * gameHandler;
+
+	int moveStack(int stack, BattleHex dest); //returned value - travelled distance
+	void makeAttack(const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter);
+
+	void handleAttackBeforeCasting(bool ranged, const CStack * attacker, const CStack * defender);
+	void handleAfterAttackCasting(bool ranged, const CStack * attacker, const CStack * defender);
+	void attackCasting(bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender);
+
+	// damage, drain life & fire shield; returns amount of drained life
+	int64_t applyBattleEffects(BattleAttack & bat, std::shared_ptr<battle::CUnitState> attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary);
+
+	void sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple);
+	void addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple);
+
+	bool canStackAct(const CStack * stack);
+
+	bool doEmptyAction(const BattleAction & ba);
+	bool doEndTacticsAction(const BattleAction & ba);
+	bool doRetreatAction(const BattleAction & ba);
+	bool doSurrenderAction(const BattleAction & ba);
+	bool doHeroSpellAction(const BattleAction & ba);
+	bool doWalkAction(const BattleAction & ba);
+	bool doWaitAction(const BattleAction & ba);
+	bool doDefendAction(const BattleAction & ba);
+	bool doAttackAction(const BattleAction & ba);
+	bool doShootAction(const BattleAction & ba);
+	bool doCatapultAction(const BattleAction & ba);
+	bool doUnitSpellAction(const BattleAction & ba);
+	bool doHealAction(const BattleAction & ba);
+
+	bool dispatchBattleAction(const BattleAction & ba);
+	bool makeBattleActionImpl(const BattleAction & ba);
+
+public:
+	explicit BattleActionProcessor(BattleProcessor * owner);
+	void setGameHandler(CGameHandler * newGameHandler);
+
+	bool makeAutomaticBattleAction(const BattleAction & ba);
+	bool makePlayerBattleAction(PlayerColor player, const BattleAction & ba);
+};

+ 741 - 0
server/battles/BattleFlowProcessor.cpp

@@ -0,0 +1,741 @@
+/*
+ * BattleFlowProcessor.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 "BattleFlowProcessor.h"
+
+#include "BattleProcessor.h"
+
+#include "../CGameHandler.h"
+
+#include "../../lib/CStack.h"
+#include "../../lib/GameSettings.h"
+#include "../../lib/battle/BattleInfo.h"
+#include "../../lib/gameState/CGameState.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
+#include "../../lib/NetPacks.h"
+#include "../../lib/spells/BonusCaster.h"
+#include "../../lib/spells/ISpellMechanics.h"
+#include "../../lib/spells/ObstacleCasterProxy.h"
+
+BattleFlowProcessor::BattleFlowProcessor(BattleProcessor * owner)
+	: owner(owner)
+	, gameHandler(nullptr)
+{
+}
+
+void BattleFlowProcessor::setGameHandler(CGameHandler * newGameHandler)
+{
+	gameHandler = newGameHandler;
+}
+
+void BattleFlowProcessor::summonGuardiansHelper(std::vector<BattleHex> & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex) //return hexes for summoning two hex monsters in output, target = unit to guard
+{
+	int x = targetPosition.getX();
+	int y = targetPosition.getY();
+
+	const bool targetIsAttacker = side == BattleSide::ATTACKER;
+
+	if (targetIsAttacker) //handle front guardians, TODO: should we handle situation when units start battle near opposite side of the battlefield? Cannot happen in normal H3...
+		BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::RIGHT, false), output);
+	else
+		BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::LEFT, false), output);
+
+	//guardian spawn locations for four default position cases for attacker and defender, non-default starting location for att and def is handled in first two if's
+	if (targetIsAttacker && ((y % 2 == 0) || (x > 1)))
+	{
+		if (targetIsTwoHex && (y % 2 == 1) && (x == 2)) //handle exceptional case
+		{
+			BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output);
+			BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output);
+		}
+		else
+		{	//add back-side guardians for two-hex target, side guardians for one-hex
+			BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::TOP_LEFT : BattleHex::EDir::TOP_RIGHT, false), output);
+			BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::BOTTOM_LEFT : BattleHex::EDir::BOTTOM_RIGHT, false), output);
+
+			if (!targetIsTwoHex && x > 2) //back guard for one-hex
+				BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false), output);
+			else if (targetIsTwoHex)//front-side guardians for two-hex target
+			{
+				BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output);
+				BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output);
+				if (x > 3) //back guard for two-hex
+					BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::LEFT, false), output);
+			}
+		}
+
+	}
+
+	else if (!targetIsAttacker && ((y % 2 == 1) || (x < GameConstants::BFIELD_WIDTH - 2)))
+	{
+		if (targetIsTwoHex && (y % 2 == 0) && (x == GameConstants::BFIELD_WIDTH - 3)) //handle exceptional case... equivalent for above for defender side
+		{
+			BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output);
+			BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output);
+		}
+		else
+		{
+			BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::TOP_RIGHT : BattleHex::EDir::TOP_LEFT, false), output);
+			BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::BOTTOM_RIGHT : BattleHex::EDir::BOTTOM_LEFT, false), output);
+
+			if (!targetIsTwoHex && x < GameConstants::BFIELD_WIDTH - 3)
+				BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false), output);
+			else if (targetIsTwoHex)
+			{
+				BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output);
+				BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output);
+				if (x < GameConstants::BFIELD_WIDTH - 4)
+					BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::RIGHT, false), output);
+			}
+		}
+	}
+
+	else if (!targetIsAttacker && y % 2 == 0)
+	{
+		BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output);
+		BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output);
+	}
+
+	else if (targetIsAttacker && y % 2 == 1)
+	{
+		BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output);
+		BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output);
+	}
+}
+
+void BattleFlowProcessor::tryPlaceMoats()
+{
+	//Moat should be initialized here, because only here we can use spellcasting
+	if (gameHandler->gameState()->curB->town && gameHandler->gameState()->curB->town->fortLevel() >= CGTownInstance::CITADEL)
+	{
+		const auto * h = gameHandler->gameState()->curB->battleGetFightingHero(BattleSide::DEFENDER);
+		const auto * actualCaster = h ? static_cast<const spells::Caster*>(h) : nullptr;
+		auto moatCaster = spells::SilentCaster(gameHandler->gameState()->curB->getSidePlayer(BattleSide::DEFENDER), actualCaster);
+		auto cast = spells::BattleCast(gameHandler->gameState()->curB, &moatCaster, spells::Mode::PASSIVE, gameHandler->gameState()->curB->town->town->moatAbility.toSpell());
+		auto target = spells::Target();
+		cast.cast(gameHandler->spellEnv, target);
+	}
+}
+
+void BattleFlowProcessor::onBattleStarted()
+{
+	gameHandler->setBattle(gameHandler->gameState()->curB);
+	assert(gameHandler->gameState()->curB);
+
+	tryPlaceMoats();
+
+	if (gameHandler->gameState()->curB->tacticDistance == 0)
+		onTacticsEnded();
+}
+
+void BattleFlowProcessor::trySummonGuardians(const CStack * stack)
+{
+	if (!stack->hasBonusOfType(BonusType::SUMMON_GUARDIANS))
+		return;
+
+	std::shared_ptr<const Bonus> summonInfo = stack->getBonus(Selector::type()(BonusType::SUMMON_GUARDIANS));
+	auto accessibility = gameHandler->getAccesibility();
+	CreatureID creatureData = CreatureID(summonInfo->subtype);
+	std::vector<BattleHex> targetHexes;
+	const bool targetIsBig = stack->unitType()->isDoubleWide(); //target = creature to guard
+	const bool guardianIsBig = creatureData.toCreature()->isDoubleWide();
+
+	/*Chosen idea for two hex units was to cover all possible surrounding hexes of target unit with as small number of stacks as possible.
+		For one-hex targets there are four guardians - front, back and one per side (up + down).
+		Two-hex targets are wider and the difference is there are two guardians per side to cover 3 hexes + extra hex in the front
+		Additionally, there are special cases for starting positions etc., where guardians would be outside of battlefield if spawned normally*/
+	if (!guardianIsBig)
+		targetHexes = stack->getSurroundingHexes();
+	else
+		summonGuardiansHelper(targetHexes, stack->getPosition(), stack->unitSide(), targetIsBig);
+
+	for(auto hex : targetHexes)
+	{
+		if(accessibility.accessible(hex, guardianIsBig, stack->unitSide())) //without this multiple creatures can occupy one hex
+		{
+			battle::UnitInfo info;
+			info.id = gameHandler->gameState()->curB->battleNextUnitId();
+			info.count =  std::max(1, (int)(stack->getCount() * 0.01 * summonInfo->val));
+			info.type = creatureData;
+			info.side = stack->unitSide();
+			info.position = hex;
+			info.summoned = true;
+
+			BattleUnitsChanged pack;
+			pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD);
+			info.save(pack.changedStacks.back().data);
+			gameHandler->sendAndApply(&pack);
+		}
+	}
+}
+
+void BattleFlowProcessor::castOpeningSpells()
+{
+	for (int i = 0; i < 2; ++i)
+	{
+		auto h = gameHandler->gameState()->curB->battleGetFightingHero(i);
+		if (!h)
+			continue;
+
+		TConstBonusListPtr bl = h->getBonuses(Selector::type()(BonusType::OPENING_BATTLE_SPELL));
+
+		for (auto b : *bl)
+		{
+			spells::BonusCaster caster(h, b);
+
+			const CSpell * spell = SpellID(b->subtype).toSpell();
+
+			spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell);
+			parameters.setSpellLevel(3);
+			parameters.setEffectDuration(b->val);
+			parameters.massive = true;
+			parameters.castIfPossible(gameHandler->spellEnv, spells::Target());
+		}
+	}
+}
+
+void BattleFlowProcessor::onTacticsEnded()
+{
+	//initial stacks appearance triggers, e.g. built-in bonus spells
+	auto initialStacks = gameHandler->gameState()->curB->stacks; //use temporary variable to outclude summoned stacks added to gameHandler->gameState()->curB->stacks from processing
+
+	for (CStack * stack : initialStacks)
+	{
+		trySummonGuardians(stack);
+		stackEnchantedTrigger(stack);
+	}
+
+	castOpeningSpells();
+
+	// it is possible that due to opening spells one side was eliminated -> check for end of battle
+	if (owner->checkBattleStateChanges())
+		return;
+
+	startNextRound(true);
+	activateNextStack();
+}
+
+void BattleFlowProcessor::startNextRound(bool isFirstRound)
+{
+	BattleNextRound bnr;
+	bnr.round = gameHandler->gameState()->curB->round + 1;
+	logGlobal->debug("Round %d", bnr.round);
+	gameHandler->sendAndApply(&bnr);
+
+	auto obstacles = gameHandler->gameState()->curB->obstacles; //we copy container, because we're going to modify it
+	for (auto &obstPtr : obstacles)
+	{
+		if (const SpellCreatedObstacle *sco = dynamic_cast<const SpellCreatedObstacle *>(obstPtr.get()))
+			if (sco->turnsRemaining == 0)
+				removeObstacle(*obstPtr);
+	}
+
+	const BattleInfo & curB = *gameHandler->gameState()->curB;
+
+	for(auto stack : curB.stacks)
+	{
+		if(stack->alive() && !isFirstRound)
+			stackEnchantedTrigger(stack);
+	}
+}
+
+const CStack * BattleFlowProcessor::getNextStack()
+{
+	std::vector<battle::Units> q;
+	gameHandler->gameState()->curB->battleGetTurnOrder(q, 1, 0, -1); //todo: get rid of "turn -1"
+
+	if(q.empty())
+		return nullptr;
+
+	if(q.front().empty())
+		return nullptr;
+
+	auto next = q.front().front();
+	const auto stack = dynamic_cast<const CStack *>(next);
+
+	// regeneration takes place before everything else but only during first turn attempt in each round
+	// also works under blind and similar effects
+	if(stack && stack->alive() && !stack->waiting)
+	{
+		BattleTriggerEffect bte;
+		bte.stackID = stack->unitId();
+		bte.effect = vstd::to_underlying(BonusType::HP_REGENERATION);
+
+		const int32_t lostHealth = stack->getMaxHealth() - stack->getFirstHPleft();
+		if(stack->hasBonusOfType(BonusType::HP_REGENERATION))
+			bte.val = std::min(lostHealth, stack->valOfBonuses(BonusType::HP_REGENERATION));
+
+		if(bte.val) // anything to heal
+			gameHandler->sendAndApply(&bte);
+	}
+
+	if(!next->willMove())
+		return nullptr;
+
+	return stack;
+}
+
+void BattleFlowProcessor::activateNextStack()
+{
+	//TODO: activate next round if next == nullptr
+	const auto & curB = *gameHandler->gameState()->curB;
+
+	// Find next stack that requires manual control
+	for (;;)
+	{
+		// battle has ended
+		if (owner->checkBattleStateChanges())
+			return;
+
+		const CStack * next = getNextStack();
+
+		if (!next)
+		{
+			// No stacks to move - start next round
+			startNextRound(false);
+			next = getNextStack();
+			if (!next)
+				throw std::runtime_error("Failed to find valid stack to act!");
+		}
+
+		BattleUnitsChanged removeGhosts;
+
+		for(auto stack : curB.stacks)
+		{
+			if(stack->ghostPending)
+				removeGhosts.changedStacks.emplace_back(stack->unitId(), UnitChanges::EOperation::REMOVE);
+		}
+
+		if(!removeGhosts.changedStacks.empty())
+			gameHandler->sendAndApply(&removeGhosts);
+
+		if (!tryMakeAutomaticAction(next))
+		{
+			setActiveStack(next);
+			break;
+		}
+	}
+}
+
+bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next)
+{
+	const auto & curB = *gameHandler->gameState()->curB;
+
+	// check for bad morale => freeze
+	int nextStackMorale = next->moraleVal();
+	if(!next->hadMorale && !next->waited() && nextStackMorale < 0)
+	{
+		auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_BAD_MORALE_DICE);
+		size_t diceIndex = std::min<size_t>(diceSize.size()-1, -nextStackMorale);
+
+		if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1)
+		{
+			//unit loses its turn - empty freeze action
+			BattleAction ba;
+			ba.actionType = EActionType::BAD_MORALE;
+			ba.side = next->unitSide();
+			ba.stackNumber = next->unitId();
+
+			makeAutomaticAction(next, ba);
+			return true;
+		}
+	}
+
+	if (next->hasBonusOfType(BonusType::ATTACKS_NEAREST_CREATURE)) //while in berserk
+	{
+		logGlobal->trace("Handle Berserk effect");
+		std::pair<const battle::Unit *, BattleHex> attackInfo = curB.getNearestStack(next);
+		if (attackInfo.first != nullptr)
+		{
+			BattleAction attack;
+			attack.actionType = EActionType::WALK_AND_ATTACK;
+			attack.side = next->unitSide();
+			attack.stackNumber = next->unitId();
+			attack.aimToHex(attackInfo.second);
+			attack.aimToUnit(attackInfo.first);
+
+			makeAutomaticAction(next, attack);
+			logGlobal->trace("Attacked nearest target %s", attackInfo.first->getDescription());
+		}
+		else
+		{
+			makeStackDoNothing(next);
+			logGlobal->trace("No target found");
+		}
+		return true;
+	}
+
+	const CGHeroInstance * curOwner = gameHandler->battleGetOwnerHero(next);
+	const int stackCreatureId = next->unitType()->getId();
+
+	if ((stackCreatureId == CreatureID::ARROW_TOWERS || stackCreatureId == CreatureID::BALLISTA)
+		&& (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, stackCreatureId)))
+	{
+		BattleAction attack;
+		attack.actionType = EActionType::SHOOT;
+		attack.side = next->unitSide();
+		attack.stackNumber = next->unitId();
+
+		//TODO: select target by priority
+
+		const battle::Unit * target = nullptr;
+
+		for(auto & elem : gameHandler->gameState()->curB->stacks)
+		{
+			if(elem->unitType()->getId() != CreatureID::CATAPULT
+			   && elem->unitOwner() != next->unitOwner()
+			   && elem->isValidTarget()
+			   && gameHandler->gameState()->curB->battleCanShoot(next, elem->getPosition()))
+			{
+				target = elem;
+				break;
+			}
+		}
+
+		if(target == nullptr)
+		{
+			makeStackDoNothing(next);
+		}
+		else
+		{
+			attack.aimToUnit(target);
+			makeAutomaticAction(next, attack);
+		}
+		return true;
+	}
+
+	if (next->unitType()->getId() == CreatureID::CATAPULT)
+	{
+		const auto & attackableBattleHexes = curB.getAttackableBattleHexes();
+
+		if (attackableBattleHexes.empty())
+		{
+			makeStackDoNothing(next);
+			return true;
+		}
+
+		if (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, CreatureID::CATAPULT))
+		{
+			BattleAction attack;
+			attack.actionType = EActionType::CATAPULT;
+			attack.side = next->unitSide();
+			attack.stackNumber = next->unitId();
+
+			makeAutomaticAction(next, attack);
+			return true;
+		}
+	}
+
+	if (next->unitType()->getId() == CreatureID::FIRST_AID_TENT)
+	{
+		TStacks possibleStacks = gameHandler->battleGetStacksIf([=](const CStack * s)
+		{
+			return s->unitOwner() == next->unitOwner() && s->canBeHealed();
+		});
+
+		if (possibleStacks.empty())
+		{
+			makeStackDoNothing(next);
+			return true;
+		}
+
+		if (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, CreatureID::FIRST_AID_TENT))
+		{
+			RandomGeneratorUtil::randomShuffle(possibleStacks, gameHandler->getRandomGenerator());
+			const CStack * toBeHealed = possibleStacks.front();
+
+			BattleAction heal;
+			heal.actionType = EActionType::STACK_HEAL;
+			heal.aimToUnit(toBeHealed);
+			heal.side = next->unitSide();
+			heal.stackNumber = next->unitId();
+
+			makeAutomaticAction(next, heal);
+			return true;
+		}
+	}
+
+	stackTurnTrigger(next); //various effects
+
+	if(next->fear)
+	{
+		makeStackDoNothing(next); //end immediately if stack was affected by fear
+		return true;
+	}
+	return false;
+}
+
+bool BattleFlowProcessor::rollGoodMorale(const CStack * next)
+{
+	//check for good morale
+	auto nextStackMorale = next->moraleVal();
+	if(    !next->hadMorale
+		&& !next->defending
+		&& !next->waited()
+		&& !next->fear
+		&& next->alive()
+		&& nextStackMorale > 0)
+	{
+		auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE);
+		size_t diceIndex = std::min<size_t>(diceSize.size()-1, nextStackMorale);
+
+		if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1)
+		{
+			BattleTriggerEffect bte;
+			bte.stackID = next->unitId();
+			bte.effect = vstd::to_underlying(BonusType::MORALE);
+			bte.val = 1;
+			bte.additionalInfo = 0;
+			gameHandler->sendAndApply(&bte); //play animation
+			return true;
+		}
+	}
+	return false;
+}
+
+void BattleFlowProcessor::onActionMade(const BattleAction &ba)
+{
+	const auto & battle = gameHandler->gameState()->curB;
+
+	const CStack * actedStack = battle->battleGetStackByID(ba.stackNumber, false);
+	const CStack * activeStack = battle->battleGetStackByID(battle->getActiveStackID(), false);
+
+	//we're after action, all results applied
+
+	// check whether action has ended the battle
+	if(owner->checkBattleStateChanges())
+		return;
+
+	// tactics - next stack will be selected by player
+	if(battle->tacticDistance != 0)
+		return;
+
+	if (ba.isUnitAction())
+	{
+		assert(activeStack != nullptr);
+		assert(actedStack != nullptr);
+
+		if (rollGoodMorale(actedStack))
+		{
+			// Good morale - same stack makes 2nd turn
+			setActiveStack(actedStack);
+			return;
+		}
+	}
+	else
+	{
+		if (activeStack && activeStack->alive())
+		{
+			// this is action made by hero AND unit is alive (e.g. not killed by casted spell)
+			// keep current active stack for next action
+			setActiveStack(activeStack);
+			return;
+		}
+	}
+
+	activateNextStack();
+}
+
+void BattleFlowProcessor::makeStackDoNothing(const CStack * next)
+{
+	BattleAction doNothing;
+	doNothing.actionType = EActionType::NO_ACTION;
+	doNothing.side = next->unitSide();
+	doNothing.stackNumber = next->unitId();
+
+	makeAutomaticAction(next, doNothing);
+}
+
+bool BattleFlowProcessor::makeAutomaticAction(const CStack *stack, BattleAction &ba)
+{
+	BattleSetActiveStack bsa;
+	bsa.stack = stack->unitId();
+	bsa.askPlayerInterface = false;
+	gameHandler->sendAndApply(&bsa);
+
+	bool ret = owner->makeAutomaticBattleAction(ba);
+	return ret;
+}
+
+void BattleFlowProcessor::stackEnchantedTrigger(const CStack * st)
+{
+	auto bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTED)));
+	for(auto b : bl)
+	{
+		const CSpell * sp = SpellID(b->subtype).toSpell();
+		if(!sp)
+			continue;
+
+		const int32_t val = bl.valOfBonuses(Selector::typeSubtype(b->type, b->subtype));
+		const int32_t level = ((val > 3) ? (val - 3) : val);
+
+		spells::BattleCast battleCast(gameHandler->gameState()->curB, st, spells::Mode::PASSIVE, sp);
+		//this makes effect accumulate for at most 50 turns by default, but effect may be permanent and last till the end of battle
+		battleCast.setEffectDuration(50);
+		battleCast.setSpellLevel(level);
+		spells::Target target;
+
+		if(val > 3)
+		{
+			for(auto s : gameHandler->gameState()->curB->battleGetAllStacks())
+				if(gameHandler->battleMatchOwner(st, s, true) && s->isValidTarget()) //all allied
+					target.emplace_back(s);
+		}
+		else
+		{
+			target.emplace_back(st);
+		}
+		battleCast.applyEffects(gameHandler->spellEnv, target, false, true);
+	}
+}
+
+void BattleFlowProcessor::removeObstacle(const CObstacleInstance & obstacle)
+{
+	BattleObstaclesChanged obsRem;
+	obsRem.changes.emplace_back(obstacle.uniqueID, ObstacleChanges::EOperation::REMOVE);
+	gameHandler->sendAndApply(&obsRem);
+}
+
+void BattleFlowProcessor::stackTurnTrigger(const CStack *st)
+{
+	BattleTriggerEffect bte;
+	bte.stackID = st->unitId();
+	bte.effect = -1;
+	bte.val = 0;
+	bte.additionalInfo = 0;
+	if (st->alive())
+	{
+		//unbind
+		if (st->hasBonus(Selector::type()(BonusType::BIND_EFFECT)))
+		{
+			bool unbind = true;
+			BonusList bl = *(st->getBonuses(Selector::type()(BonusType::BIND_EFFECT)));
+			auto adjacent = gameHandler->gameState()->curB->battleAdjacentUnits(st);
+
+			for (auto b : bl)
+			{
+				if(b->additionalInfo != CAddInfo::NONE)
+				{
+					const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(b->additionalInfo[0]); //binding stack must be alive and adjacent
+					if(stack)
+					{
+						if(vstd::contains(adjacent, stack)) //binding stack is still present
+							unbind = false;
+					}
+				}
+				else
+				{
+					unbind = false;
+				}
+			}
+			if (unbind)
+			{
+				BattleSetStackProperty ssp;
+				ssp.which = BattleSetStackProperty::UNBIND;
+				ssp.stackID = st->unitId();
+				gameHandler->sendAndApply(&ssp);
+			}
+		}
+
+		if (st->hasBonusOfType(BonusType::POISON))
+		{
+			std::shared_ptr<const Bonus> b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, SpellID::POISON).And(Selector::type()(BonusType::STACK_HEALTH)));
+			if (b) //TODO: what if not?...
+			{
+				bte.val = std::max (b->val - 10, -(st->valOfBonuses(BonusType::POISON)));
+				if (bte.val < b->val) //(negative) poison effect increases - update it
+				{
+					bte.effect = vstd::to_underlying(BonusType::POISON);
+					gameHandler->sendAndApply(&bte);
+				}
+			}
+		}
+		if(st->hasBonusOfType(BonusType::MANA_DRAIN) && !st->drainedMana)
+		{
+			const PlayerColor opponent = gameHandler->gameState()->curB->otherPlayer(gameHandler->gameState()->curB->battleGetOwner(st));
+			const CGHeroInstance * opponentHero = gameHandler->gameState()->curB->getHero(opponent);
+			if(opponentHero)
+			{
+				ui32 manaDrained = st->valOfBonuses(BonusType::MANA_DRAIN);
+				vstd::amin(manaDrained, opponentHero->mana);
+				if(manaDrained)
+				{
+					bte.effect = vstd::to_underlying(BonusType::MANA_DRAIN);
+					bte.val = manaDrained;
+					bte.additionalInfo = opponentHero->id.getNum(); //for sanity
+					gameHandler->sendAndApply(&bte);
+				}
+			}
+		}
+		if (st->isLiving() && !st->hasBonusOfType(BonusType::FEARLESS))
+		{
+			bool fearsomeCreature = false;
+			for (CStack * stack : gameHandler->gameState()->curB->stacks)
+			{
+				if (gameHandler->battleMatchOwner(st, stack) && stack->alive() && stack->hasBonusOfType(BonusType::FEAR))
+				{
+					fearsomeCreature = true;
+					break;
+				}
+			}
+			if (fearsomeCreature)
+			{
+				if (gameHandler->getRandomGenerator().nextInt(99) < 10) //fixed 10%
+				{
+					bte.effect = vstd::to_underlying(BonusType::FEAR);
+					gameHandler->sendAndApply(&bte);
+				}
+			}
+		}
+		BonusList bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTER)));
+		int side = gameHandler->gameState()->curB->whatSide(st->unitOwner());
+		if(st->canCast() && gameHandler->gameState()->curB->battleGetEnchanterCounter(side) == 0)
+		{
+			bool cast = false;
+			while(!bl.empty() && !cast)
+			{
+				auto bonus = *RandomGeneratorUtil::nextItem(bl, gameHandler->getRandomGenerator());
+				auto spellID = SpellID(bonus->subtype);
+				const CSpell * spell = SpellID(spellID).toSpell();
+				bl.remove_if([&bonus](const Bonus * b)
+				{
+					return b == bonus.get();
+				});
+				spells::BattleCast parameters(gameHandler->gameState()->curB, st, spells::Mode::ENCHANTER, spell);
+				parameters.setSpellLevel(bonus->val);
+				parameters.massive = true;
+				parameters.smart = true;
+				//todo: recheck effect level
+				if(parameters.castIfPossible(gameHandler->spellEnv, spells::Target(1, spells::Destination())))
+				{
+					cast = true;
+
+					int cooldown = bonus->additionalInfo[0];
+					BattleSetStackProperty ssp;
+					ssp.which = BattleSetStackProperty::ENCHANTER_COUNTER;
+					ssp.absolute = false;
+					ssp.val = cooldown;
+					ssp.stackID = st->unitId();
+					gameHandler->sendAndApply(&ssp);
+				}
+			}
+		}
+	}
+}
+
+void BattleFlowProcessor::setActiveStack(const CStack * stack)
+{
+	assert(stack);
+
+	logGlobal->trace("Activating %s", stack->nodeName());
+	BattleSetActiveStack sas;
+	sas.stack = stack->unitId();
+	gameHandler->sendAndApply(&sas);
+}

+ 55 - 0
server/battles/BattleFlowProcessor.h

@@ -0,0 +1,55 @@
+/*
+ * BattleFlowProcessor.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
+
+VCMI_LIB_NAMESPACE_BEGIN
+class CStack;
+struct BattleHex;
+class BattleAction;
+struct CObstacleInstance;
+VCMI_LIB_NAMESPACE_END
+
+class CGameHandler;
+class BattleProcessor;
+
+/// Controls flow of battles - battle startup actions and switching to next stack or next round after actions
+class BattleFlowProcessor : boost::noncopyable
+{
+	BattleProcessor * owner;
+	CGameHandler * gameHandler;
+
+	const CStack * getNextStack();
+
+	bool rollGoodMorale(const CStack * stack);
+	bool tryMakeAutomaticAction(const CStack * stack);
+
+	void summonGuardiansHelper(std::vector<BattleHex> & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex);
+	void trySummonGuardians(const CStack * stack);
+	void tryPlaceMoats();
+	void castOpeningSpells();
+	void activateNextStack();
+	void startNextRound(bool isFirstRound);
+
+	void stackEnchantedTrigger(const CStack * stack);
+	void removeObstacle(const CObstacleInstance & obstacle);
+	void stackTurnTrigger(const CStack * stack);
+	void setActiveStack(const CStack * stack);
+
+	void makeStackDoNothing(const CStack * next);
+	bool makeAutomaticAction(const CStack * stack, BattleAction & ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack)
+
+public:
+	explicit BattleFlowProcessor(BattleProcessor * owner);
+	void setGameHandler(CGameHandler * newGameHandler);
+
+	void onBattleStarted();
+	void onTacticsEnded();
+	void onActionMade(const BattleAction & ba);
+};

+ 250 - 0
server/battles/BattleProcessor.cpp

@@ -0,0 +1,250 @@
+/*
+ * BattleProcessor.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 "BattleProcessor.h"
+
+#include "BattleActionProcessor.h"
+#include "BattleFlowProcessor.h"
+#include "BattleResultProcessor.h"
+
+#include "../CGameHandler.h"
+#include "../queries/QueriesProcessor.h"
+#include "../queries/BattleQueries.h"
+
+#include "../../lib/TerrainHandler.h"
+#include "../../lib/battle/BattleInfo.h"
+#include "../../lib/gameState/CGameState.h"
+#include "../../lib/mapping/CMap.h"
+#include "../../lib/modding/IdentifierStorage.h"
+
+BattleProcessor::BattleProcessor(CGameHandler * gameHandler)
+	: gameHandler(gameHandler)
+	, flowProcessor(std::make_unique<BattleFlowProcessor>(this))
+	, actionsProcessor(std::make_unique<BattleActionProcessor>(this))
+	, resultProcessor(std::make_unique<BattleResultProcessor>(this))
+{
+	setGameHandler(gameHandler);
+}
+
+BattleProcessor::BattleProcessor():
+	BattleProcessor(nullptr)
+{
+}
+
+BattleProcessor::~BattleProcessor() = default;
+
+void BattleProcessor::engageIntoBattle(PlayerColor player)
+{
+	//notify interfaces
+	PlayerBlocked pb;
+	pb.player = player;
+	pb.reason = PlayerBlocked::UPCOMING_BATTLE;
+	pb.startOrEnd = PlayerBlocked::BLOCKADE_STARTED;
+	gameHandler->sendAndApply(&pb);
+}
+
+void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile,
+								const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank,
+								const CGTownInstance *town) //use hero=nullptr for no hero
+{
+	if(gameHandler->gameState()->curB)
+		gameHandler->gameState()->curB.dellNull();
+
+	engageIntoBattle(army1->tempOwner);
+	engageIntoBattle(army2->tempOwner);
+
+	static const CArmedInstance *armies[2];
+	armies[0] = army1;
+	armies[1] = army2;
+	static const CGHeroInstance*heroes[2];
+	heroes[0] = hero1;
+	heroes[1] = hero2;
+
+	setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces
+
+	auto lastBattleQuery = std::dynamic_pointer_cast<CBattleQuery>(gameHandler->queries->topQuery(gameHandler->gameState()->curB->sides[0].color));
+
+	//existing battle query for retying auto-combat
+	if(lastBattleQuery)
+	{
+		for(int i : {0, 1})
+		{
+			if(heroes[i])
+			{
+				SetMana restoreInitialMana;
+				restoreInitialMana.val = lastBattleQuery->initialHeroMana[i];
+				restoreInitialMana.hid = heroes[i]->id;
+				gameHandler->sendAndApply(&restoreInitialMana);
+			}
+		}
+
+		lastBattleQuery->bi = gameHandler->gameState()->curB;
+		lastBattleQuery->result = std::nullopt;
+		lastBattleQuery->belligerents[0] = gameHandler->gameState()->curB->sides[0].armyObject;
+		lastBattleQuery->belligerents[1] = gameHandler->gameState()->curB->sides[1].armyObject;
+	}
+
+	auto nextBattleQuery = std::make_shared<CBattleQuery>(gameHandler, gameHandler->gameState()->curB);
+	for(int i : {0, 1})
+	{
+		if(heroes[i])
+		{
+			nextBattleQuery->initialHeroMana[i] = heroes[i]->mana;
+		}
+	}
+	gameHandler->queries->addQuery(nextBattleQuery);
+
+	flowProcessor->onBattleStarted();
+}
+
+void BattleProcessor::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank)
+{
+	startBattlePrimary(army1, army2, tile,
+		army1->ID == Obj::HERO ? static_cast<const CGHeroInstance*>(army1) : nullptr,
+		army2->ID == Obj::HERO ? static_cast<const CGHeroInstance*>(army2) : nullptr,
+		creatureBank);
+}
+
+void BattleProcessor::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank)
+{
+	startBattleI(army1, army2, army2->visitablePos(), creatureBank);
+}
+
+void BattleProcessor::setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town)
+{
+	const auto & t = *gameHandler->getTile(tile);
+	TerrainId terrain = t.terType->getId();
+	if (gameHandler->gameState()->map->isCoastalTile(tile)) //coastal tile is always ground
+		terrain = ETerrainId::SAND;
+
+	BattleField terType = gameHandler->gameState()->battleGetBattlefieldType(tile, gameHandler->getRandomGenerator());
+	if (heroes[0] && heroes[0]->boat && heroes[1] && heroes[1]->boat)
+		terType = BattleField(*VLC->identifiers()->getIdentifier("core", "battlefield.ship_to_ship"));
+
+	//send info about battles
+	BattleStart bs;
+	bs.info = BattleInfo::setupBattle(tile, terrain, terType, armies, heroes, creatureBank, town);
+
+	engageIntoBattle(bs.info->sides[0].color);
+	engageIntoBattle(bs.info->sides[1].color);
+
+	auto lastBattleQuery = std::dynamic_pointer_cast<CBattleQuery>(gameHandler->queries->topQuery(bs.info->sides[0].color));
+	bs.info->replayAllowed = lastBattleQuery == nullptr && !bs.info->sides[1].color.isValidPlayer();
+
+	gameHandler->sendAndApply(&bs);
+}
+
+bool BattleProcessor::checkBattleStateChanges()
+{
+	//check if drawbridge state need to be changes
+	if (gameHandler->battleGetSiegeLevel() > 0)
+		updateGateState();
+
+	//check if battle ended
+	if (auto result = gameHandler->battleIsFinished())
+	{
+		setBattleResult(EBattleResult::NORMAL, *result);
+		return true;
+	}
+
+	return false;
+}
+
+void BattleProcessor::updateGateState()
+{
+	// GATE_BRIDGE - leftmost tile, located over moat
+	// GATE_OUTER - central tile, mostly covered by gate image
+	// GATE_INNER - rightmost tile, inside the walls
+
+	// GATE_OUTER or GATE_INNER:
+	// - if defender moves unit on these tiles, bridge will open
+	// - if there is a creature (dead or alive) on these tiles, bridge will always remain open
+	// - blocked to attacker if bridge is closed
+
+	// GATE_BRIDGE
+	// - if there is a unit or corpse here, bridge can't open (and can't close in fortress)
+	// - if Force Field is cast here, bridge can't open (but can close, in any town)
+	// - deals moat damage to attacker if bridge is closed (fortress only)
+
+	bool hasForceFieldOnBridge = !gameHandler->battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), true).empty();
+	bool hasStackAtGateInner   = gameHandler->gameState()->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_INNER), false) != nullptr;
+	bool hasStackAtGateOuter   = gameHandler->gameState()->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_OUTER), false) != nullptr;
+	bool hasStackAtGateBridge  = gameHandler->gameState()->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_BRIDGE), false) != nullptr;
+	bool hasWideMoat           = vstd::contains_if(gameHandler->battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), false), [](const std::shared_ptr<const CObstacleInstance> & obst)
+	{
+		return obst->obstacleType == CObstacleInstance::MOAT;
+	});
+
+	BattleUpdateGateState db;
+	db.state = gameHandler->gameState()->curB->si.gateState;
+	if (gameHandler->gameState()->curB->si.wallState[EWallPart::GATE] == EWallState::DESTROYED)
+	{
+		db.state = EGateState::DESTROYED;
+	}
+	else if (db.state == EGateState::OPENED)
+	{
+		bool hasStackOnLongBridge = hasStackAtGateBridge && hasWideMoat;
+		bool gateCanClose = !hasStackAtGateInner && !hasStackAtGateOuter && !hasStackOnLongBridge;
+
+		if (gateCanClose)
+			db.state = EGateState::CLOSED;
+		else
+			db.state = EGateState::OPENED;
+	}
+	else // CLOSED or BLOCKED
+	{
+		bool gateBlocked = hasForceFieldOnBridge || hasStackAtGateBridge;
+
+		if (gateBlocked)
+			db.state = EGateState::BLOCKED;
+		else
+			db.state = EGateState::CLOSED;
+	}
+
+	if (db.state != gameHandler->gameState()->curB->si.gateState)
+		gameHandler->sendAndApply(&db);
+}
+
+bool BattleProcessor::makePlayerBattleAction(PlayerColor player, const BattleAction &ba)
+{
+	bool result = actionsProcessor->makePlayerBattleAction(player, ba);
+	flowProcessor->onActionMade(ba);
+	return result;
+}
+
+void BattleProcessor::setBattleResult(EBattleResult resultType, int victoriusSide)
+{
+	resultProcessor->setBattleResult(resultType, victoriusSide);
+	resultProcessor->endBattle(gameHandler->gameState()->curB->tile, gameHandler->gameState()->curB->battleGetFightingHero(0), gameHandler->gameState()->curB->battleGetFightingHero(1));
+}
+
+bool BattleProcessor::makeAutomaticBattleAction(const BattleAction &ba)
+{
+	return actionsProcessor->makeAutomaticBattleAction(ba);
+}
+
+void BattleProcessor::endBattleConfirm(const BattleInfo * battleInfo)
+{
+	resultProcessor->endBattleConfirm(battleInfo);
+}
+
+void BattleProcessor::battleAfterLevelUp(const BattleResult &result)
+{
+	resultProcessor->battleAfterLevelUp(result);
+}
+
+void BattleProcessor::setGameHandler(CGameHandler * newGameHandler)
+{
+	gameHandler = newGameHandler;
+
+	actionsProcessor->setGameHandler(newGameHandler);
+	flowProcessor->setGameHandler(newGameHandler);
+	resultProcessor->setGameHandler(newGameHandler);
+}

+ 80 - 0
server/battles/BattleProcessor.h

@@ -0,0 +1,80 @@
+/*
+ * BattleProcessor.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 "../../lib/GameConstants.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+class CGHeroInstance;
+class CGTownInstance;
+class CArmedInstance;
+class BattleAction;
+class int3;
+class BattleInfo;
+struct BattleResult;
+VCMI_LIB_NAMESPACE_END
+
+class CGameHandler;
+class CBattleQuery;
+class BattleActionProcessor;
+class BattleFlowProcessor;
+class BattleResultProcessor;
+
+/// Main class for battle handling. Contains all public interface for battles that is accessible from outside, e.g. for CGameHandler
+class BattleProcessor : boost::noncopyable
+{
+	friend class BattleActionProcessor;
+	friend class BattleFlowProcessor;
+	friend class BattleResultProcessor;
+
+	CGameHandler * gameHandler;
+	std::unique_ptr<BattleActionProcessor> actionsProcessor;
+	std::unique_ptr<BattleFlowProcessor> flowProcessor;
+	std::unique_ptr<BattleResultProcessor> resultProcessor;
+
+	void updateGateState();
+	void engageIntoBattle(PlayerColor player);
+
+	bool checkBattleStateChanges();
+	void setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town);
+
+	bool makeAutomaticBattleAction(const BattleAction & ba);
+
+	void setBattleResult(EBattleResult resultType, int victoriusSide);
+
+public:
+	explicit BattleProcessor(CGameHandler * gameHandler);
+	BattleProcessor();
+	~BattleProcessor();
+
+	void setGameHandler(CGameHandler * gameHandler);
+
+	/// Starts battle with specified parameters
+	void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr);
+	/// Starts battle between two armies (which can also be heroes) at specified tile
+	void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false);
+	/// Starts battle between two armies (which can also be heroes) at position of 2nd object
+	void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false);
+
+	/// Processing of incoming battle action netpack
+	bool makePlayerBattleAction(PlayerColor player, const BattleAction & ba);
+
+	/// Applies results of a battle once player agrees to them
+	void endBattleConfirm(const BattleInfo * battleInfo);
+	/// Applies results of a battle after potential levelup
+	void battleAfterLevelUp(const BattleResult & result);
+
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{
+
+	}
+
+};
+

+ 539 - 0
server/battles/BattleResultProcessor.cpp

@@ -0,0 +1,539 @@
+/*
+ * BattleResultProcessor.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 "BattleResultProcessor.h"
+
+#include "../CGameHandler.h"
+#include "../processors/HeroPoolProcessor.h"
+#include "../queries/QueriesProcessor.h"
+#include "../queries/BattleQueries.h"
+
+#include "../../lib/ArtifactUtils.h"
+#include "../../lib/CStack.h"
+#include "../../lib/GameSettings.h"
+#include "../../lib/battle/BattleInfo.h"
+#include "../../lib/gameState/CGameState.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
+#include "../../lib/serializer/Cast.h"
+#include "../../lib/spells/CSpellHandler.h"
+
+BattleResultProcessor::BattleResultProcessor(BattleProcessor * owner)
+//	: owner(owner)
+	: gameHandler(nullptr)
+{
+}
+
+void BattleResultProcessor::setGameHandler(CGameHandler * newGameHandler)
+{
+	gameHandler = newGameHandler;
+}
+
+CasualtiesAfterBattle::CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat):
+	army(battleSide.armyObject)
+{
+	heroWithDeadCommander = ObjectInstanceID();
+
+	PlayerColor color = battleSide.color;
+
+	for(CStack * st : bat->stacks)
+	{
+		if(st->summoned) //don't take into account temporary summoned stacks
+			continue;
+		if(st->unitOwner() != color) //remove only our stacks
+			continue;
+
+		logGlobal->debug("Calculating casualties for %s", st->nodeName());
+
+		st->health.takeResurrected();
+
+		if(st->unitSlot() == SlotID::ARROW_TOWERS_SLOT)
+		{
+			logGlobal->debug("Ignored arrow towers stack.");
+		}
+		else if(st->unitSlot() == SlotID::WAR_MACHINES_SLOT)
+		{
+			auto warMachine = st->unitType()->warMachine;
+
+			if(warMachine == ArtifactID::NONE)
+			{
+				logGlobal->error("Invalid creature in war machine virtual slot. Stack: %s", st->nodeName());
+			}
+			//catapult artifact remain even if "creature" killed in siege
+			else if(warMachine != ArtifactID::CATAPULT && st->getCount() <= 0)
+			{
+				logGlobal->debug("War machine has been destroyed");
+				auto hero = dynamic_ptr_cast<CGHeroInstance> (army);
+				if (hero)
+					removedWarMachines.push_back (ArtifactLocation(hero, hero->getArtPos(warMachine, true)));
+				else
+					logGlobal->error("War machine in army without hero");
+			}
+		}
+		else if(st->unitSlot() == SlotID::SUMMONED_SLOT_PLACEHOLDER)
+		{
+			if(st->alive() && st->getCount() > 0)
+			{
+				logGlobal->debug("Permanently summoned %d units.", st->getCount());
+				const CreatureID summonedType = st->creatureId();
+				summoned[summonedType] += st->getCount();
+			}
+		}
+		else if(st->unitSlot() == SlotID::COMMANDER_SLOT_PLACEHOLDER)
+		{
+			if (nullptr == st->base)
+			{
+				logGlobal->error("Stack with no base in commander slot. Stack: %s", st->nodeName());
+			}
+			else
+			{
+				auto c = dynamic_cast <const CCommanderInstance *>(st->base);
+				if(c)
+				{
+					auto h = dynamic_cast <const CGHeroInstance *>(army);
+					if(h && h->commander == c && (st->getCount() == 0 || !st->alive()))
+					{
+						logGlobal->debug("Commander is dead.");
+						heroWithDeadCommander = army->id; //TODO: unify commander handling
+					}
+				}
+				else
+					logGlobal->error("Stack with invalid instance in commander slot. Stack: %s", st->nodeName());
+			}
+		}
+		else if(st->base && !army->slotEmpty(st->unitSlot()))
+		{
+			logGlobal->debug("Count: %d; base count: %d", st->getCount(), army->getStackCount(st->unitSlot()));
+			if(st->getCount() == 0 || !st->alive())
+			{
+				logGlobal->debug("Stack has been destroyed.");
+				StackLocation sl(army, st->unitSlot());
+				newStackCounts.push_back(TStackAndItsNewCount(sl, 0));
+			}
+			else if(st->getCount() < army->getStackCount(st->unitSlot()))
+			{
+				logGlobal->debug("Stack lost %d units.", army->getStackCount(st->unitSlot()) - st->getCount());
+				StackLocation sl(army, st->unitSlot());
+				newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount()));
+			}
+			else if(st->getCount() > army->getStackCount(st->unitSlot()))
+			{
+				logGlobal->debug("Stack gained %d units.", st->getCount() - army->getStackCount(st->unitSlot()));
+				StackLocation sl(army, st->unitSlot());
+				newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount()));
+			}
+		}
+		else
+		{
+			logGlobal->warn("Unable to process stack: %s", st->nodeName());
+		}
+	}
+}
+
+void CasualtiesAfterBattle::updateArmy(CGameHandler *gh)
+{
+	for (TStackAndItsNewCount &ncount : newStackCounts)
+	{
+		if (ncount.second > 0)
+			gh->changeStackCount(ncount.first, ncount.second, true);
+		else
+			gh->eraseStack(ncount.first, true);
+	}
+	for (auto summoned_iter : summoned)
+	{
+		SlotID slot = army->getSlotFor(summoned_iter.first);
+		if (slot.validSlot())
+		{
+			StackLocation location(army, slot);
+			gh->addToSlot(location, summoned_iter.first.toCreature(), summoned_iter.second);
+		}
+		else
+		{
+			//even if it will be possible to summon anything permanently it should be checked for free slot
+			//necromancy is handled separately
+			gh->complain("No free slot to put summoned creature");
+		}
+	}
+	for (auto al : removedWarMachines)
+	{
+		gh->removeArtifact(al);
+	}
+	if (heroWithDeadCommander != ObjectInstanceID())
+	{
+		SetCommanderProperty scp;
+		scp.heroid = heroWithDeadCommander;
+		scp.which = SetCommanderProperty::ALIVE;
+		scp.amount = 0;
+		gh->sendAndApply(&scp);
+	}
+}
+
+FinishingBattleHelper::FinishingBattleHelper(std::shared_ptr<const CBattleQuery> Query, int remainingBattleQueriesCount)
+{
+	assert(Query->result);
+	assert(Query->bi);
+	auto &result = *Query->result;
+	auto &info = *Query->bi;
+
+	winnerHero = result.winner != 0 ? info.sides[1].hero : info.sides[0].hero;
+	loserHero = result.winner != 0 ? info.sides[0].hero : info.sides[1].hero;
+	victor = info.sides[result.winner].color;
+	loser = info.sides[!result.winner].color;
+	winnerSide = result.winner;
+	this->remainingBattleQueriesCount = remainingBattleQueriesCount;
+}
+
+FinishingBattleHelper::FinishingBattleHelper()
+{
+	winnerHero = loserHero = nullptr;
+	winnerSide = 0;
+	remainingBattleQueriesCount = 0;
+}
+
+void BattleResultProcessor::endBattle(int3 tile, const CGHeroInstance * heroAttacker, const CGHeroInstance * heroDefender)
+{
+	auto const & giveExp = [](BattleResult &r)
+	{
+		if (r.winner > 1)
+		{
+			// draw
+			return;
+		}
+		r.exp[0] = 0;
+		r.exp[1] = 0;
+		for (auto i = r.casualties[!r.winner].begin(); i!=r.casualties[!r.winner].end(); i++)
+		{
+			r.exp[r.winner] += VLC->creh->objects.at(i->first)->valOfBonuses(BonusType::STACK_HEALTH) * i->second;
+		}
+	};
+
+	LOG_TRACE(logGlobal);
+
+	//Fill BattleResult structure with exp info
+	giveExp(*battleResult);
+
+	if (battleResult->result == EBattleResult::NORMAL) // give 500 exp for defeating hero, unless he escaped
+	{
+		if(heroAttacker)
+			battleResult->exp[1] += 500;
+		if(heroDefender)
+			battleResult->exp[0] += 500;
+	}
+
+	if(heroAttacker)
+		battleResult->exp[0] = heroAttacker->calculateXp(battleResult->exp[0]);//scholar skill
+	if(heroDefender)
+		battleResult->exp[1] = heroDefender->calculateXp(battleResult->exp[1]);
+
+	auto battleQuery = std::dynamic_pointer_cast<CBattleQuery>(gameHandler->queries->topQuery(gameHandler->gameState()->curB->sides[0].color));
+	if (!battleQuery)
+	{
+		logGlobal->error("Cannot find battle query!");
+		gameHandler->complain("Player " + boost::lexical_cast<std::string>(gameHandler->gameState()->curB->sides[0].color) + " has no battle query at the top!");
+		return;
+	}
+
+	battleQuery->result = std::make_optional(*battleResult);
+
+	//Check how many battle gameHandler->queries were created (number of players blocked by battle)
+	const int queriedPlayers = battleQuery ? (int)boost::count(gameHandler->queries->allQueries(), battleQuery) : 0;
+	finishingBattle = std::make_unique<FinishingBattleHelper>(battleQuery, queriedPlayers);
+
+	// in battles against neutrals, 1st player can ask to replay battle manually
+	if (!gameHandler->gameState()->curB->sides[1].color.isValidPlayer())
+	{
+		auto battleDialogQuery = std::make_shared<CBattleDialogQuery>(gameHandler, gameHandler->gameState()->curB);
+		battleResult->queryID = battleDialogQuery->queryID;
+		gameHandler->queries->addQuery(battleDialogQuery);
+	}
+	else
+		battleResult->queryID = -1;
+
+	//set same battle result for all gameHandler->queries
+	for(auto q : gameHandler->queries->allQueries())
+	{
+		auto otherBattleQuery = std::dynamic_pointer_cast<CBattleQuery>(q);
+		if(otherBattleQuery)
+			otherBattleQuery->result = battleQuery->result;
+	}
+
+	gameHandler->sendAndApply(battleResult.get()); //after this point casualties objects are destroyed
+
+	if (battleResult->queryID == -1)
+		endBattleConfirm(gameHandler->gameState()->curB);
+}
+
+void BattleResultProcessor::endBattleConfirm(const BattleInfo * battleInfo)
+{
+	auto battleQuery = std::dynamic_pointer_cast<CBattleQuery>(gameHandler->queries->topQuery(battleInfo->sides.at(0).color));
+	if(!battleQuery)
+	{
+		logGlobal->trace("No battle query, battle end was confirmed by another player");
+		return;
+	}
+
+	const EBattleResult result = battleResult.get()->result;
+
+	CasualtiesAfterBattle cab1(battleInfo->sides.at(0), battleInfo), cab2(battleInfo->sides.at(1), battleInfo); //calculate casualties before deleting battle
+	ChangeSpells cs; //for Eagle Eye
+
+	if(!finishingBattle->isDraw() && finishingBattle->winnerHero)
+	{
+		if (int eagleEyeLevel = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_LEVEL_LIMIT, -1))
+		{
+			double eagleEyeChance = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_CHANCE, 0);
+			for(auto & spellId : battleInfo->sides.at(!battleResult->winner).usedSpellsHistory)
+			{
+				auto spell = spellId.toSpell(VLC->spells());
+				if(spell && spell->getLevel() <= eagleEyeLevel && !finishingBattle->winnerHero->spellbookContainsSpell(spell->getId()) && gameHandler->getRandomGenerator().nextInt(99) < eagleEyeChance)
+					cs.spells.insert(spell->getId());
+			}
+		}
+	}
+	std::vector<const CArtifactInstance *> arts; //display them in window
+
+	if(result == EBattleResult::NORMAL && !finishingBattle->isDraw() && finishingBattle->winnerHero)
+	{
+		auto sendMoveArtifact = [&](const CArtifactInstance *art, MoveArtifact *ma)
+		{
+			const auto slot = ArtifactUtils::getArtAnyPosition(finishingBattle->winnerHero, art->getTypeId());
+			if(slot != ArtifactPosition::PRE_FIRST)
+			{
+				arts.push_back(art);
+				ma->dst = ArtifactLocation(finishingBattle->winnerHero, slot);
+				if(ArtifactUtils::isSlotBackpack(slot))
+					ma->askAssemble = false;
+				gameHandler->sendAndApply(ma);
+			}
+		};
+
+		if (finishingBattle->loserHero)
+		{
+			//TODO: wrap it into a function, somehow (std::variant -_-)
+			auto artifactsWorn = finishingBattle->loserHero->artifactsWorn;
+			for (auto artSlot : artifactsWorn)
+			{
+				MoveArtifact ma;
+				ma.src = ArtifactLocation(finishingBattle->loserHero, artSlot.first);
+				const CArtifactInstance * art =  ma.src.getArt();
+				if (art && !art->artType->isBig() &&
+					art->artType->getId() != ArtifactID::SPELLBOOK)
+						// don't move war machines or locked arts (spellbook)
+				{
+					sendMoveArtifact(art, &ma);
+				}
+			}
+			for(int slotNumber = finishingBattle->loserHero->artifactsInBackpack.size() - 1; slotNumber >= 0; slotNumber--)
+			{
+				//we assume that no big artifacts can be found
+				MoveArtifact ma;
+				ma.src = ArtifactLocation(finishingBattle->loserHero,
+					ArtifactPosition(GameConstants::BACKPACK_START + slotNumber)); //backpack automatically shifts arts to beginning
+				const CArtifactInstance * art =  ma.src.getArt();
+				if (art->artType->getId() != ArtifactID::GRAIL) //grail may not be won
+				{
+					sendMoveArtifact(art, &ma);
+				}
+			}
+			if (finishingBattle->loserHero->commander) //TODO: what if commanders belong to no hero?
+			{
+				artifactsWorn = finishingBattle->loserHero->commander->artifactsWorn;
+				for (auto artSlot : artifactsWorn)
+				{
+					MoveArtifact ma;
+					ma.src = ArtifactLocation(finishingBattle->loserHero->commander.get(), artSlot.first);
+					const CArtifactInstance * art =  ma.src.getArt();
+					if (art && !art->artType->isBig())
+					{
+						sendMoveArtifact(art, &ma);
+					}
+				}
+			}
+		}
+		for (auto armySlot : battleInfo->sides.at(!battleResult->winner).armyObject->stacks)
+		{
+			auto artifactsWorn = armySlot.second->artifactsWorn;
+			for (auto artSlot : artifactsWorn)
+			{
+				MoveArtifact ma;
+				ma.src = ArtifactLocation(armySlot.second, artSlot.first);
+				const CArtifactInstance * art =  ma.src.getArt();
+				if (art && !art->artType->isBig())
+				{
+					sendMoveArtifact(art, &ma);
+				}
+			}
+		}
+	}
+
+	if (arts.size()) //display loot
+	{
+		InfoWindow iw;
+		iw.player = finishingBattle->winnerHero->tempOwner;
+
+		iw.text.appendLocalString (EMetaText::GENERAL_TXT, 30); //You have captured enemy artifact
+
+		for (auto art : arts) //TODO; separate function to display loot for various ojects?
+		{
+			iw.components.emplace_back(
+				Component::EComponentType::ARTIFACT, art->artType->getId(),
+				art->artType->getId() == ArtifactID::SPELL_SCROLL? art->getScrollSpellID() : 0, 0);
+			if (iw.components.size() >= 14)
+			{
+				gameHandler->sendAndApply(&iw);
+				iw.components.clear();
+			}
+		}
+		if (iw.components.size())
+		{
+			gameHandler->sendAndApply(&iw);
+		}
+	}
+	//Eagle Eye secondary skill handling
+	if (!cs.spells.empty())
+	{
+		cs.learn = 1;
+		cs.hid = finishingBattle->winnerHero->id;
+
+		InfoWindow iw;
+		iw.player = finishingBattle->winnerHero->tempOwner;
+		iw.text.appendLocalString(EMetaText::GENERAL_TXT, 221); //Through eagle-eyed observation, %s is able to learn %s
+		iw.text.replaceRawString(finishingBattle->winnerHero->getNameTranslated());
+
+		std::ostringstream names;
+		for (int i = 0; i < cs.spells.size(); i++)
+		{
+			names << "%s";
+			if (i < cs.spells.size() - 2)
+				names << ", ";
+			else if (i < cs.spells.size() - 1)
+				names << "%s";
+		}
+		names << ".";
+
+		iw.text.replaceRawString(names.str());
+
+		auto it = cs.spells.begin();
+		for (int i = 0; i < cs.spells.size(); i++, it++)
+		{
+			iw.text.replaceLocalString(EMetaText::SPELL_NAME, it->toEnum());
+			if (i == cs.spells.size() - 2) //we just added pre-last name
+				iw.text.replaceLocalString(EMetaText::GENERAL_TXT, 141); // " and "
+			iw.components.emplace_back(Component::EComponentType::SPELL, *it, 0, 0);
+		}
+		gameHandler->sendAndApply(&iw);
+		gameHandler->sendAndApply(&cs);
+	}
+	cab1.updateArmy(gameHandler);
+	cab2.updateArmy(gameHandler); //take casualties after battle is deleted
+
+	if(finishingBattle->loserHero) //remove beaten hero
+	{
+		RemoveObject ro(finishingBattle->loserHero->id);
+		gameHandler->sendAndApply(&ro);
+	}
+	if(finishingBattle->isDraw() && finishingBattle->winnerHero) //for draw case both heroes should be removed
+	{
+		RemoveObject ro(finishingBattle->winnerHero->id);
+		gameHandler->sendAndApply(&ro);
+	}
+
+	if(battleResult->winner == BattleSide::DEFENDER
+	   && finishingBattle->winnerHero
+	   && finishingBattle->winnerHero->visitedTown
+	   && !finishingBattle->winnerHero->inTownGarrison
+	   && finishingBattle->winnerHero->visitedTown->garrisonHero == finishingBattle->winnerHero)
+	{
+		gameHandler->swapGarrisonOnSiege(finishingBattle->winnerHero->visitedTown->id); //return defending visitor from garrison to its rightful place
+	}
+	//give exp
+	if(!finishingBattle->isDraw() && battleResult->exp[finishingBattle->winnerSide] && finishingBattle->winnerHero)
+		gameHandler->changePrimSkill(finishingBattle->winnerHero, PrimarySkill::EXPERIENCE, battleResult->exp[finishingBattle->winnerSide]);
+
+	BattleResultAccepted raccepted;
+	raccepted.heroResult[0].army = const_cast<CArmedInstance*>(battleInfo->sides.at(0).armyObject);
+	raccepted.heroResult[1].army = const_cast<CArmedInstance*>(battleInfo->sides.at(1).armyObject);
+	raccepted.heroResult[0].hero = const_cast<CGHeroInstance*>(battleInfo->sides.at(0).hero);
+	raccepted.heroResult[1].hero = const_cast<CGHeroInstance*>(battleInfo->sides.at(1).hero);
+	raccepted.heroResult[0].exp = battleResult->exp[0];
+	raccepted.heroResult[1].exp = battleResult->exp[1];
+	raccepted.winnerSide = finishingBattle->winnerSide;
+	gameHandler->sendAndApply(&raccepted);
+
+	gameHandler->queries->popIfTop(battleQuery);
+	//--> continuation (battleAfterLevelUp) occurs after level-up gameHandler->queries are handled or on removing query
+}
+
+void BattleResultProcessor::battleAfterLevelUp(const BattleResult &result)
+{
+	LOG_TRACE(logGlobal);
+
+	if(!finishingBattle)
+		return;
+
+	finishingBattle->remainingBattleQueriesCount--;
+	logGlobal->trace("Decremented gameHandler->queries count to %d", finishingBattle->remainingBattleQueriesCount);
+
+	if (finishingBattle->remainingBattleQueriesCount > 0)
+		//Battle results will be handled when all battle gameHandler->queries are closed
+		return;
+
+	//TODO consider if we really want it to work like above. ATM each player as unblocked as soon as possible
+	// but the battle consequences are applied after final player is unblocked. Hard to abuse...
+	// Still, it looks like a hole.
+
+	// Necromancy if applicable.
+	const CStackBasicDescriptor raisedStack = finishingBattle->winnerHero ? finishingBattle->winnerHero->calculateNecromancy(*battleResult) : CStackBasicDescriptor();
+	// Give raised units to winner and show dialog, if any were raised,
+	// units will be given after casualties are taken
+	const SlotID necroSlot = raisedStack.type ? finishingBattle->winnerHero->getSlotFor(raisedStack.type) : SlotID();
+
+	if (necroSlot != SlotID())
+	{
+		finishingBattle->winnerHero->showNecromancyDialog(raisedStack, gameHandler->getRandomGenerator());
+		gameHandler->addToSlot(StackLocation(finishingBattle->winnerHero, necroSlot), raisedStack.type, raisedStack.count);
+	}
+
+	BattleResultsApplied resultsApplied;
+	resultsApplied.player1 = finishingBattle->victor;
+	resultsApplied.player2 = finishingBattle->loser;
+	gameHandler->sendAndApply(&resultsApplied);
+
+	gameHandler->setBattle(nullptr);
+
+	//handle victory/loss of engaged players
+	std::set<PlayerColor> playerColors = {finishingBattle->loser, finishingBattle->victor};
+	gameHandler->checkVictoryLossConditions(playerColors);
+
+	if (result.result == EBattleResult::SURRENDER)
+		gameHandler->heroPool->onHeroSurrendered(finishingBattle->loser, finishingBattle->loserHero);
+
+	if (result.result == EBattleResult::ESCAPE)
+		gameHandler->heroPool->onHeroEscaped(finishingBattle->loser, finishingBattle->loserHero);
+
+	if (result.winner != 2 && finishingBattle->winnerHero && finishingBattle->winnerHero->stacks.empty()
+		&& (!finishingBattle->winnerHero->commander || !finishingBattle->winnerHero->commander->alive))
+	{
+		RemoveObject ro(finishingBattle->winnerHero->id);
+		gameHandler->sendAndApply(&ro);
+
+		if (VLC->settings()->getBoolean(EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS))
+			gameHandler->heroPool->onHeroEscaped(finishingBattle->victor, finishingBattle->winnerHero);
+	}
+
+	finishingBattle.reset();
+}
+
+void BattleResultProcessor::setBattleResult(EBattleResult resultType, int victoriusSide)
+{
+	battleResult = std::make_unique<BattleResult>();
+	battleResult->result = resultType;
+	battleResult->winner = victoriusSide; //surrendering side loses
+	gameHandler->gameState()->curB->calculateCasualties(battleResult->casualties);
+}

+ 78 - 0
server/battles/BattleResultProcessor.h

@@ -0,0 +1,78 @@
+/*
+ * BattleProcessor.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 "../../lib/GameConstants.h"
+#include "../../lib/NetPacks.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+struct SideInBattle;
+VCMI_LIB_NAMESPACE_END
+
+class CBattleQuery;
+class BattleProcessor;
+class CGameHandler;
+
+struct CasualtiesAfterBattle
+{
+	using TStackAndItsNewCount = std::pair<StackLocation, int>;
+	using TSummoned = std::map<CreatureID, TQuantity>;
+	//	enum {ERASE = -1};
+	const CArmedInstance * army;
+	std::vector<TStackAndItsNewCount> newStackCounts;
+	std::vector<ArtifactLocation> removedWarMachines;
+	TSummoned summoned;
+	ObjectInstanceID heroWithDeadCommander; //TODO: unify stack locations
+
+	CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat);
+	void updateArmy(CGameHandler * gh);
+};
+
+struct FinishingBattleHelper
+{
+	FinishingBattleHelper();
+	FinishingBattleHelper(std::shared_ptr<const CBattleQuery> Query, int RemainingBattleQueriesCount);
+
+	inline bool isDraw() const {return winnerSide == 2;}
+
+	const CGHeroInstance *winnerHero, *loserHero;
+	PlayerColor victor, loser;
+	ui8 winnerSide;
+
+	int remainingBattleQueriesCount;
+
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{
+		h & winnerHero;
+		h & loserHero;
+		h & victor;
+		h & loser;
+		h & winnerSide;
+		h & remainingBattleQueriesCount;
+	}
+};
+
+class BattleResultProcessor : boost::noncopyable
+{
+	//	BattleProcessor * owner;
+	CGameHandler * gameHandler;
+
+	std::unique_ptr<BattleResult> battleResult;
+	std::unique_ptr<FinishingBattleHelper> finishingBattle;
+
+public:
+	explicit BattleResultProcessor(BattleProcessor * owner);
+	void setGameHandler(CGameHandler * newGameHandler);
+
+	void setBattleResult(EBattleResult resultType, int victoriusSide);
+	void endBattle(int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2); //ends battle
+	void endBattleConfirm(const BattleInfo * battleInfo);
+	void battleAfterLevelUp(const BattleResult & result);
+};

+ 11 - 11
server/HeroPoolProcessor.cpp → server/processors/HeroPoolProcessor.cpp

@@ -10,17 +10,17 @@
 #include "StdInc.h"
 #include "HeroPoolProcessor.h"
 
-#include "CGameHandler.h"
-
-#include "../lib/CHeroHandler.h"
-#include "../lib/CPlayerState.h"
-#include "../lib/GameSettings.h"
-#include "../lib/NetPacks.h"
-#include "../lib/StartInfo.h"
-#include "../lib/mapObjects/CGTownInstance.h"
-#include "../lib/gameState/CGameState.h"
-#include "../lib/gameState/TavernHeroesPool.h"
-#include "../lib/gameState/TavernSlot.h"
+#include "../CGameHandler.h"
+
+#include "../../lib/CHeroHandler.h"
+#include "../../lib/CPlayerState.h"
+#include "../../lib/GameSettings.h"
+#include "../../lib/NetPacks.h"
+#include "../../lib/StartInfo.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
+#include "../../lib/gameState/CGameState.h"
+#include "../../lib/gameState/TavernHeroesPool.h"
+#include "../../lib/gameState/TavernSlot.h"
 
 HeroPoolProcessor::HeroPoolProcessor()
 	: gameHandler(nullptr)

+ 0 - 0
server/HeroPoolProcessor.h → server/processors/HeroPoolProcessor.h


+ 30 - 17
server/PlayerMessageProcessor.cpp → server/processors/PlayerMessageProcessor.cpp

@@ -10,18 +10,19 @@
 #include "StdInc.h"
 #include "PlayerMessageProcessor.h"
 
-#include "CGameHandler.h"
-#include "CVCMIServer.h"
-
-#include "../lib/serializer/Connection.h"
-#include "../lib/CGeneralTextHandler.h"
-#include "../lib/CHeroHandler.h"
-#include "../lib/CPlayerState.h"
-#include "../lib/GameConstants.h"
-#include "../lib/NetPacks.h"
-#include "../lib/StartInfo.h"
-#include "../lib/gameState/CGameState.h"
-#include "../lib/mapObjects/CGTownInstance.h"
+#include "../CGameHandler.h"
+#include "../CVCMIServer.h"
+
+#include "../../lib/serializer/Connection.h"
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/CHeroHandler.h"
+#include "../../lib/modding/IdentifierStorage.h"
+#include "../../lib/CPlayerState.h"
+#include "../../lib/GameConstants.h"
+#include "../../lib/NetPacks.h"
+#include "../../lib/StartInfo.h"
+#include "../../lib/gameState/CGameState.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
 #include "../lib/modding/IdentifierStorage.h"
 #include "../lib/modding/ModScope.h"
 
@@ -212,15 +213,27 @@ void PlayerMessageProcessor::cheatGiveMachines(PlayerColor player, const CGHeroI
 		gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::FIRST_AID_TENT], ArtifactPosition::MACH3);
 }
 
-void PlayerMessageProcessor::cheatGiveArtifacts(PlayerColor player, const CGHeroInstance * hero)
+void PlayerMessageProcessor::cheatGiveArtifacts(PlayerColor player, const CGHeroInstance * hero, std::vector<std::string> words)
 {
 	if (!hero)
 		return;
 
-	for(int g = 7; g < VLC->arth->objects.size(); ++g) //including artifacts from mods
+	if (!words.empty())
 	{
-		if(VLC->arth->objects[g]->canBePutAt(hero))
-			gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[g], ArtifactPosition::FIRST_AVAILABLE);
+		for (auto const & word : words)
+		{
+			auto artID = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "artifact", word, false);
+			if(artID &&  VLC->arth->objects[*artID])
+				gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[*artID], ArtifactPosition::FIRST_AVAILABLE);
+		}
+	}
+	else
+	{
+		for(int g = 7; g < VLC->arth->objects.size(); ++g) //including artifacts from mods
+		{
+			if(VLC->arth->objects[g]->canBePutAt(hero))
+				gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[g], ArtifactPosition::FIRST_AVAILABLE);
+		}
 	}
 }
 
@@ -433,7 +446,7 @@ void PlayerMessageProcessor::executeCheatCode(const std::string & cheatName, Pla
 	const auto & doCheatGiveArmyCustom = [&]() { cheatGiveArmy(player, hero, words); };
 	const auto & doCheatGiveArmyFixed = [&](std::vector<std::string> customWords) { cheatGiveArmy(player, hero, customWords); };
 	const auto & doCheatGiveMachines = [&]() { cheatGiveMachines(player, hero); };
-	const auto & doCheatGiveArtifacts = [&]() { cheatGiveArtifacts(player, hero); };
+	const auto & doCheatGiveArtifacts = [&]() { cheatGiveArtifacts(player, hero, words); };
 	const auto & doCheatLevelup = [&]() { cheatLevelup(player, hero, words); };
 	const auto & doCheatExperience = [&]() { cheatExperience(player, hero, words); };
 	const auto & doCheatMovement = [&]() { cheatMovement(player, hero, words); };

+ 2 - 2
server/PlayerMessageProcessor.h → server/processors/PlayerMessageProcessor.h

@@ -9,7 +9,7 @@
  */
 #pragma once
 
-#include "../lib/GameConstants.h"
+#include "../../lib/GameConstants.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 class CGHeroInstance;
@@ -31,7 +31,7 @@ class PlayerMessageProcessor
 	void cheatBuildTown(PlayerColor player, const CGTownInstance * town);
 	void cheatGiveArmy(PlayerColor player, const CGHeroInstance * hero, std::vector<std::string> words);
 	void cheatGiveMachines(PlayerColor player, const CGHeroInstance * hero);
-	void cheatGiveArtifacts(PlayerColor player, const CGHeroInstance * hero);
+	void cheatGiveArtifacts(PlayerColor player, const CGHeroInstance * hero, std::vector<std::string> words);
 	void cheatLevelup(PlayerColor player, const CGHeroInstance * hero, std::vector<std::string> words);
 	void cheatExperience(PlayerColor player, const CGHeroInstance * hero, std::vector<std::string> words);
 	void cheatMovement(PlayerColor player, const CGHeroInstance * hero, std::vector<std::string> words);

+ 75 - 0
server/queries/BattleQueries.cpp

@@ -0,0 +1,75 @@
+/*
+ * BattleQueries.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 "BattleQueries.h"
+#include "MapQueries.h"
+
+#include "../CGameHandler.h"
+#include "../battles/BattleProcessor.h"
+
+#include "../../lib/battle/BattleInfo.h"
+
+void CBattleQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const
+{
+	if(result)
+		objectVisit.visitedObject->battleFinished(objectVisit.visitingHero, *result);
+}
+
+CBattleQuery::CBattleQuery(CGameHandler * owner, const BattleInfo * Bi):
+	CGhQuery(owner)
+{
+	belligerents[0] = Bi->sides[0].armyObject;
+	belligerents[1] = Bi->sides[1].armyObject;
+
+	bi = Bi;
+
+	for(auto & side : bi->sides)
+		addPlayer(side.color);
+}
+
+CBattleQuery::CBattleQuery(CGameHandler * owner):
+	CGhQuery(owner), bi(nullptr)
+{
+	belligerents[0] = belligerents[1] = nullptr;
+}
+
+bool CBattleQuery::blocksPack(const CPack * pack) const
+{
+	const char * name = typeid(*pack).name();
+	return strcmp(name, typeid(MakeAction).name()) != 0;
+}
+
+void CBattleQuery::onRemoval(PlayerColor color)
+{
+	if(result)
+		gh->battles->battleAfterLevelUp(*result);
+}
+
+CBattleDialogQuery::CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi):
+	CDialogQuery(owner)
+{
+	bi = Bi;
+
+	for(auto & side : bi->sides)
+		addPlayer(side.color);
+}
+
+void CBattleDialogQuery::onRemoval(PlayerColor color)
+{
+	assert(answer);
+	if(*answer == 1)
+	{
+		gh->startBattlePrimary(bi->sides[0].armyObject, bi->sides[1].armyObject, bi->tile, bi->sides[0].hero, bi->sides[1].hero, bi->creatureBank, bi->town);
+	}
+	else
+	{
+		gh->battles->endBattleConfirm(bi);
+	}
+}

+ 40 - 0
server/queries/BattleQueries.h

@@ -0,0 +1,40 @@
+/*
+ * BattleQueries.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 "CQuery.h"
+
+#include "../../lib/NetPacks.h"
+
+class CBattleQuery : public CGhQuery
+{
+public:
+	std::array<const CArmedInstance *,2> belligerents;
+	std::array<int, 2> initialHeroMana;
+
+	const BattleInfo *bi;
+	std::optional<BattleResult> result;
+
+	CBattleQuery(CGameHandler * owner);
+	CBattleQuery(CGameHandler * owner, const BattleInfo * Bi); //TODO
+	virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override;
+	virtual bool blocksPack(const CPack *pack) const override;
+	virtual void onRemoval(PlayerColor color) override;
+};
+
+class CBattleDialogQuery : public CDialogQuery
+{
+public:
+	CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi);
+
+	const BattleInfo * bi;
+
+	virtual void onRemoval(PlayerColor color) override;
+};

+ 201 - 0
server/queries/CQuery.cpp

@@ -0,0 +1,201 @@
+/*
+ * CQuery.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 "CQuery.h"
+
+#include "QueriesProcessor.h"
+
+#include "../CGameHandler.h"
+
+#include "../../lib/serializer/Cast.h"
+#include "../../lib/NetPacks.h"
+
+template <typename Container>
+std::string formatContainer(const Container & c, std::string delimeter = ", ", std::string opener = "(", std::string closer=")")
+{
+	std::string ret = opener;
+	auto itr = std::begin(c);
+	if(itr != std::end(c))
+	{
+		ret += std::to_string(*itr);
+		while(++itr != std::end(c))
+		{
+			ret += delimeter;
+			ret += std::to_string(*itr);
+		}
+	}
+	ret += closer;
+	return ret;
+}
+
+std::ostream & operator<<(std::ostream & out, const CQuery & query)
+{
+	return out << query.toString();
+}
+
+std::ostream & operator<<(std::ostream & out, QueryPtr query)
+{
+	return out << "[" << query.get() << "] " << query->toString();
+}
+
+CQuery::CQuery(QueriesProcessor * Owner):
+	owner(Owner)
+{
+	boost::unique_lock<boost::mutex> l(QueriesProcessor::mx);
+
+	static QueryID QID = QueryID(0);
+
+	queryID = ++QID;
+	logGlobal->trace("Created a new query with id %d", queryID);
+}
+
+CQuery::~CQuery()
+{
+	logGlobal->trace("Destructed the query with id %d", queryID);
+}
+
+void CQuery::addPlayer(PlayerColor color)
+{
+	if(color.isValidPlayer())
+		players.push_back(color);
+}
+
+std::string CQuery::toString() const
+{
+	const auto size = players.size();
+	const std::string plural = size > 1 ? "s" : "";
+	std::string names;
+
+	for(size_t i = 0; i < size; i++)
+	{
+		names += boost::to_upper_copy<std::string>(players[i].getStr());
+
+		if(i < size - 2)
+			names += ", ";
+		else if(size > 1 && i == size - 2)
+			names += " and ";
+	}
+	std::string ret = boost::str(boost::format("A query of type '%s' and qid = %d affecting player%s %s")
+		% typeid(*this).name()
+		% queryID 
+		% plural
+		% names
+	);
+	return ret;
+}
+
+bool CQuery::endsByPlayerAnswer() const
+{
+	return false;
+}
+
+void CQuery::onRemoval(PlayerColor color)
+{
+
+}
+
+bool CQuery::blocksPack(const CPack * pack) const
+{
+	return false;
+}
+
+void CQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const
+{
+
+}
+
+void CQuery::onExposure(QueryPtr topQuery)
+{
+	logGlobal->trace("Exposed query with id %d", queryID);
+	owner->popQuery(*this);
+}
+
+void CQuery::onAdding(PlayerColor color)
+{
+
+}
+
+void CQuery::onAdded(PlayerColor color)
+{
+
+}
+
+void CQuery::setReply(const JsonNode & reply)
+{
+
+}
+
+bool CQuery::blockAllButReply(const CPack * pack) const
+{
+	//We accept only query replies from correct player
+	if(auto reply = dynamic_ptr_cast<QueryReply>(pack))
+		return !vstd::contains(players, reply->player);
+
+	return true;
+}
+
+CGhQuery::CGhQuery(CGameHandler * owner):
+	CQuery(owner->queries.get()), gh(owner)
+{
+
+}
+
+CDialogQuery::CDialogQuery(CGameHandler * owner):
+	CGhQuery(owner)
+{
+
+}
+
+bool CDialogQuery::endsByPlayerAnswer() const
+{
+	return true;
+}
+
+bool CDialogQuery::blocksPack(const CPack * pack) const
+{
+	return blockAllButReply(pack);
+}
+
+void CDialogQuery::setReply(const JsonNode & reply)
+{
+	if(reply.getType() == JsonNode::JsonType::DATA_INTEGER)
+		answer = reply.Integer();
+}
+
+CGenericQuery::CGenericQuery(QueriesProcessor * Owner, PlayerColor color, std::function<void(const JsonNode &)> Callback):
+	CQuery(Owner), callback(Callback)
+{
+	addPlayer(color);
+}
+
+bool CGenericQuery::blocksPack(const CPack * pack) const
+{
+	return blockAllButReply(pack);
+}
+
+bool CGenericQuery::endsByPlayerAnswer() const
+{
+	return true;
+}
+
+void CGenericQuery::onExposure(QueryPtr topQuery)
+{
+	//do nothing
+}
+
+void CGenericQuery::setReply(const JsonNode & reply)
+{
+	this->reply = reply;
+}
+
+void CGenericQuery::onRemoval(PlayerColor color)
+{
+	callback(reply);
+}

+ 99 - 0
server/queries/CQuery.h

@@ -0,0 +1,99 @@
+/*
+ * CQuery.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 "../../lib/GameConstants.h"
+#include "../../lib/JsonNode.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+struct CPack;
+
+VCMI_LIB_NAMESPACE_END
+
+class CObjectVisitQuery;
+class QueriesProcessor;
+class CQuery;
+
+using QueryPtr = std::shared_ptr<CQuery>;
+
+// This class represents any kind of prolonged interaction that may need to do something special after it is over.
+// It does not necessarily has to be "query" requiring player action, it can be also used internally within server.
+// Examples:
+// - all kinds of blocking dialog windows
+// - battle
+// - object visit
+// - hero movement
+// Queries can cause another queries, forming a stack of queries for each player. Eg: hero movement -> object visit -> dialog.
+class CQuery
+{
+public:
+	std::vector<PlayerColor> players; //players that are affected (often "blocked") by query
+	QueryID queryID;
+
+	CQuery(QueriesProcessor * Owner);
+
+
+	virtual bool blocksPack(const CPack *pack) const; //query can block attempting actions by player. Eg. he can't move hero during the battle.
+
+	virtual bool endsByPlayerAnswer() const; //query is removed after player gives answer (like dialogs)
+	virtual void onAdding(PlayerColor color); //called just before query is pushed on stack
+	virtual void onAdded(PlayerColor color); //called right after query is pushed on stack
+	virtual void onRemoval(PlayerColor color); //called after query is removed from stack
+	virtual void onExposure(QueryPtr topQuery);//called when query immediately above is removed and this is exposed (becomes top)
+	virtual std::string toString() const;
+
+	virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const;
+
+	virtual void setReply(const JsonNode & reply);
+
+	virtual ~CQuery();
+protected:
+	QueriesProcessor * owner;
+	void addPlayer(PlayerColor color);
+	bool blockAllButReply(const CPack * pack) const;
+};
+
+std::ostream &operator<<(std::ostream &out, const CQuery &query);
+std::ostream &operator<<(std::ostream &out, QueryPtr query);
+
+class CGhQuery : public CQuery
+{
+public:
+	CGhQuery(CGameHandler * owner);
+protected:
+	CGameHandler * gh;
+};
+
+class CDialogQuery : public CGhQuery
+{
+public:
+	CDialogQuery(CGameHandler * owner);
+	virtual bool endsByPlayerAnswer() const override;
+	virtual bool blocksPack(const CPack *pack) const override;
+	void setReply(const JsonNode & reply) override;
+protected:
+	std::optional<ui32> answer;
+};
+
+class CGenericQuery : public CQuery
+{
+public:
+	CGenericQuery(QueriesProcessor * Owner, PlayerColor color, std::function<void(const JsonNode &)> Callback);
+
+	bool blocksPack(const CPack * pack) const override;
+	bool endsByPlayerAnswer() const override;
+	void onExposure(QueryPtr topQuery) override;
+	void setReply(const JsonNode & reply) override;
+	void onRemoval(PlayerColor color) override;
+private:
+	std::function<void(const JsonNode &)> callback;
+	JsonNode reply;
+};

+ 227 - 0
server/queries/MapQueries.cpp

@@ -0,0 +1,227 @@
+/*
+ * MapQueries.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 "MapQueries.h"
+
+#include "QueriesProcessor.h"
+#include "../CGameHandler.h"
+#include "../../lib/mapObjects/MiscObjects.h"
+#include "../../lib/serializer/Cast.h"
+
+CObjectVisitQuery::CObjectVisitQuery(CGameHandler * owner, const CGObjectInstance * Obj, const CGHeroInstance * Hero, int3 Tile):
+	CGhQuery(owner), visitedObject(Obj), visitingHero(Hero), tile(Tile), removeObjectAfterVisit(false)
+{
+	addPlayer(Hero->tempOwner);
+}
+
+bool CObjectVisitQuery::blocksPack(const CPack *pack) const
+{
+	//During the visit itself ALL actions are blocked.
+	//(However, the visit may trigger a query above that'll pass some.)
+	return true;
+}
+
+void CObjectVisitQuery::onRemoval(PlayerColor color)
+{
+	gh->objectVisitEnded(*this);
+
+	//TODO or should it be destructor?
+	//Can object visit affect 2 players and what would be desired behavior?
+	if(removeObjectAfterVisit)
+		gh->removeObject(visitedObject);
+}
+
+void CObjectVisitQuery::onExposure(QueryPtr topQuery)
+{
+	//Object may have been removed and deleted.
+	if(gh->isValidObject(visitedObject))
+		topQuery->notifyObjectAboutRemoval(*this);
+
+	owner->popIfTop(*this);
+}
+
+void CGarrisonDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const
+{
+	objectVisit.visitedObject->garrisonDialogClosed(objectVisit.visitingHero);
+}
+
+CGarrisonDialogQuery::CGarrisonDialogQuery(CGameHandler * owner, const CArmedInstance * up, const CArmedInstance * down):
+	CDialogQuery(owner)
+{
+	exchangingArmies[0] = up;
+	exchangingArmies[1] = down;
+
+	addPlayer(up->tempOwner);
+	addPlayer(down->tempOwner);
+}
+
+bool CGarrisonDialogQuery::blocksPack(const CPack * pack) const
+{
+	std::set<ObjectInstanceID> ourIds;
+	ourIds.insert(this->exchangingArmies[0]->id);
+	ourIds.insert(this->exchangingArmies[1]->id);
+
+	if(auto stacks = dynamic_ptr_cast<ArrangeStacks>(pack))
+		return !vstd::contains(ourIds, stacks->id1) || !vstd::contains(ourIds, stacks->id2);
+
+	if(auto stacks = dynamic_ptr_cast<BulkSplitStack>(pack))
+		return !vstd::contains(ourIds, stacks->srcOwner);
+
+	if(auto stacks = dynamic_ptr_cast<BulkMergeStacks>(pack))
+		return !vstd::contains(ourIds, stacks->srcOwner);
+
+	if(auto stacks = dynamic_ptr_cast<BulkSmartSplitStack>(pack))
+		return !vstd::contains(ourIds, stacks->srcOwner);
+
+	if(auto stacks = dynamic_ptr_cast<BulkMoveArmy>(pack))
+		return !vstd::contains(ourIds, stacks->srcArmy) || !vstd::contains(ourIds, stacks->destArmy);
+
+	if(auto arts = dynamic_ptr_cast<ExchangeArtifacts>(pack))
+	{
+		if(auto id1 = std::visit(GetEngagedHeroIds(), arts->src.artHolder))
+			if(!vstd::contains(ourIds, *id1))
+				return true;
+
+		if(auto id2 = std::visit(GetEngagedHeroIds(), arts->dst.artHolder))
+			if(!vstd::contains(ourIds, *id2))
+				return true;
+		return false;
+	}
+	if(auto dismiss = dynamic_ptr_cast<DisbandCreature>(pack))
+		return !vstd::contains(ourIds, dismiss->id);
+
+	if(auto arts = dynamic_ptr_cast<BulkExchangeArtifacts>(pack))
+		return !vstd::contains(ourIds, arts->srcHero) || !vstd::contains(ourIds, arts->dstHero);
+
+	if(auto art = dynamic_ptr_cast<EraseArtifactByClient>(pack))
+	{
+		if (auto id = std::visit(GetEngagedHeroIds(), art->al.artHolder))
+			return !vstd::contains(ourIds, *id);
+	}
+
+	if(auto dismiss = dynamic_ptr_cast<AssembleArtifacts>(pack))
+		return !vstd::contains(ourIds, dismiss->heroID);
+
+	if(auto upgrade = dynamic_ptr_cast<UpgradeCreature>(pack))
+		return !vstd::contains(ourIds, upgrade->id);
+
+	if(auto formation = dynamic_ptr_cast<SetFormation>(pack))
+		return !vstd::contains(ourIds, formation->hid);
+
+	return CDialogQuery::blocksPack(pack);
+}
+
+void CBlockingDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const
+{
+	assert(answer);
+	objectVisit.visitedObject->blockingDialogAnswered(objectVisit.visitingHero, *answer);
+}
+
+CBlockingDialogQuery::CBlockingDialogQuery(CGameHandler * owner, const BlockingDialog & bd):
+	CDialogQuery(owner)
+{
+	this->bd = bd;
+	addPlayer(bd.player);
+}
+
+void CTeleportDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const
+{
+	// do not change to dynamic_ptr_cast - SIGSEGV!
+	auto obj = dynamic_cast<const CGTeleport*>(objectVisit.visitedObject);
+	if(obj)
+		obj->teleportDialogAnswered(objectVisit.visitingHero, *answer, td.exits);
+	else
+		logGlobal->error("Invalid instance in teleport query");
+}
+
+CTeleportDialogQuery::CTeleportDialogQuery(CGameHandler * owner, const TeleportDialog & td):
+	CDialogQuery(owner)
+{
+	this->td = td;
+	addPlayer(td.player);
+}
+
+CHeroLevelUpDialogQuery::CHeroLevelUpDialogQuery(CGameHandler * owner, const HeroLevelUp & Hlu, const CGHeroInstance * Hero):
+	CDialogQuery(owner), hero(Hero)
+{
+	hlu = Hlu;
+	addPlayer(hero->tempOwner);
+}
+
+void CHeroLevelUpDialogQuery::onRemoval(PlayerColor color)
+{
+	assert(answer);
+	logGlobal->trace("Completing hero level-up query. %s gains skill %d", hero->getObjectName(), answer.value());
+	gh->levelUpHero(hero, hlu.skills[*answer]);
+}
+
+void CHeroLevelUpDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const
+{
+	objectVisit.visitedObject->heroLevelUpDone(objectVisit.visitingHero);
+}
+
+CCommanderLevelUpDialogQuery::CCommanderLevelUpDialogQuery(CGameHandler * owner, const CommanderLevelUp & Clu, const CGHeroInstance * Hero):
+	CDialogQuery(owner), hero(Hero)
+{
+	clu = Clu;
+	addPlayer(hero->tempOwner);
+}
+
+void CCommanderLevelUpDialogQuery::onRemoval(PlayerColor color)
+{
+	assert(answer);
+	logGlobal->trace("Completing commander level-up query. Commander of hero %s gains skill %s", hero->getObjectName(), answer.value());
+	gh->levelUpCommander(hero->commander, clu.skills[*answer]);
+}
+
+void CCommanderLevelUpDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const
+{
+	objectVisit.visitedObject->heroLevelUpDone(objectVisit.visitingHero);
+}
+
+CHeroMovementQuery::CHeroMovementQuery(CGameHandler * owner, const TryMoveHero & Tmh, const CGHeroInstance * Hero, bool VisitDestAfterVictory):
+	CGhQuery(owner), tmh(Tmh), visitDestAfterVictory(VisitDestAfterVictory), hero(Hero)
+{
+	players.push_back(hero->tempOwner);
+}
+
+void CHeroMovementQuery::onExposure(QueryPtr topQuery)
+{
+	assert(players.size() == 1);
+
+	if(visitDestAfterVictory && hero->tempOwner == players[0]) //hero still alive, so he won with the guard
+		//TODO what if there were H4-like escape? we should also check pos
+	{
+		logGlobal->trace("Hero %s after victory over guard finishes visit to %s", hero->getNameTranslated(), tmh.end.toString());
+		//finish movement
+		visitDestAfterVictory = false;
+		gh->visitObjectOnTile(*gh->getTile(hero->convertToVisitablePos(tmh.end)), hero);
+	}
+
+	owner->popIfTop(*this);
+}
+
+void CHeroMovementQuery::onRemoval(PlayerColor color)
+{
+	PlayerBlocked pb;
+	pb.player = color;
+	pb.reason = PlayerBlocked::ONGOING_MOVEMENT;
+	pb.startOrEnd = PlayerBlocked::BLOCKADE_ENDED;
+	gh->sendAndApply(&pb);
+}
+
+void CHeroMovementQuery::onAdding(PlayerColor color)
+{
+	PlayerBlocked pb;
+	pb.player = color;
+	pb.reason = PlayerBlocked::ONGOING_MOVEMENT;
+	pb.startOrEnd = PlayerBlocked::BLOCKADE_STARTED;
+	gh->sendAndApply(&pb);
+}

+ 103 - 0
server/queries/MapQueries.h

@@ -0,0 +1,103 @@
+/*
+ * MapQueries.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 "CQuery.h"
+
+#include "../../lib/NetPacks.h"
+
+//Created when hero visits object.
+//Removed when query above is resolved (or immediately after visit if no queries were created)
+class CObjectVisitQuery : public CGhQuery
+{
+public:
+	const CGObjectInstance *visitedObject;
+	const CGHeroInstance *visitingHero;
+	int3 tile; //may be different than hero pos -> eg. visit via teleport
+	bool removeObjectAfterVisit;
+
+	CObjectVisitQuery(CGameHandler * owner, const CGObjectInstance *Obj, const CGHeroInstance *Hero, int3 Tile);
+
+	virtual bool blocksPack(const CPack *pack) const override;
+	virtual void onRemoval(PlayerColor color) override;
+	virtual void onExposure(QueryPtr topQuery) override;
+};
+
+//Created when hero attempts move and something happens
+//(not necessarily position change, could be just an object interaction).
+class CHeroMovementQuery : public CGhQuery
+{
+public:
+	TryMoveHero tmh;
+	bool visitDestAfterVictory; //if hero moved to guarded tile and it should be visited once guard is defeated
+	const CGHeroInstance *hero;
+
+	virtual void onExposure(QueryPtr topQuery) override;
+
+	CHeroMovementQuery(CGameHandler * owner, const TryMoveHero & Tmh, const CGHeroInstance * Hero, bool VisitDestAfterVictory = false);
+	virtual void onAdding(PlayerColor color) override;
+	virtual void onRemoval(PlayerColor color) override;
+};
+
+
+class CGarrisonDialogQuery : public CDialogQuery //used also for hero exchange dialogs
+{
+public:
+	std::array<const CArmedInstance *,2> exchangingArmies;
+
+	CGarrisonDialogQuery(CGameHandler * owner, const CArmedInstance *up, const CArmedInstance *down);
+	virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override;
+	virtual bool blocksPack(const CPack *pack) const override;
+};
+
+//yes/no and component selection dialogs
+class CBlockingDialogQuery : public CDialogQuery
+{
+public:
+	BlockingDialog bd; //copy of pack... debug purposes
+
+	CBlockingDialogQuery(CGameHandler * owner, const BlockingDialog &bd);
+
+	virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override;
+};
+
+class CTeleportDialogQuery : public CDialogQuery
+{
+public:
+	TeleportDialog td; //copy of pack... debug purposes
+
+	CTeleportDialogQuery(CGameHandler * owner, const TeleportDialog &td);
+
+	virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override;
+};
+
+class CHeroLevelUpDialogQuery : public CDialogQuery
+{
+public:
+	CHeroLevelUpDialogQuery(CGameHandler * owner, const HeroLevelUp &Hlu, const CGHeroInstance * Hero);
+
+	virtual void onRemoval(PlayerColor color) override;
+	virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override;
+
+	HeroLevelUp hlu;
+	const CGHeroInstance * hero;
+};
+
+class CCommanderLevelUpDialogQuery : public CDialogQuery
+{
+public:
+	CCommanderLevelUpDialogQuery(CGameHandler * owner, const CommanderLevelUp &Clu, const CGHeroInstance * Hero);
+
+	virtual void onRemoval(PlayerColor color) override;
+	virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override;
+
+	CommanderLevelUp clu;
+	const CGHeroInstance * hero;
+};

+ 129 - 0
server/queries/QueriesProcessor.cpp

@@ -0,0 +1,129 @@
+/*
+ * CQuery.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 "QueriesProcessor.h"
+
+#include "CQuery.h"
+
+boost::mutex QueriesProcessor::mx;
+
+void QueriesProcessor::popQuery(PlayerColor player, QueryPtr query)
+{
+	LOG_TRACE_PARAMS(logGlobal, "player='%s', query='%s'", player % query);
+	if(topQuery(player) != query)
+	{
+		logGlobal->trace("Cannot remove, not a top!");
+		return;
+	}
+
+	queries[player] -= query;
+	auto nextQuery = topQuery(player);
+
+	query->onRemoval(player);
+
+	//Exposure on query below happens only if removal didn't trigger any new query
+	if(nextQuery && nextQuery == topQuery(player))
+		nextQuery->onExposure(query);
+}
+
+void QueriesProcessor::popQuery(const CQuery &query)
+{
+	LOG_TRACE_PARAMS(logGlobal, "query='%s'", query);
+
+	assert(query.players.size());
+	for(auto player : query.players)
+	{
+		auto top = topQuery(player);
+		if(top.get() == &query)
+			popQuery(top);
+		else
+		{
+			logGlobal->trace("Cannot remove query %s", query.toString());
+			logGlobal->trace("Queries found:");
+			for(auto q : queries[player])
+			{
+				logGlobal->trace(q->toString());
+			}
+		}
+	}
+}
+
+void QueriesProcessor::popQuery(QueryPtr query)
+{
+	for(auto player : query->players)
+		popQuery(player, query);
+}
+
+void QueriesProcessor::addQuery(QueryPtr query)
+{
+	for(auto player : query->players)
+		addQuery(player, query);
+
+	for(auto player : query->players)
+		query->onAdded(player);
+}
+
+void QueriesProcessor::addQuery(PlayerColor player, QueryPtr query)
+{
+	LOG_TRACE_PARAMS(logGlobal, "player='%d', query='%s'", player.getNum() % query);
+	query->onAdding(player);
+	queries[player].push_back(query);
+}
+
+QueryPtr QueriesProcessor::topQuery(PlayerColor player)
+{
+	return vstd::backOrNull(queries[player]);
+}
+
+void QueriesProcessor::popIfTop(QueryPtr query)
+{
+	LOG_TRACE_PARAMS(logGlobal, "query='%d'", query);
+	if(!query)
+		logGlobal->error("The query is nullptr! Ignoring.");
+
+	popIfTop(*query);
+}
+
+void QueriesProcessor::popIfTop(const CQuery & query)
+{
+	for(PlayerColor color : query.players)
+		if(topQuery(color).get() == &query)
+			popQuery(color, topQuery(color));
+}
+
+std::vector<std::shared_ptr<const CQuery>> QueriesProcessor::allQueries() const
+{
+	std::vector<std::shared_ptr<const CQuery>> ret;
+	for(auto & playerQueries : queries)
+		for(auto & query : playerQueries.second)
+			ret.push_back(query);
+
+	return ret;
+}
+
+std::vector<QueryPtr> QueriesProcessor::allQueries()
+{
+	//TODO code duplication with const function :(
+	std::vector<QueryPtr> ret;
+	for(auto & playerQueries : queries)
+		for(auto & query : playerQueries.second)
+			ret.push_back(query);
+
+	return ret;
+}
+
+QueryPtr QueriesProcessor::getQuery(QueryID queryID)
+{
+	for(auto & playerQueries : queries)
+		for(auto & query : playerQueries.second)
+			if(query->queryID == queryID)
+				return query;
+	return nullptr;
+}

+ 40 - 0
server/queries/QueriesProcessor.h

@@ -0,0 +1,40 @@
+/*
+ * QueriesProcessor.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 "../../lib/GameConstants.h"
+
+class CQuery;
+using QueryPtr = std::shared_ptr<CQuery>;
+
+class QueriesProcessor
+{
+private:
+	void addQuery(PlayerColor player, QueryPtr query);
+	void popQuery(PlayerColor player, QueryPtr query);
+
+	std::map<PlayerColor, std::vector<QueryPtr>> queries; //player => stack of queries
+
+public:
+	static boost::mutex mx;
+
+	void addQuery(QueryPtr query);
+	void popQuery(const CQuery &query);
+	void popQuery(QueryPtr query);
+	void popIfTop(const CQuery &query); //removes this query if it is at the top (otherwise, do nothing)
+	void popIfTop(QueryPtr query); //removes this query if it is at the top (otherwise, do nothing)
+
+	QueryPtr topQuery(PlayerColor player);
+
+	std::vector<std::shared_ptr<const CQuery>> allQueries() const;
+	std::vector<QueryPtr> allQueries();
+	QueryPtr getQuery(QueryID queryID);
+	//void removeQuery
+};

Някои файлове не бяха показани, защото твърде много файлове са промени