Răsfoiți Sursa

Merge pull request #2425 from IvanSavenko/fix_combat_replay

Attempt to fix combat replay queries
Ivan Savenko 2 ani în urmă
părinte
comite
6c52293dd9

+ 1 - 1
AI/BattleAI/BattleAI.cpp

@@ -826,7 +826,7 @@ void CBattleAI::evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcas
 	ps.value = totalGain;
 }
 
-void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side)
+void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
 {
 	LOG_TRACE(logAi);
 	side = Side;

+ 1 - 1
AI/BattleAI/BattleAI.h

@@ -83,7 +83,7 @@ public:
 	BattleAction selectStackAction(const CStack * stack);
 	std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack *stack);
 
-	void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool Side) override;
+	void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool Side, bool replayAllowed) override;
 	//void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero
 	//void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero
 	//void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack

+ 13 - 9
AI/Nullkiller/AIGateway.cpp

@@ -809,7 +809,7 @@ void AIGateway::makeTurn()
 		for (auto h : cb->getHeroesInfo())
 		{
 			if (h->movementPointsRemaining())
-				logAi->warn("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining());
+				logAi->info("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining());
 		}
 #if NKAI_TRACE_LEVEL == 0
 	}
@@ -1065,14 +1065,14 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re
 	}
 }
 
-void AIGateway::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side)
+void AIGateway::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed)
 {
 	NET_EVENT_HANDLER;
 	assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE);
 	status.setBattle(ONGOING_BATTLE);
 	const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit
 	battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString());
-	CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side);
+	CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side, replayAllowed);
 }
 
 void AIGateway::battleEnd(const BattleResult * br, QueryID queryID)
@@ -1083,12 +1083,16 @@ void AIGateway::battleEnd(const BattleResult * br, QueryID queryID)
 	bool won = br->winner == myCb->battleGetMySide();
 	logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename);
 	battlename.clear();
-	status.addQuery(queryID, "Combat result dialog");
-	const int confirmAction = 0;
-	requestActionASAP([=]()
+
+	if (queryID != -1)
 	{
-		answerQuery(queryID, confirmAction);
-	});
+		status.addQuery(queryID, "Combat result dialog");
+		const int confirmAction = 0;
+		requestActionASAP([=]()
+		{
+			answerQuery(queryID, confirmAction);
+		});
+	}
 	CAdventureAI::battleEnd(br, queryID);
 }
 
@@ -1175,7 +1179,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 	if(startHpos == dst)
 	{
 		//FIXME: this assertion fails also if AI moves onto defeated guarded object
-		assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object
+		//assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object
 		cb->moveHero(*h, h->convertFromVisitablePos(dst));
 		afterMovementCheck(); // TODO: is it feasible to hero get killed there if game work properly?
 		// If revisiting, teleport probing is never done, and so the entries into the list would remain unused and uncleared

+ 1 - 1
AI/Nullkiller/AIGateway.h

@@ -169,7 +169,7 @@ public:
 	void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
 	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override;
 
-	void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) override;
+	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;
 
 	void makeTurn();

+ 1 - 1
AI/StupidAI/StupidAI.cpp

@@ -242,7 +242,7 @@ void CStupidAI::battleStacksEffectsSet(const SetStackEffect & sse)
 	print("battleStacksEffectsSet called");
 }
 
-void CStupidAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side)
+void CStupidAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
 {
 	print("battleStart called");
 	side = Side;

+ 1 - 1
AI/StupidAI/StupidAI.h

@@ -44,7 +44,7 @@ public:
 	void battleSpellCast(const BattleSpellCast *sc) override;
 	void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks
 	//void battleTriggerEffect(const BattleTriggerEffect & bte) override;
-	void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; //called by engine when battle starts; side=0 - left, side=1 - right
+	void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right
 	void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack
 
 private:

+ 12 - 8
AI/VCAI/VCAI.cpp

@@ -819,7 +819,7 @@ void VCAI::makeTurn()
 		for (auto h : cb->getHeroesInfo())
 		{
 			if (h->movementPointsRemaining())
-				logAi->warn("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining());
+				logAi->info("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining());
 		}
 	}
 	catch (boost::thread_interrupted & e)
@@ -1575,14 +1575,14 @@ void VCAI::completeGoal(Goals::TSubgoal goal)
 
 }
 
-void VCAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side)
+void VCAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed)
 {
 	NET_EVENT_HANDLER;
 	assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE);
 	status.setBattle(ONGOING_BATTLE);
 	const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit
 	battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString());
-	CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side);
+	CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side, replayAllowed);
 }
 
 void VCAI::battleEnd(const BattleResult * br, QueryID queryID)
@@ -1593,12 +1593,16 @@ void VCAI::battleEnd(const BattleResult * br, QueryID queryID)
 	bool won = br->winner == myCb->battleGetMySide();
 	logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename);
 	battlename.clear();
-	status.addQuery(queryID, "Combat result dialog");
-	const int confirmAction = 0;
-	requestActionASAP([=]()
+
+	if (queryID != -1)
 	{
-		answerQuery(queryID, confirmAction);
-	});
+		status.addQuery(queryID, "Combat result dialog");
+		const int confirmAction = 0;
+		requestActionASAP([=]()
+		{
+			answerQuery(queryID, confirmAction);
+		});
+	}
 	CAdventureAI::battleEnd(br, queryID);
 }
 

+ 1 - 1
AI/VCAI/VCAI.h

@@ -201,7 +201,7 @@ public:
 	void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) override;
 	void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
 
-	void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) override;
+	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;
 
 	void makeTurn();

+ 16 - 18
client/CPlayerInterface.cpp

@@ -652,26 +652,20 @@ void CPlayerInterface::battleStartBefore(const CCreatureSet *army1, const CCreat
 		waitForAllDialogs();
 }
 
-void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side)
+void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	bool autoBattleResultRefused = (lastBattleArmies.first == army1 && lastBattleArmies.second == army2);
-	lastBattleArmies.first = army1;
-	lastBattleArmies.second = army2;
-	//quick combat with neutral creatures only
-	auto * army2_object = dynamic_cast<const CGObjectInstance *>(army2);
-	if((!autoBattleResultRefused && !allowBattleReplay && army2_object
-		&& (army2_object->getOwner() == PlayerColor::UNFLAGGABLE || army2_object->getOwner() == PlayerColor::NEUTRAL)
-		&& settings["adventure"]["quickCombat"].Bool())
-		|| settings["adventure"]["alwaysSkipCombat"].Bool())
+
+	bool useQuickCombat = settings["adventure"]["quickCombat"].Bool();
+	bool forceQuickCombat = settings["adventure"]["forceQuickCombat"].Bool();
+
+	if ((replayAllowed && useQuickCombat) || forceQuickCombat)
 	{
 		autofightingAI = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String());
 		autofightingAI->initBattleInterface(env, cb);
-		autofightingAI->battleStart(army1, army2, int3(0,0,0), hero1, hero2, side);
+		autofightingAI->battleStart(army1, army2, tile, hero1, hero2, side, false);
 		isAutoFightOn = true;
 		cb->registerBattleInterface(autofightingAI);
-		// Player shouldn't be able to move on adventure map if quick combat is going
-		allowBattleReplay = true;
 	}
 
 	//Don't wait for dialogs when we are non-active hot-seat player
@@ -843,13 +837,17 @@ void CPlayerInterface::battleEnd(const BattleResult *br, QueryID queryID)
 
 		if(!battleInt)
 		{
-			bool allowManualReplay = allowBattleReplay && !settings["adventure"]["alwaysSkipCombat"].Bool();
-			allowBattleReplay = false;
+			bool allowManualReplay = queryID != -1;
+
 			auto wnd = std::make_shared<BattleResultWindow>(*br, *this, allowManualReplay);
-			wnd->resultCallback = [=](ui32 selection)
+
+			if (allowManualReplay)
 			{
-				cb->selectionMade(selection, queryID);
-			};
+				wnd->resultCallback = [=](ui32 selection)
+				{
+					cb->selectionMade(selection, queryID);
+				};
+			}
 			GH.windows().pushWindow(wnd);
 			// #1490 - during AI turn when quick combat is on, we need to display the message and wait for user to close it.
 			// Otherwise NewTurn causes freeze.

+ 1 - 3
client/CPlayerInterface.h

@@ -65,8 +65,6 @@ class CPlayerInterface : public CGameInterface, public IUpdateable
 	int firstCall;
 	int autosaveCount;
 
-	std::pair<const CCreatureSet *, const CCreatureSet *> lastBattleArmies;
-	bool allowBattleReplay = false;
 	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)
 
@@ -169,7 +167,7 @@ protected: // Call-ins from server, should not be called directly, but only via
 	void battleTriggerEffect(const BattleTriggerEffect & bte) override; //various one-shot effect
 	void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) override;
 	void battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) override; //called by engine just before battle starts; side=0 - left, side=1 - right
-	void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; //called by engine when battle starts; side=0 - left, side=1 - right
+	void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right
 	void battleUnitsChanged(const std::vector<UnitChanges> & units) override;
 	void battleObstaclesChanged(const std::vector<ObstacleChanges> & obstacles) override;
 	void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack

+ 1 - 1
client/Client.cpp

@@ -580,7 +580,7 @@ void CClient::battleStarted(const BattleInfo * info)
 	auto callBattleStart = [&](PlayerColor color, ui8 side)
 	{
 		if(vstd::contains(battleints, color))
-			battleints[color]->battleStart(leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side);
+			battleints[color]->battleStart(leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side, info->replayAllowed);
 	};
 	
 	callBattleStart(leftSide.color, 0);

+ 1 - 1
client/battle/BattleWindow.cpp

@@ -501,7 +501,7 @@ void BattleWindow::bAutofightf()
 
 		auto ai = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String());
 		ai->initBattleInterface(owner.curInt->env, owner.curInt->cb);
-		ai->battleStart(owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.curInt->cb->battleGetMySide());
+		ai->battleStart(owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.curInt->cb->battleGetMySide(), false);
 		owner.curInt->autofightingAI = ai;
 		owner.curInt->cb->registerBattleInterface(ai);
 

+ 2 - 2
config/schemas/settings.json

@@ -223,7 +223,7 @@
 			"type" : "object",
 			"additionalProperties" : false,
 			"default" : {},
-			"required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "alwaysSkipCombat", "borderScroll", "leftButtonDrag" ],
+			"required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag" ],
 			"properties" : {
 				"heroMoveTime" : {
 					"type" : "number",
@@ -253,7 +253,7 @@
 					"type" : "boolean",
 					"default" : true
 				},
-				"alwaysSkipCombat" : {
+				"forceQuickCombat" : {
 					"type" : "boolean",
 					"default" : false
 				},

+ 2 - 2
lib/CGameInterface.cpp

@@ -168,13 +168,13 @@ void CAdventureAI::battleCatapultAttacked(const CatapultAttack & ca)
 }
 
 void CAdventureAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile,
-							   const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side)
+							   const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed)
 {
 	assert(!battleAI);
 	assert(cbc);
 	battleAI = CDynLibHandler::getNewBattleAI(getBattleAIName());
 	battleAI->initBattleInterface(env, cbc);
-	battleAI->battleStart(army1, army2, tile, hero1, hero2, side);
+	battleAI->battleStart(army1, army2, tile, hero1, hero2, side, replayAllowed);
 }
 
 void CAdventureAI::battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged)

+ 1 - 1
lib/CGameInterface.h

@@ -149,7 +149,7 @@ public:
 	virtual void yourTacticPhase(int distance) override;
 	virtual void battleNewRound(int round) override;
 	virtual void battleCatapultAttacked(const CatapultAttack & ca) override;
-	virtual void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override;
+	virtual void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override;
 	virtual void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) override;
 	virtual void actionStarted(const BattleAction &action) override;
 	virtual void battleNewRoundFirst(int round) override;

+ 1 - 1
lib/IGameEventsReceiver.h

@@ -69,7 +69,7 @@ public:
 	virtual void battleStacksEffectsSet(const SetStackEffect & sse){};//called when a specific effect is set to stacks
 	virtual void battleTriggerEffect(const BattleTriggerEffect & bte){}; //called for various one-shot effects
 	virtual void battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) {}; //called just before battle start
-	virtual void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side){}; //called by engine when battle starts; side=0 - left, side=1 - right
+	virtual void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed){}; //called by engine when battle starts; side=0 - left, side=1 - right
 	virtual void battleUnitsChanged(const std::vector<UnitChanges> & units){};
 	virtual void battleObstaclesChanged(const std::vector<ObstacleChanges> & obstacles){};
 	virtual void battleCatapultAttacked(const CatapultAttack & ca){}; //called when catapult makes an attack

+ 1 - 0
lib/battle/BattleInfo.cpp

@@ -207,6 +207,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 	curB->round = -2;
 	curB->activeStack = -1;
 	curB->creatureBank = creatureBank;
+	curB->replayAllowed = false;
 
 	if(town)
 	{

+ 5 - 0
lib/battle/BattleInfo.h

@@ -36,6 +36,7 @@ public:
 	const CGTownInstance * town; //used during town siege, nullptr if this is not a siege (note that fortless town IS also a siege)
 	int3 tile; //for background and bonuses
 	bool creatureBank; //auxilary field, do not serialize
+	bool replayAllowed;
 	std::vector<CStack*> stacks;
 	std::vector<std::shared_ptr<CObstacleInstance> > obstacles;
 	SiegeInfo si;
@@ -61,6 +62,10 @@ public:
 		h & tacticsSide;
 		h & tacticDistance;
 		h & static_cast<CBonusSystemNode&>(*this);
+		if (version > 824)
+			h & replayAllowed;
+		else
+			replayAllowed = false;
 	}
 
 	//////////////////////////////////////////////////////////////////////////

+ 1 - 1
lib/serializer/CSerializer.h

@@ -14,7 +14,7 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-const ui32 SERIALIZATION_VERSION = 824;
+const ui32 SERIALIZATION_VERSION = 825;
 const ui32 MINIMAL_SERIALIZATION_VERSION = 824;
 const std::string SAVEGAME_MAGIC = "VCMISVG";
 

+ 27 - 14
server/CGameHandler.cpp

@@ -607,9 +607,15 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con
 	const int queriedPlayers = battleQuery ? (int)boost::count(queries.allQueries(), battleQuery) : 0;
 	finishingBattle = std::make_unique<FinishingBattleHelper>(battleQuery, queriedPlayers);
 	
-	auto battleDialogQuery = std::make_shared<CBattleDialogQuery>(this, gs->curB);
-	battleResult.data->queryID = battleDialogQuery->queryID;
-	queries.addQuery(battleDialogQuery);
+	// in battles against neutrals, 1st player can ask to replay battle manually
+	if (!gs->curB->sides[1].color.isValidPlayer())
+	{
+		auto battleDialogQuery = std::make_shared<CBattleDialogQuery>(this, gs->curB);
+		battleResult.data->queryID = battleDialogQuery->queryID;
+		queries.addQuery(battleDialogQuery);
+	}
+	else
+		battleResult.data->queryID = -1;
 	
 	//set same battle result for all queries
 	for(auto q : queries.allQueries())
@@ -620,6 +626,9 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con
 	}
 	
 	sendAndApply(battleResult.data); //after this point casualties objects are destroyed
+
+	if (battleResult.data->queryID == -1)
+		endBattleConfirm(gs->curB);
 }
 
 void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo)
@@ -2118,6 +2127,10 @@ void CGameHandler::setupBattle(int3 tile, const CArmedInstance *armies[2], const
 	//send info about battles
 	BattleStart bs;
 	bs.info = BattleInfo::setupBattle(tile, terrain, terType, armies, heroes, creatureBank, town);
+
+	auto lastBattleQuery = std::dynamic_pointer_cast<CBattleQuery>(queries.topQuery(bs.info->sides[0].color));
+	bs.info->replayAllowed = lastBattleQuery == nullptr && !bs.info->sides[1].color.isValidPlayer();
+
 	sendAndApply(&bs);
 }
 
@@ -2587,39 +2600,39 @@ void CGameHandler::startBattlePrimary(const CArmedInstance *army1, const CArmedI
 	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>(queries.topQuery(gs->curB->sides[0].color));
+
 	//existing battle query for retying auto-combat
-	auto battleQuery = std::dynamic_pointer_cast<CBattleQuery>(queries.topQuery(gs->curB->sides[0].color));
-	if(battleQuery)
+	if(lastBattleQuery)
 	{
 		for(int i : {0, 1})
 		{
 			if(heroes[i])
 			{
 				SetMana restoreInitialMana;
-				restoreInitialMana.val = battleQuery->initialHeroMana[i];
+				restoreInitialMana.val = lastBattleQuery->initialHeroMana[i];
 				restoreInitialMana.hid = heroes[i]->id;
 				sendAndApply(&restoreInitialMana);
 			}
 		}
 		
-		battleQuery->bi = gs->curB;
-		battleQuery->result = std::nullopt;
-		battleQuery->belligerents[0] = gs->curB->sides[0].armyObject;
-		battleQuery->belligerents[1] = gs->curB->sides[1].armyObject;
+		lastBattleQuery->bi = gs->curB;
+		lastBattleQuery->result = std::nullopt;
+		lastBattleQuery->belligerents[0] = gs->curB->sides[0].armyObject;
+		lastBattleQuery->belligerents[1] = gs->curB->sides[1].armyObject;
 	}
 
-	battleQuery = std::make_shared<CBattleQuery>(this, gs->curB);
+	auto nextBattleQuery = std::make_shared<CBattleQuery>(this, gs->curB);
 	for(int i : {0, 1})
 	{
 		if(heroes[i])
 		{
-			battleQuery->initialHeroMana[i] = heroes[i]->mana;
+			nextBattleQuery->initialHeroMana[i] = heroes[i]->mana;
 		}
 	}
-	queries.addQuery(battleQuery);
+	queries.addQuery(nextBattleQuery);
 
 	this->battleThread = std::make_unique<boost::thread>(boost::thread(&CGameHandler::runBattle, this));
 }

+ 1 - 1
server/CQuery.cpp

@@ -176,7 +176,7 @@ void CObjectVisitQuery::onExposure(QueryPtr topQuery)
 	if(gh->isValidObject(visitedObject))
 		topQuery->notifyObjectAboutRemoval(*this);
 
-	owner->popQuery(*this);
+	owner->popIfTop(*this);
 }
 
 void Queries::popQuery(PlayerColor player, QueryPtr query)