Browse Source

Merge pull request #2673 from IvanSavenko/concurrent_battles

Concurrent battles support
Ivan Savenko 2 năm trước cách đây
mục cha
commit
5f07f3f091
100 tập tin đã thay đổi với 1682 bổ sung1266 xóa
  1. 27 27
      AI/BattleAI/BattleAI.cpp
  2. 6 6
      AI/BattleAI/BattleAI.h
  3. 25 25
      AI/BattleAI/BattleEvaluator.cpp
  4. 6 3
      AI/BattleAI/BattleEvaluator.h
  5. 1 1
      AI/BattleAI/BattleExchangeVariant.h
  6. 26 2
      AI/BattleAI/StackWithBonuses.cpp
  7. 7 2
      AI/BattleAI/StackWithBonuses.h
  8. 6 6
      AI/EmptyAI/CEmptyAI.cpp
  9. 3 3
      AI/EmptyAI/CEmptyAI.h
  10. 8 7
      AI/Nullkiller/AIGateway.cpp
  11. 3 3
      AI/Nullkiller/AIGateway.h
  12. 37 34
      AI/StupidAI/StupidAI.cpp
  13. 18 16
      AI/StupidAI/StupidAI.h
  14. 7 7
      AI/VCAI/VCAI.cpp
  15. 3 3
      AI/VCAI/VCAI.h
  16. 44 27
      CCallback.cpp
  17. 26 17
      CCallback.h
  18. 37 37
      client/CPlayerInterface.cpp
  19. 21 21
      client/CPlayerInterface.h
  20. 20 30
      client/Client.cpp
  21. 6 7
      client/Client.h
  22. 32 32
      client/NetPacksClient.cpp
  23. 19 19
      client/battle/BattleActionsController.cpp
  24. 1 1
      client/battle/BattleAnimationClasses.cpp
  25. 2 2
      client/battle/BattleEffectsController.cpp
  26. 11 11
      client/battle/BattleFieldController.cpp
  27. 34 23
      client/battle/BattleInterface.cpp
  28. 10 3
      client/battle/BattleInterface.h
  29. 6 6
      client/battle/BattleInterfaceClasses.cpp
  30. 5 5
      client/battle/BattleObstacleController.cpp
  31. 9 9
      client/battle/BattleSiegeController.cpp
  32. 8 10
      client/battle/BattleStacksController.cpp
  33. 12 12
      client/battle/BattleWindow.cpp
  34. 1 1
      client/windows/CSpellWindow.cpp
  35. 6 6
      client/windows/GUIClasses.cpp
  36. 0 2
      cmake_modules/VCMI_lib.cmake
  37. 2 1
      include/vcmi/Environment.h
  38. 47 44
      lib/CGameInfoCallback.cpp
  39. 7 5
      lib/CGameInfoCallback.h
  40. 32 32
      lib/CGameInterface.cpp
  41. 20 19
      lib/CGameInterface.h
  42. 18 18
      lib/IGameEventsReceiver.h
  43. 83 3
      lib/NetPacks.h
  44. 47 43
      lib/NetPacksLib.cpp
  45. 33 34
      lib/battle/BattleInfo.cpp
  46. 14 5
      lib/battle/BattleInfo.h
  47. 10 3
      lib/battle/BattleProxy.cpp
  48. 2 0
      lib/battle/BattleProxy.h
  49. 21 0
      lib/battle/CBattleInfoCallback.cpp
  50. 2 1
      lib/battle/CBattleInfoCallback.h
  51. 10 5
      lib/battle/CBattleInfoEssentials.cpp
  52. 2 2
      lib/battle/CBattleInfoEssentials.h
  53. 0 42
      lib/battle/CCallbackBase.cpp
  54. 0 45
      lib/battle/CCallbackBase.h
  55. 29 4
      lib/battle/CPlayerBattleCallback.cpp
  56. 12 0
      lib/battle/CPlayerBattleCallback.h
  57. 7 0
      lib/battle/IBattleInfoCallback.h
  58. 9 3
      lib/battle/IBattleState.h
  59. 1 0
      lib/constants/EntityIdentifiers.cpp
  60. 6 0
      lib/constants/EntityIdentifiers.h
  61. 33 4
      lib/gameState/CGameState.cpp
  62. 12 1
      lib/gameState/CGameState.h
  63. 1 1
      lib/pathfinder/CPathfinder.cpp
  64. 1 0
      lib/registerTypes/RegisterTypes.h
  65. 6 2
      lib/spells/BattleSpellMechanics.cpp
  66. 1 1
      lib/spells/BattleSpellMechanics.h
  67. 2 0
      lib/spells/effects/Catapult.cpp
  68. 5 0
      lib/spells/effects/Clone.cpp
  69. 4 0
      lib/spells/effects/Damage.cpp
  70. 2 0
      lib/spells/effects/DemonSummon.cpp
  71. 3 0
      lib/spells/effects/Dispel.cpp
  72. 4 0
      lib/spells/effects/Heal.cpp
  73. 1 0
      lib/spells/effects/Sacrifice.cpp
  74. 2 0
      lib/spells/effects/Summon.cpp
  75. 3 0
      lib/spells/effects/Timed.cpp
  76. 1 1
      scripting/lua/LuaScriptingContext.cpp
  77. 8 6
      server/CGameHandler.cpp
  78. 3 6
      server/CGameHandler.h
  79. 2 2
      server/NetPacksServer.cpp
  80. 46 27
      server/TurnTimerHandler.cpp
  81. 6 5
      server/TurnTimerHandler.h
  82. 161 144
      server/battles/BattleActionProcessor.cpp
  83. 28 27
      server/battles/BattleActionProcessor.h
  84. 114 111
      server/battles/BattleFlowProcessor.cpp
  85. 23 18
      server/battles/BattleFlowProcessor.h
  86. 103 57
      server/battles/BattleProcessor.cpp
  87. 12 10
      server/battles/BattleProcessor.h
  88. 94 58
      server/battles/BattleResultProcessor.cpp
  89. 10 11
      server/battles/BattleResultProcessor.h
  90. 0 0
      server/battles/ServerBattleCallback.cpp
  91. 0 0
      server/battles/ServerBattleCallback.h
  92. 39 18
      server/queries/BattleQueries.cpp
  93. 9 4
      server/queries/BattleQueries.h
  94. 9 3
      test/battle/CBattleInfoCallbackTest.cpp
  95. 12 12
      test/game/CGameStateTest.cpp
  96. 0 1
      test/mock/BattleFake.cpp
  97. 11 0
      test/mock/BattleFake.h
  98. 1 1
      test/mock/mock_Environment.h
  99. 2 0
      test/mock/mock_IBattleInfoCallback.h
  100. 1 0
      test/mock/mock_IGameInfoCallback.h

+ 27 - 27
AI/BattleAI/BattleAI.cpp

@@ -52,7 +52,7 @@ void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::share
 	setCbc(CB);
 	env = ENV;
 	cb = CB;
-	playerID = *CB->getPlayerID(); //TODO should be sth in callback
+	playerID = *CB->getPlayerID();
 	wasWaitingForRealize = CB->waitTillRealize;
 	wasUnlockingGs = CB->unlockGsWhenWaiting;
 	CB->waitTillRealize = false;
@@ -66,9 +66,9 @@ void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::share
 	autobattlePreferences = autocombatPreferences;
 }
 
-BattleAction CBattleAI::useHealingTent(const CStack *stack)
+BattleAction CBattleAI::useHealingTent(const BattleID & battleID, const CStack *stack)
 {
-	auto healingTargets = cb->battleGetStacks(CBattleInfoEssentials::ONLY_MINE);
+	auto healingTargets = cb->getBattle(battleID)->battleGetStacks(CBattleInfoEssentials::ONLY_MINE);
 	std::map<int, const CStack*> woundHpToStack;
 	for(const auto * stack : healingTargets)
 	{
@@ -82,12 +82,12 @@ BattleAction CBattleAI::useHealingTent(const CStack *stack)
 		return BattleAction::makeHeal(stack, woundHpToStack.rbegin()->second); //last element of the woundHpToStack is the most wounded stack
 }
 
-void CBattleAI::yourTacticPhase(int distance)
+void CBattleAI::yourTacticPhase(const BattleID & battleID, int distance)
 {
-	cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide()));
+	cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide()));
 }
 
-float getStrengthRatio(std::shared_ptr<CBattleCallback> cb, int side)
+static float getStrengthRatio(std::shared_ptr<CBattleInfoCallback> cb, int side)
 {
 	auto stacks = cb->battleGetAllStacks();
 	auto our = 0, enemy = 0;
@@ -108,7 +108,7 @@ float getStrengthRatio(std::shared_ptr<CBattleCallback> cb, int side)
 	return enemy == 0 ? 1.0f : static_cast<float>(our) / enemy;
 }
 
-void CBattleAI::activeStack(const CStack * stack )
+void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
 {
 	LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName());
 
@@ -128,12 +128,12 @@ void CBattleAI::activeStack(const CStack * stack )
 	{
 		if(stack->creatureId() == CreatureID::CATAPULT)
 		{
-			cb->battleMakeUnitAction(useCatapult(stack));
+			cb->battleMakeUnitAction(battleID, useCatapult(battleID, stack));
 			return;
 		}
 		if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON) && stack->hasBonusOfType(BonusType::HEALER))
 		{
-			cb->battleMakeUnitAction(useHealingTent(stack));
+			cb->battleMakeUnitAction(battleID, useHealingTent(battleID, stack));
 			return;
 		}
 
@@ -141,7 +141,7 @@ void CBattleAI::activeStack(const CStack * stack )
 		logAi->trace("Build evaluator and targets");
 #endif
 
-		BattleEvaluator evaluator(env, cb, stack, playerID, side, getStrengthRatio(cb, side));
+		BattleEvaluator evaluator(env, cb, stack, playerID, battleID, side, getStrengthRatio(cb->getBattle(battleID), side));
 
 		result = evaluator.selectStackAction(stack);
 
@@ -157,9 +157,9 @@ void CBattleAI::activeStack(const CStack * stack )
 
 		logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start));
 
-		if(auto action = considerFleeingOrSurrendering())
+		if(auto action = considerFleeingOrSurrendering(battleID))
 		{
-			cb->battleMakeUnitAction(*action);
+			cb->battleMakeUnitAction(battleID, *action);
 			return;
 		}
 	}
@@ -183,17 +183,17 @@ void CBattleAI::activeStack(const CStack * stack )
 
 	logAi->trace("BattleAI decission made in %lld", timeElapsed(start));
 
-	cb->battleMakeUnitAction(result);
+	cb->battleMakeUnitAction(battleID, result);
 }
 
-BattleAction CBattleAI::useCatapult(const CStack * stack)
+BattleAction CBattleAI::useCatapult(const BattleID & battleID, const CStack * stack)
 {
 	BattleAction attack;
 	BattleHex targetHex = BattleHex::INVALID;
 
-	if(cb->battleGetGateState() == EGateState::CLOSED)
+	if(cb->getBattle(battleID)->battleGetGateState() == EGateState::CLOSED)
 	{
-		targetHex = cb->wallPartToBattleHex(EWallPart::GATE);
+		targetHex = cb->getBattle(battleID)->wallPartToBattleHex(EWallPart::GATE);
 	}
 	else
 	{
@@ -209,11 +209,11 @@ BattleAction CBattleAI::useCatapult(const CStack * stack)
 
 		for(auto wallPart : wallParts)
 		{
-			auto wallState = cb->battleGetWallState(wallPart);
+			auto wallState = cb->getBattle(battleID)->battleGetWallState(wallPart);
 
 			if(wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED)
 			{
-				targetHex = cb->wallPartToBattleHex(wallPart);
+				targetHex = cb->getBattle(battleID)->wallPartToBattleHex(wallPart);
 				break;
 			}
 		}
@@ -234,7 +234,7 @@ BattleAction CBattleAI::useCatapult(const CStack * stack)
 	return attack;
 }
 
-void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
+void CBattleAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
 {
 	LOG_TRACE(logAi);
 	side = Side;
@@ -247,17 +247,17 @@ void CBattleAI::print(const std::string &text) const
 	logAi->trace("%s Battle AI[%p]: %s", playerID.toString(), this, text);
 }
 
-std::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering()
+std::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering(const BattleID & battleID)
 {
 	BattleStateInfoForRetreat bs;
 
-	bs.canFlee = cb->battleCanFlee();
-	bs.canSurrender = cb->battleCanSurrender(playerID);
-	bs.ourSide = cb->battleGetMySide();
-	bs.ourHero = cb->battleGetMyHero(); 
+	bs.canFlee = cb->getBattle(battleID)->battleCanFlee();
+	bs.canSurrender = cb->getBattle(battleID)->battleCanSurrender(playerID);
+	bs.ourSide = cb->getBattle(battleID)->battleGetMySide();
+	bs.ourHero = cb->getBattle(battleID)->battleGetMyHero();
 	bs.enemyHero = nullptr;
 
-	for(auto stack : cb->battleGetAllStacks(false))
+	for(auto stack : cb->getBattle(battleID)->battleGetAllStacks(false))
 	{
 		if(stack->alive())
 		{
@@ -266,7 +266,7 @@ std::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering()
 			else
 			{
 				bs.enemyStacks.push_back(stack);
-				bs.enemyHero = cb->battleGetOwnerHero(stack);
+				bs.enemyHero = cb->getBattle(battleID)->battleGetOwnerHero(stack);
 			}
 		}
 	}
@@ -278,7 +278,7 @@ std::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering()
 		return std::nullopt;
 	}
 
-	auto result = cb->makeSurrenderRetreatDecision(bs);
+	auto result = cb->makeSurrenderRetreatDecision(battleID, bs);
 
 	if(!result && bs.canFlee && bs.turnsSkippedByDefense > 30)
 	{

+ 6 - 6
AI/BattleAI/BattleAI.h

@@ -71,16 +71,16 @@ public:
 	void initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB) override;
 	void initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB, AutocombatPreferences autocombatPreferences) override;
 
-	void activeStack(const CStack * stack) override; //called when it's turn of that stack
-	void yourTacticPhase(int distance) override;
+	void activeStack(const BattleID & battleID, const CStack * stack) override; //called when it's turn of that stack
+	void yourTacticPhase(const BattleID & battleID, int distance) override;
 
-	std::optional<BattleAction> considerFleeingOrSurrendering();
+	std::optional<BattleAction> considerFleeingOrSurrendering(const BattleID & battleID);
 
 	void print(const std::string &text) const;
-	BattleAction useCatapult(const CStack *stack);
-	BattleAction useHealingTent(const CStack *stack);
+	BattleAction useCatapult(const BattleID & battleID, const CStack *stack);
+	BattleAction useHealingTent(const BattleID & battleID, const CStack *stack);
 
-	void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool Side, bool replayAllowed) override;
+	void battleStart(const BattleID & battleID, 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

+ 25 - 25
AI/BattleAI/BattleEvaluator.cpp

@@ -53,12 +53,12 @@ std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const
 
 	for(EWallPart wallPart : { EWallPart::BOTTOM_WALL, EWallPart::BELOW_GATE, EWallPart::OVER_GATE, EWallPart::UPPER_WALL })
 	{
-		auto state = cb->battleGetWallState(wallPart);
+		auto state = cb->getBattle(battleID)->battleGetWallState(wallPart);
 
 		if(state != EWallState::DESTROYED)
 			continue;
 
-		auto wallHex = cb->wallPartToBattleHex((EWallPart)wallPart);
+		auto wallHex = cb->getBattle(battleID)->wallPartToBattleHex((EWallPart)wallPart);
 		auto moatHex = wallHex.cloneInDirection(BattleHex::LEFT);
 
 		result.push_back(moatHex);
@@ -70,15 +70,15 @@ std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const
 std::optional<PossibleSpellcast> BattleEvaluator::findBestCreatureSpell(const CStack *stack)
 {
 	//TODO: faerie dragon type spell should be selected by server
-	SpellID creatureSpellToCast = cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), stack, CBattleInfoCallback::RANDOM_AIMED);
+	SpellID creatureSpellToCast = cb->getBattle(battleID)->battleGetRandomStackSpell(CRandomGenerator::getDefault(), stack, CBattleInfoCallback::RANDOM_AIMED);
 	if(stack->hasBonusOfType(BonusType::SPELLCASTER) && stack->canCast() && creatureSpellToCast != SpellID::NONE)
 	{
 		const CSpell * spell = creatureSpellToCast.toSpell();
 
-		if(spell->canBeCast(getCbc().get(), spells::Mode::CREATURE_ACTIVE, stack))
+		if(spell->canBeCast(cb->getBattle(battleID).get(), spells::Mode::CREATURE_ACTIVE, stack))
 		{
 			std::vector<PossibleSpellcast> possibleCasts;
-			spells::BattleCast temp(getCbc().get(), stack, spells::Mode::CREATURE_ACTIVE, spell);
+			spells::BattleCast temp(cb->getBattle(battleID).get(), stack, spells::Mode::CREATURE_ACTIVE, spell);
 			for(auto & target : temp.findPotentialTargets())
 			{
 				PossibleSpellcast ps;
@@ -201,7 +201,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 	if(score <= EvaluationResult::INEFFECTIVE_SCORE
 		&& !stack->hasBonusOfType(BonusType::FLYING)
 		&& stack->unitSide() == BattleSide::ATTACKER
-		&& cb->battleGetSiegeLevel() >= CGTownInstance::CITADEL)
+		&& cb->getBattle(battleID)->battleGetSiegeLevel() >= CGTownInstance::CITADEL)
 	{
 		auto brokenWallMoat = getBrokenWallMoatHexes();
 
@@ -228,8 +228,8 @@ uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock>
 
 BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes)
 {
-	auto reachability = cb->getReachability(stack);
-	auto avHexes = cb->battleGetAvailableHexes(reachability, stack, false);
+	auto reachability = cb->getBattle(battleID)->getReachability(stack);
+	auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false);
 
 	if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked
 	{
@@ -325,16 +325,16 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
 
 bool BattleEvaluator::canCastSpell()
 {
-	auto hero = cb->battleGetMyHero();
+	auto hero = cb->getBattle(battleID)->battleGetMyHero();
 	if(!hero)
 		return false;
 
-	return cb->battleCanCastSpell(hero, spells::Mode::HERO) == ESpellCastProblem::OK;
+	return cb->getBattle(battleID)->battleCanCastSpell(hero, spells::Mode::HERO) == ESpellCastProblem::OK;
 }
 
 bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 {
-	auto hero = cb->battleGetMyHero();
+	auto hero = cb->getBattle(battleID)->battleGetMyHero();
 	if(!hero)
 		return false;
 
@@ -343,7 +343,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 	std::vector<const CSpell*> possibleSpells;
 	vstd::copy_if(VLC->spellh->objects, std::back_inserter(possibleSpells), [hero, this](const CSpell *s) -> bool
 	{
-		return s->canBeCast(cb.get(), spells::Mode::HERO, hero);
+		return s->canBeCast(cb->getBattle(battleID).get(), spells::Mode::HERO, hero);
 	});
 	LOGFL("I can cast %d spells.", possibleSpells.size());
 
@@ -358,7 +358,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 	std::vector<PossibleSpellcast> possibleCasts;
 	for(auto spell : possibleSpells)
 	{
-		spells::BattleCast temp(cb.get(), hero, spells::Mode::HERO, spell);
+		spells::BattleCast temp(cb->getBattle(battleID).get(), hero, spells::Mode::HERO, spell);
 
 		if(spell->getTargetType() == spells::AimType::LOCATION)
 			continue;
@@ -390,7 +390,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 		for(auto & round : queue)
 		{
 			if(!firstRound)
-				state->nextRound(0);//todo: set actual value?
+				state->nextRound();
 			for(auto unit : round)
 			{
 				if(!vstd::contains(values, unit->unitId()))
@@ -468,7 +468,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 	ValueMap valueOfStack;
 	ValueMap healthOfStack;
 
-	TStacks all = cb->battleGetAllStacks(false);
+	TStacks all = cb->getBattle(battleID)->battleGetAllStacks(false);
 
 	size_t ourRemainingTurns = 0;
 
@@ -477,7 +477,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 		healthOfStack[unit->unitId()] = unit->getAvailableHealth();
 		valueOfStack[unit->unitId()] = 0;
 
-		if(cb->battleGetOwner(unit) == playerID && unit->canMove() && !unit->moved())
+		if(cb->getBattle(battleID)->battleGetOwner(unit) == playerID && unit->canMove() && !unit->moved())
 			ourRemainingTurns++;
 	}
 
@@ -494,12 +494,12 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 
 	std::vector<battle::Units> turnOrder;
 
-	cb->battleGetTurnOrder(turnOrder, amount, 2); //no more than 1 turn after current, each unit at least once
+	cb->getBattle(battleID)->battleGetTurnOrder(turnOrder, amount, 2); //no more than 1 turn after current, each unit at least once
 
 	{
 		bool enemyHadTurn = false;
 
-		auto state = std::make_shared<HypotheticBattle>(env.get(), cb);
+		auto state = std::make_shared<HypotheticBattle>(env.get(), cb->getBattle(battleID));
 
 		evaluateQueue(valueOfStack, turnOrder, state, 0, &enemyHadTurn);
 
@@ -531,7 +531,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 				logAi->trace("Evaluating %s", ps.spell->getNameTranslated());
 #endif
 
-				auto state = std::make_shared<HypotheticBattle>(env.get(), cb);
+				auto state = std::make_shared<HypotheticBattle>(env.get(), cb->getBattle(battleID));
 
 				spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell);
 				cast.castEval(state->getServerCallback(), ps.dest);
@@ -540,7 +540,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 
 				auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool
 					{
-						auto original = cb->battleGetUnitByID(u->unitId());
+						auto original = cb->getBattle(battleID)->battleGetUnitByID(u->unitId());
 						return  !original || u->speed() != original->speed();
 					});
 
@@ -583,7 +583,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 					if(oldHealth != newHealth)
 					{
 						auto damage = std::abs(oldHealth - newHealth);
-						auto originalDefender = cb->battleGetUnitByID(unit->unitId());
+						auto originalDefender = cb->getBattle(battleID)->battleGetUnitByID(unit->unitId());
 
 						auto dpsReduce = AttackPossibility::calculateDamageReduce(
 							nullptr,
@@ -639,7 +639,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 		spellcast.setTarget(castToPerform.dest);
 		spellcast.side = side;
 		spellcast.stackNumber = (!side) ? -1 : -2;
-		cb->battleMakeSpellAction(spellcast);
+		cb->battleMakeSpellAction(battleID, spellcast);
 		activeActionMade = true;
 
 		return true;
@@ -656,8 +656,8 @@ void BattleEvaluator::evaluateCreatureSpellcast(const CStack * stack, PossibleSp
 	using ValueMap = PossibleSpellcast::ValueMap;
 
 	RNGStub rngStub;
-	HypotheticBattle state(env.get(), cb);
-	TStacks all = cb->battleGetAllStacks(false);
+	HypotheticBattle state(env.get(), cb->getBattle(battleID));
+	TStacks all = cb->getBattle(battleID)->battleGetAllStacks(false);
 
 	ValueMap healthOfStack;
 	ValueMap newHealthOfStack;
@@ -686,7 +686,7 @@ void BattleEvaluator::evaluateCreatureSpellcast(const CStack * stack, PossibleSp
 
 		auto healthDiff = newHealthOfStack[unitId] - healthOfStack[unitId];
 
-		if(localUnit->unitOwner() != getCbc()->getPlayerID())
+		if(localUnit->unitOwner() != cb->getBattle(battleID)->getPlayerID())
 			healthDiff = -healthDiff;
 
 		if(healthDiff < 0)

+ 6 - 3
AI/BattleAI/BattleEvaluator.h

@@ -32,6 +32,7 @@ class BattleEvaluator
 	bool activeActionMade = false;
 	std::optional<AttackPossibility> cachedAttack;
 	PlayerColor playerID;
+	BattleID battleID;
 	int side;
 	float cachedScore;
 	DamageCache damageCache;
@@ -52,11 +53,12 @@ public:
 		std::shared_ptr<CBattleCallback> cb,
 		const battle::Unit * activeStack,
 		PlayerColor playerID,
+		BattleID battleID,
 		int side,
 		float strengthRatio)
-		:scoreEvaluator(cb, env, strengthRatio), cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), strengthRatio(strengthRatio)
+		:scoreEvaluator(cb->getBattle(battleID), env, strengthRatio), cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), strengthRatio(strengthRatio), battleID(battleID)
 	{
-		hb = std::make_shared<HypotheticBattle>(env.get(), cb);
+		hb = std::make_shared<HypotheticBattle>(env.get(), cb->getBattle(battleID));
 		damageCache.buildDamageCache(hb, side);
 
 		targets = std::make_unique<PotentialTargets>(activeStack, damageCache, hb);
@@ -70,9 +72,10 @@ public:
 		DamageCache & damageCache,
 		const battle::Unit * activeStack,
 		PlayerColor playerID,
+		BattleID battleID,
 		int side,
 		float strengthRatio)
-		:scoreEvaluator(cb, env, strengthRatio), cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), hb(hb), damageCache(damageCache), strengthRatio(strengthRatio)
+		:scoreEvaluator(cb->getBattle(battleID), env, strengthRatio), cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), hb(hb), damageCache(damageCache), strengthRatio(strengthRatio), battleID(battleID)
 	{
 		targets = std::make_unique<PotentialTargets>(activeStack, damageCache, hb);
 		cachedScore = EvaluationResult::INEFFECTIVE_SCORE;

+ 1 - 1
AI/BattleAI/BattleExchangeVariant.h

@@ -133,4 +133,4 @@ public:
 
 	float getPositiveEffectMultiplier() { return 1; }
 	float getNegativeEffectMultiplier() { return negativeEffectMultiplier; }
-};
+};

+ 26 - 2
AI/BattleAI/StackWithBonuses.cpp

@@ -320,12 +320,17 @@ battle::Units HypotheticBattle::getUnitsIf(battle::UnitFilter predicate) const
 	return ret;
 }
 
+BattleID HypotheticBattle::getBattleID() const
+{
+	return subject->getBattle()->getBattleID();
+}
+
 int32_t HypotheticBattle::getActiveStackID() const
 {
 	return activeUnitId;
 }
 
-void HypotheticBattle::nextRound(int32_t roundNr)
+void HypotheticBattle::nextRound()
 {
 	//TODO:HypotheticBattle::nextRound
 	for(auto unit : battleAliveUnits())
@@ -462,6 +467,24 @@ int64_t HypotheticBattle::getActualDamage(const DamageRange & damage, int32_t at
 	return (damage.min + damage.max) / 2;
 }
 
+std::vector<SpellID> HypotheticBattle::getUsedSpells(ui8 side) const
+{
+	// TODO
+	return {};
+}
+
+int3 HypotheticBattle::getLocation() const
+{
+	// TODO
+	return int3(-1, -1, -1);
+}
+
+bool HypotheticBattle::isCreatureBank() const
+{
+	// TODO
+	return false;
+}
+
 int64_t HypotheticBattle::getTreeVersion() const
 {
 	return getBonusBearer()->getTreeVersion() + bonusTreeVersion;
@@ -552,8 +575,9 @@ const Services * HypotheticBattle::HypotheticEnvironment::services() const
 	return env->services();
 }
 
-const Environment::BattleCb * HypotheticBattle::HypotheticEnvironment::battle() const
+const Environment::BattleCb * HypotheticBattle::HypotheticEnvironment::battle(const BattleID & battleID) const
 {
+	assert(battleID == owner->getBattleID());
 	return owner;
 }
 

+ 7 - 2
AI/BattleAI/StackWithBonuses.h

@@ -110,11 +110,13 @@ public:
 
 	std::shared_ptr<StackWithBonuses> getForUpdate(uint32_t id);
 
+	BattleID getBattleID() const override;
+
 	int32_t getActiveStackID() const override;
 
 	battle::Units getUnitsIf(battle::UnitFilter predicate) const override;
 
-	void nextRound(int32_t roundNr) override;
+	void nextRound() override;
 	void nextTurn(uint32_t unitId) override;
 
 	void addUnit(uint32_t id, const JsonNode & data) override;
@@ -136,6 +138,9 @@ public:
 	uint32_t nextUnitId() const override;
 
 	int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override;
+	std::vector<SpellID> getUsedSpells(ui8 side) const override;
+	int3 getLocation() const override;
+	bool isCreatureBank() const override;
 
 	int64_t getTreeVersion() const;
 
@@ -177,7 +182,7 @@ private:
 		HypotheticEnvironment(HypotheticBattle * owner_, const Environment * upperEnvironment);
 
 		const Services * services() const override;
-		const BattleCb * battle() const override;
+		const BattleCb * battle(const BattleID & battleID) const override;
 		const GameCb * game() const override;
 		vstd::CLoggerBase * logger() const override;
 		events::EventBus * eventBus() const override;

+ 6 - 6
AI/EmptyAI/CEmptyAI.cpp

@@ -27,7 +27,7 @@ void CEmptyAI::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_p
 	cb = CB;
 	env = ENV;
 	human=false;
-	playerID = *cb->getMyColor();
+	playerID = *cb->getPlayerID();
 }
 
 void CEmptyAI::yourTurn(QueryID queryID)
@@ -36,14 +36,14 @@ void CEmptyAI::yourTurn(QueryID queryID)
 	cb->endTurn();
 }
 
-void CEmptyAI::activeStack(const CStack * stack)
+void CEmptyAI::activeStack(const BattleID & battleID, const CStack * stack)
 {
-	cb->battleMakeUnitAction(BattleAction::makeDefend(stack));
+	cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack));
 }
 
-void CEmptyAI::yourTacticPhase(int distance)
+void CEmptyAI::yourTacticPhase(const BattleID & battleID, int distance)
 {
-	cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide()));
+	cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide()));
 }
 
 void CEmptyAI::heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector<SecondarySkill> &skills, QueryID queryID)
@@ -76,7 +76,7 @@ void CEmptyAI::showMapObjectSelectDialog(QueryID askID, const Component & icon,
 	cb->selectionMade(0, askID);
 }
 
-std::optional<BattleAction> CEmptyAI::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState)
+std::optional<BattleAction> CEmptyAI::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState)
 {
 	return std::nullopt;
 }

+ 3 - 3
AI/EmptyAI/CEmptyAI.h

@@ -24,15 +24,15 @@ public:
 
 	void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
 	void yourTurn(QueryID queryID) override;
-	void yourTacticPhase(int distance) override;
-	void activeStack(const CStack * stack) override;
+	void yourTacticPhase(const BattleID & battleID, int distance) override;
+	void activeStack(const BattleID & battleID, const CStack * stack) override;
 	void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector<SecondarySkill> &skills, QueryID queryID) override;
 	void commanderGotLevel (const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID) override;
 	void showBlockingDialog(const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel) override;
 	void 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;
+	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override;
 };
 
 #define NAME "EmptyAI 0.1"

+ 8 - 7
AI/Nullkiller/AIGateway.cpp

@@ -21,6 +21,7 @@
 #include "../../lib/serializer/BinarySerializer.h"
 #include "../../lib/serializer/BinaryDeserializer.h"
 #include "../../lib/battle/BattleStateInfoForRetreat.h"
+#include "../../lib/battle/BattleInfo.h"
 
 #include "AIGateway.h"
 #include "Goals/Goals.h"
@@ -510,7 +511,7 @@ void AIGateway::showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositio
 	NET_EVENT_HANDLER;
 }
 
-std::optional<BattleAction> AIGateway::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState)
+std::optional<BattleAction> AIGateway::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState)
 {
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;
@@ -535,7 +536,7 @@ void AIGateway::initGameInterface(std::shared_ptr<Environment> env, std::shared_
 	cbc = CB;
 
 	NET_EVENT_HANDLER;
-	playerID = *myCb->getMyColor();
+	playerID = *myCb->getPlayerID();
 	myCb->waitTillRealize = true;
 	myCb->unlockGsWhenWaiting = true;
 
@@ -1080,22 +1081,22 @@ 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, bool replayAllowed)
+void AIGateway::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed)
 {
 	NET_EVENT_HANDLER;
 	assert(!playerID.isValidPlayer() || 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, replayAllowed);
+	CAdventureAI::battleStart(battleID, army1, army2, tile, hero1, hero2, side, replayAllowed);
 }
 
-void AIGateway::battleEnd(const BattleResult * br, QueryID queryID)
+void AIGateway::battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID)
 {
 	NET_EVENT_HANDLER;
 	assert(status.getBattle() == ONGOING_BATTLE);
 	status.setBattle(ENDING_BATTLE);
-	bool won = br->winner == myCb->battleGetMySide();
+	bool won = br->winner == myCb->getBattle(battleID)->battleGetMySide();
 	logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.toString(), (won ? "won" : "lost"), battlename);
 	battlename.clear();
 
@@ -1108,7 +1109,7 @@ void AIGateway::battleEnd(const BattleResult * br, QueryID queryID)
 			answerQuery(queryID, confirmAction);
 		});
 	}
-	CAdventureAI::battleEnd(br, queryID);
+	CAdventureAI::battleEnd(battleID, br, queryID);
 }
 
 void AIGateway::waitTillFree()

+ 3 - 3
AI/Nullkiller/AIGateway.h

@@ -167,10 +167,10 @@ public:
 	void heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) override;
 	void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) override;
 	void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
-	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override;
+	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) 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 battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override;
+	void battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) override;
 
 	void makeTurn();
 

+ 37 - 34
AI/StupidAI/StupidAI.cpp

@@ -14,6 +14,7 @@
 #include "../../CCallback.h"
 #include "../../lib/CCreatureHandler.h"
 #include "../../lib/battle/BattleAction.h"
+#include "../../lib/battle/BattleInfo.h"
 
 static std::shared_ptr<CBattleCallback> cbc;
 
@@ -53,12 +54,12 @@ void CStupidAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::share
 	initBattleInterface(ENV, CB);
 }
 
-void CStupidAI::actionFinished(const BattleAction &action)
+void CStupidAI::actionFinished(const BattleID & battleID, const BattleAction &action)
 {
 	print("actionFinished called");
 }
 
-void CStupidAI::actionStarted(const BattleAction &action)
+void CStupidAI::actionStarted(const BattleID & battleID, const BattleAction &action)
 {
 	print("actionStarted called");
 }
@@ -71,11 +72,11 @@ public:
 	std::vector<BattleHex> attackFrom; //for melee fight
 	EnemyInfo(const CStack * _s) : s(_s), adi(0), adr(0)
 	{}
-	void calcDmg(const CStack * ourStack)
+	void calcDmg(const BattleID & battleID, const CStack * ourStack)
 	{
 		// FIXME: provide distance info for Jousting bonus
 		DamageEstimation retal;
-		DamageEstimation dmg = cbc->battleEstimateDamage(ourStack, s, 0, &retal);
+		DamageEstimation dmg = cbc->getBattle(battleID)->battleEstimateDamage(ourStack, s, 0, &retal);
 		adi = static_cast<int>((dmg.damage.min + dmg.damage.max) / 2);
 		adr = static_cast<int>((retal.damage.min + retal.damage.max) / 2);
 	}
@@ -91,14 +92,14 @@ bool isMoreProfitable(const EnemyInfo &ei1, const EnemyInfo& ei2)
 	return (ei1.adi-ei1.adr) < (ei2.adi - ei2.adr);
 }
 
-static bool willSecondHexBlockMoreEnemyShooters(const BattleHex &h1, const BattleHex &h2)
+static bool willSecondHexBlockMoreEnemyShooters(const BattleID & battleID, const BattleHex &h1, const BattleHex &h2)
 {
 	int shooters[2] = {0}; //count of shooters on hexes
 
 	for(int i = 0; i < 2; i++)
 	{
 		for (auto & neighbour : (i ? h2 : h1).neighbouringTiles())
-			if(const auto * s = cbc->battleGetUnitByPos(neighbour))
+			if(const auto * s = cbc->getBattle(battleID)->battleGetUnitByPos(neighbour))
 				if(s->isShooter())
 					shooters[i]++;
 	}
@@ -106,16 +107,16 @@ static bool willSecondHexBlockMoreEnemyShooters(const BattleHex &h1, const Battl
 	return shooters[0] < shooters[1];
 }
 
-void CStupidAI::yourTacticPhase(int distance)
+void CStupidAI::yourTacticPhase(const BattleID & battleID, int distance)
 {
-	cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide()));
+	cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide()));
 }
 
-void CStupidAI::activeStack( const CStack * stack )
+void CStupidAI::activeStack(const BattleID & battleID, const CStack * stack)
 {
 	//boost::this_thread::sleep_for(boost::chrono::seconds(2));
 	print("activeStack called for " + stack->nodeName());
-	ReachabilityInfo dists = cb->getReachability(stack);
+	ReachabilityInfo dists = cb->getBattle(battleID)->getReachability(stack);
 	std::vector<EnemyInfo> enemiesShootable, enemiesReachable, enemiesUnreachable;
 
 	if(stack->creatureId() == CreatureID::CATAPULT)
@@ -128,24 +129,24 @@ void CStupidAI::activeStack( const CStack * stack )
 		attack.side = side;
 		attack.stackNumber = stack->unitId();
 
-		cb->battleMakeUnitAction(attack);
+		cb->battleMakeUnitAction(battleID, attack);
 		return;
 	}
 	else if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON))
 	{
-		cb->battleMakeUnitAction(BattleAction::makeDefend(stack));
+		cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack));
 		return;
 	}
 
-	for (const CStack *s : cb->battleGetStacks(CBattleCallback::ONLY_ENEMY))
+	for (const CStack *s : cb->getBattle(battleID)->battleGetStacks(CBattleInfoEssentials::ONLY_ENEMY))
 	{
-		if(cb->battleCanShoot(stack, s->getPosition()))
+		if(cb->getBattle(battleID)->battleCanShoot(stack, s->getPosition()))
 		{
 			enemiesShootable.push_back(s);
 		}
 		else
 		{
-			std::vector<BattleHex> avHexes = cb->battleGetAvailableHexes(stack, false);
+			std::vector<BattleHex> avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(stack, false);
 
 			for (BattleHex hex : avHexes)
 			{
@@ -168,21 +169,23 @@ void CStupidAI::activeStack( const CStack * stack )
 	}
 
 	for ( auto & enemy : enemiesReachable )
-		enemy.calcDmg( stack );
+		enemy.calcDmg(battleID, stack);
 
 	for ( auto & enemy : enemiesShootable )
-		enemy.calcDmg( stack );
+		enemy.calcDmg(battleID, stack);
 
 	if(enemiesShootable.size())
 	{
 		const EnemyInfo &ei= *std::max_element(enemiesShootable.begin(), enemiesShootable.end(), isMoreProfitable);
-		cb->battleMakeUnitAction(BattleAction::makeShotAttack(stack, ei.s));
+		cb->battleMakeUnitAction(battleID, BattleAction::makeShotAttack(stack, ei.s));
 		return;
 	}
 	else if(enemiesReachable.size())
 	{
 		const EnemyInfo &ei= *std::max_element(enemiesReachable.begin(), enemiesReachable.end(), &isMoreProfitable);
-		cb->battleMakeUnitAction(BattleAction::makeMeleeAttack(stack, ei.s->getPosition(), *std::max_element(ei.attackFrom.begin(), ei.attackFrom.end(), &willSecondHexBlockMoreEnemyShooters)));
+		BattleHex targetHex = *std::max_element(ei.attackFrom.begin(), ei.attackFrom.end(), [&](auto a, auto b) { return willSecondHexBlockMoreEnemyShooters(battleID, a, b);});
+
+		cb->battleMakeUnitAction(battleID, BattleAction::makeMeleeAttack(stack, ei.s->getPosition(), targetHex));
 		return;
 	}
 	else if(enemiesUnreachable.size()) //due to #955 - a buggy battle may occur when there are no enemies
@@ -194,26 +197,26 @@ void CStupidAI::activeStack( const CStack * stack )
 
 		if(dists.distToNearestNeighbour(stack, closestEnemy->s) < GameConstants::BFIELD_SIZE)
 		{
-			cb->battleMakeUnitAction(goTowards(stack, closestEnemy->s->getAttackableHexes(stack)));
+			cb->battleMakeUnitAction(battleID, goTowards(battleID, stack, closestEnemy->s->getAttackableHexes(stack)));
 			return;
 		}
 	}
 
-	cb->battleMakeUnitAction(BattleAction::makeDefend(stack));
+	cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack));
 	return;
 }
 
-void CStupidAI::battleAttack(const BattleAttack *ba)
+void CStupidAI::battleAttack(const BattleID & battleID, const BattleAttack *ba)
 {
 	print("battleAttack called");
 }
 
-void CStupidAI::battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged)
+void CStupidAI::battleStacksAttacked(const BattleID & battleID, const std::vector<BattleStackAttacked> & bsa, bool ranged)
 {
 	print("battleStacksAttacked called");
 }
 
-void CStupidAI::battleEnd(const BattleResult *br, QueryID queryID)
+void CStupidAI::battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID)
 {
 	print("battleEnd called");
 }
@@ -223,38 +226,38 @@ void CStupidAI::battleEnd(const BattleResult *br, QueryID queryID)
 // 	print("battleResultsApplied called");
 // }
 
-void CStupidAI::battleNewRoundFirst(int round)
+void CStupidAI::battleNewRoundFirst(const BattleID & battleID)
 {
 	print("battleNewRoundFirst called");
 }
 
-void CStupidAI::battleNewRound(int round)
+void CStupidAI::battleNewRound(const BattleID & battleID)
 {
 	print("battleNewRound called");
 }
 
-void CStupidAI::battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport)
+void CStupidAI::battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport)
 {
 	print("battleStackMoved called");
 }
 
-void CStupidAI::battleSpellCast(const BattleSpellCast *sc)
+void CStupidAI::battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc)
 {
 	print("battleSpellCast called");
 }
 
-void CStupidAI::battleStacksEffectsSet(const SetStackEffect & sse)
+void CStupidAI::battleStacksEffectsSet(const BattleID & battleID, 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, bool replayAllowed)
+void CStupidAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
 {
 	print("battleStart called");
 	side = Side;
 }
 
-void CStupidAI::battleCatapultAttacked(const CatapultAttack & ca)
+void CStupidAI::battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca)
 {
 	print("battleCatapultAttacked called");
 }
@@ -264,10 +267,10 @@ void CStupidAI::print(const std::string &text) const
 	logAi->trace("CStupidAI  [%p]: %s", this, text);
 }
 
-BattleAction CStupidAI::goTowards(const CStack * stack, std::vector<BattleHex> hexes) const
+BattleAction CStupidAI::goTowards(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> hexes) const
 {
-	auto reachability = cb->getReachability(stack);
-	auto avHexes = cb->battleGetAvailableHexes(reachability, stack, false);
+	auto reachability = cb->getBattle(battleID)->getReachability(stack);
+	auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false);
 
 	if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked
 	{

+ 18 - 16
AI/StupidAI/StupidAI.h

@@ -11,6 +11,7 @@
 
 #include "../../lib/battle/BattleHex.h"
 #include "../../lib/battle/ReachabilityInfo.h"
+#include "../../lib/CGameInterface.h"
 
 class EnemyInfo;
 
@@ -30,25 +31,26 @@ public:
 
 	void initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB) override;
 	void initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB, AutocombatPreferences autocombatPreferences) 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 activeStack(const CStack * stack) override; //called when it's turn of that stack
-	void yourTacticPhase(int distance) override;
-
-	void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack
-	void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) override; //called when stack receives damage (after battleAttack())
-	void battleEnd(const BattleResult *br, QueryID queryID) override;
+
+	void actionFinished(const BattleID & battleID, const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero
+	void actionStarted(const BattleID & battleID, const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero
+	void activeStack(const BattleID & battleID, const CStack * stack) override; //called when it's turn of that stack
+	void yourTacticPhase(const BattleID & battleID, int distance) override;
+
+	void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; //called when stack is performing attack
+	void battleStacksAttacked(const BattleID & battleID, const std::vector<BattleStackAttacked> & bsa, bool ranged) override; //called when stack receives damage (after battleAttack())
+	void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override;
 	//void battleResultsApplied() override; //called when all effects of last battle are applied
-	void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied;
-	void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn
-	void battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport) override;
-	void battleSpellCast(const BattleSpellCast *sc) override;
-	void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks
+	void battleNewRoundFirst(const BattleID & battleID) override; //called at the beginning of each turn before changes are applied;
+	void battleNewRound(const BattleID & battleID) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn
+	void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport) override;
+	void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override;
+	void battleStacksEffectsSet(const BattleID & battleID, 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, 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
+	void battleStart(const BattleID & battleID, 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 BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack
 
 private:
-	BattleAction goTowards(const CStack * stack, std::vector<BattleHex> hexes) const;
+	BattleAction goTowards(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> hexes) const;
 };
 

+ 7 - 7
AI/VCAI/VCAI.cpp

@@ -600,7 +600,7 @@ void VCAI::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<C
 	ah->init(CB.get());
 
 	NET_EVENT_HANDLER; //sets ah->rm->cb
-	playerID = *myCb->getMyColor();
+	playerID = *myCb->getPlayerID();
 	myCb->waitTillRealize = true;
 	myCb->unlockGsWhenWaiting = true;
 
@@ -1577,22 +1577,22 @@ 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, bool replayAllowed)
+void VCAI::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed)
 {
 	NET_EVENT_HANDLER;
 	assert(!playerID.isValidPlayer() || 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, replayAllowed);
+	CAdventureAI::battleStart(battleID, army1, army2, tile, hero1, hero2, side, replayAllowed);
 }
 
-void VCAI::battleEnd(const BattleResult * br, QueryID queryID)
+void VCAI::battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID)
 {
 	NET_EVENT_HANDLER;
 	assert(status.getBattle() == ONGOING_BATTLE);
 	status.setBattle(ENDING_BATTLE);
-	bool won = br->winner == myCb->battleGetMySide();
+	bool won = br->winner == myCb->getBattle(battleID)->battleGetMySide();
 	logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.toString(), (won ? "won" : "lost"), battlename);
 	battlename.clear();
 
@@ -1605,7 +1605,7 @@ void VCAI::battleEnd(const BattleResult * br, QueryID queryID)
 			answerQuery(queryID, confirmAction);
 		});
 	}
-	CAdventureAI::battleEnd(br, queryID);
+	CAdventureAI::battleEnd(battleID, br, queryID);
 }
 
 void VCAI::waitTillFree()
@@ -2894,7 +2894,7 @@ bool shouldVisit(HeroPtr h, const CGObjectInstance * obj)
 	return true;
 }
 
-std::optional<BattleAction> VCAI::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState)
+std::optional<BattleAction> VCAI::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState)
 {
 	return std::nullopt;
 }

+ 3 - 3
AI/VCAI/VCAI.h

@@ -201,9 +201,9 @@ 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, bool replayAllowed) override;
-	void battleEnd(const BattleResult * br, QueryID queryID) override;
-	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override;
+	void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override;
+	void battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) override;
+	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override;
 
 	void makeTurn();
 	void mainLoop();

+ 44 - 27
CCallback.cpp

@@ -203,16 +203,17 @@ bool CCallback::buildBuilding(const CGTownInstance *town, BuildingID buildingID)
 	return true;
 }
 
-void CBattleCallback::battleMakeSpellAction(const BattleAction & action)
+void CBattleCallback::battleMakeSpellAction(const BattleID & battleID, const BattleAction & action)
 {
 	assert(action.actionType == EActionType::HERO_SPELL);
 	MakeAction mca(action);
+	mca.battleID = battleID;
 	sendRequest(&mca);
 }
 
 int CBattleCallback::sendRequest(const CPackForServer * request)
 {
-	int requestID = cl->sendRequest(request, *player);
+	int requestID = cl->sendRequest(request, *getPlayerID());
 	if(waitTillRealize)
 	{
 		logGlobal->trace("We'll wait till request %d is answered.\n", requestID);
@@ -226,8 +227,7 @@ int CBattleCallback::sendRequest(const CPackForServer * request)
 
 void CCallback::swapGarrisonHero( const CGTownInstance *town )
 {
-	if(town->tempOwner == *player
-	   || (town->garrisonHero && town->garrisonHero->tempOwner == *player ))
+	if(town->tempOwner == *player || (town->garrisonHero && town->garrisonHero->tempOwner == *player ))
 	{
 		GarrisonHeroSwap pack(town->id);
 		sendRequest(&pack);
@@ -236,7 +236,7 @@ void CCallback::swapGarrisonHero( const CGTownInstance *town )
 
 void CCallback::buyArtifact(const CGHeroInstance *hero, ArtifactID aid)
 {
-	if(hero->tempOwner != player) return;
+	if(hero->tempOwner != *player) return;
 
 	BuyArtifact pack(hero->id,aid);
 	sendRequest(&pack);
@@ -297,8 +297,8 @@ void CCallback::buildBoat( const IShipyard *obj )
 	sendRequest(&bb);
 }
 
-CCallback::CCallback(CGameState * GS, std::optional<PlayerColor> Player, CClient * C):
-	CBattleCallback(Player, C)
+CCallback::CCallback(CGameState * GS, std::optional<PlayerColor> Player, CClient * C)
+	: CBattleCallback(Player, C)
 {
 	gs = GS;
 
@@ -306,10 +306,7 @@ CCallback::CCallback(CGameState * GS, std::optional<PlayerColor> Player, CClient
 	unlockGsWhenWaiting = false;
 }
 
-CCallback::~CCallback()
-{
-//trivial, but required. Don`t remove.
-}
+CCallback::~CCallback() = default;
 
 bool CCallback::canMoveBetween(const int3 &a, const int3 &b)
 {
@@ -322,6 +319,11 @@ std::shared_ptr<const CPathsInfo> CCallback::getPathsInfo(const CGHeroInstance *
 	return cl->getPathsInfo(h);
 }
 
+std::optional<PlayerColor> CCallback::getPlayerID() const
+{
+	return CBattleCallback::getPlayerID();
+}
+
 int3 CCallback::getGuardingCreaturePosition(int3 tile)
 {
 	if (!gs->map->isInTheMap(tile))
@@ -364,36 +366,51 @@ void CCallback::unregisterBattleInterface(std::shared_ptr<IBattleEventsReceiver>
 	cl->additionalBattleInts[*player] -= battleEvents;
 }
 
-#if SCRIPTING_ENABLED
-scripting::Pool * CBattleCallback::getContextPool() const
+CBattleCallback::CBattleCallback(std::optional<PlayerColor> player, CClient * C):
+	cl(C),
+	player(player)
 {
-	return cl->getGlobalContextPool();
 }
-#endif
 
-CBattleCallback::CBattleCallback(std::optional<PlayerColor> Player, CClient * C)
+void CBattleCallback::battleMakeUnitAction(const BattleID & battleID, const BattleAction & action)
 {
-	player = Player;
-	cl = C;
-}
-
-void CBattleCallback::battleMakeUnitAction(const BattleAction & action)
-{
-	assert(!cl->gs->curB->tacticDistance);
+	assert(!cl->gs->getBattle(battleID)->tacticDistance);
 	MakeAction ma;
 	ma.ba = action;
+	ma.battleID = battleID;
 	sendRequest(&ma);
 }
 
-void CBattleCallback::battleMakeTacticAction( const BattleAction & action )
+void CBattleCallback::battleMakeTacticAction(const BattleID & battleID, const BattleAction & action )
 {
-	assert(cl->gs->curB->tacticDistance);
+	assert(cl->gs->getBattle(battleID)->tacticDistance);
 	MakeAction ma;
 	ma.ba = action;
+	ma.battleID = battleID;
 	sendRequest(&ma);
 }
 
-std::optional<BattleAction> CBattleCallback::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState)
+std::optional<BattleAction> CBattleCallback::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState)
+{
+	return cl->playerint[getPlayerID().value()]->makeSurrenderRetreatDecision(battleID, battleState);
+}
+
+std::shared_ptr<CPlayerBattleCallback> CBattleCallback::getBattle(const BattleID & battleID)
+{
+	return activeBattles.at(battleID);
+}
+
+std::optional<PlayerColor> CBattleCallback::getPlayerID() const
+{
+	return player;
+}
+
+void CBattleCallback::onBattleStarted(const IBattleInfo * info)
+{
+	activeBattles[info->getBattleID()] = std::make_shared<CPlayerBattleCallback>(info, *getPlayerID());
+}
+
+void CBattleCallback::onBattleEnded(const BattleID & battleID)
 {
-	return cl->playerint[getPlayerID().value()]->makeSurrenderRetreatDecision(battleState);
+	activeBattles.erase(battleID);
 }

+ 26 - 17
CCallback.h

@@ -53,10 +53,13 @@ public:
 	bool waitTillRealize = false; //if true, request functions will return after they are realized by server
 	bool unlockGsWhenWaiting = false;//if true after sending each request, gs mutex will be unlocked so the changes can be applied; NOTICE caller must have gs mx locked prior to any call to actiob callback!
 	//battle
-	virtual void battleMakeSpellAction(const BattleAction & action) = 0;
-	virtual void battleMakeUnitAction(const BattleAction & action) = 0;
-	virtual void battleMakeTacticAction(const BattleAction & action) = 0;
-	virtual std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) = 0;
+	virtual void battleMakeSpellAction(const BattleID & battleID, const BattleAction & action) = 0;
+	virtual void battleMakeUnitAction(const BattleID & battleID, const BattleAction & action) = 0;
+	virtual void battleMakeTacticAction(const BattleID & battleID, const BattleAction & action) = 0;
+	virtual std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) = 0;
+
+	virtual std::shared_ptr<CPlayerBattleCallback> getBattle(const BattleID & battleID) = 0;
+	virtual std::optional<PlayerColor> getPlayerID() const = 0;
 };
 
 class IGameActionCallback
@@ -108,30 +111,34 @@ public:
 	virtual void bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap) = 0;
 };
 
-class CBattleCallback : public IBattleCallback, public CPlayerBattleCallback
+class CBattleCallback : public IBattleCallback
 {
+	std::map<BattleID, std::shared_ptr<CPlayerBattleCallback>> activeBattles;
+
+	std::optional<PlayerColor> player;
+
 protected:
 	int sendRequest(const CPackForServer * request); //returns requestID (that'll be matched to requestID in PackageApplied)
 	CClient *cl;
 
 public:
-	CBattleCallback(std::optional<PlayerColor> Player, CClient * C);
-	void battleMakeSpellAction(const BattleAction & action) override;//for casting spells by hero - DO NOT use it for moving active stack
-	void battleMakeUnitAction(const BattleAction & action) override;
-	void battleMakeTacticAction(const BattleAction & action) override; // performs tactic phase actions
-	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override;
-
-#if SCRIPTING_ENABLED
-	scripting::Pool * getContextPool() const override;
-#endif
+	CBattleCallback(std::optional<PlayerColor> player, CClient * C);
+	void battleMakeSpellAction(const BattleID & battleID, const BattleAction & action) override;//for casting spells by hero - DO NOT use it for moving active stack
+	void battleMakeUnitAction(const BattleID & battleID, const BattleAction & action) override;
+	void battleMakeTacticAction(const BattleID & battleID, const BattleAction & action) override; // performs tactic phase actions
+	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override;
+
+	std::shared_ptr<CPlayerBattleCallback> getBattle(const BattleID & battleID) override;
+	std::optional<PlayerColor> getPlayerID() const override;
+
+	void onBattleStarted(const IBattleInfo * info);
+	void onBattleEnded(const BattleID & battleID);
 
 	friend class CCallback;
 	friend class CClient;
 };
 
-class CCallback : public CPlayerSpecificInfoCallback,
-	public IGameActionCallback,
-	public CBattleCallback
+class CCallback : public CPlayerSpecificInfoCallback, public CBattleCallback, public IGameActionCallback
 {
 public:
 	CCallback(CGameState * GS, std::optional<PlayerColor> Player, CClient * C);
@@ -142,6 +149,8 @@ public:
 	virtual int3 getGuardingCreaturePosition(int3 tile);
 	virtual std::shared_ptr<const CPathsInfo> getPathsInfo(const CGHeroInstance * h);
 
+	std::optional<PlayerColor> getPlayerID() const override;
+
 	//Set of metrhods that allows adding more interfaces for this player that'll receive game event call-ins.
 	void registerBattleInterface(std::shared_ptr<IBattleEventsReceiver> battleEvents);
 	void unregisterBattleInterface(std::shared_ptr<IBattleEventsReceiver> battleEvents);

+ 37 - 37
client/CPlayerInterface.cpp

@@ -658,7 +658,7 @@ void CPlayerInterface::buildChanged(const CGTownInstance *town, BuildingID build
 	}
 }
 
-void CPlayerInterface::battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2)
+void CPlayerInterface::battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2)
 {
 	// when battle starts, game will send battleStart pack *before* movement confirmation
 	// and since network thread wait for battle intro to play, movement confirmation will only happen after intro
@@ -670,7 +670,7 @@ 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, bool replayAllowed)
+void CPlayerInterface::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 
@@ -685,7 +685,7 @@ void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet
 		autocombatPreferences.enableSpellsUsage = settings["battle"]["enableAutocombatSpells"].Bool();
 
 		autofightingAI->initBattleInterface(env, cb, autocombatPreferences);
-		autofightingAI->battleStart(army1, army2, tile, hero1, hero2, side, false);
+		autofightingAI->battleStart(battleID, army1, army2, tile, hero1, hero2, side, false);
 		isAutoFightOn = true;
 		cb->registerBattleInterface(autofightingAI);
 	}
@@ -697,7 +697,7 @@ void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet
 	BATTLE_EVENT_POSSIBLE_RETURN;
 }
 
-void CPlayerInterface::battleUnitsChanged(const std::vector<UnitChanges> & units)
+void CPlayerInterface::battleUnitsChanged(const BattleID & battleID, const std::vector<UnitChanges> & units)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
@@ -708,7 +708,7 @@ void CPlayerInterface::battleUnitsChanged(const std::vector<UnitChanges> & units
 		{
 		case UnitChanges::EOperation::RESET_STATE:
 			{
-				const CStack * stack = cb->battleGetStackByID(info.id );
+				const CStack * stack = cb->getBattle(battleID)->battleGetStackByID(info.id );
 
 				if(!stack)
 				{
@@ -723,7 +723,7 @@ void CPlayerInterface::battleUnitsChanged(const std::vector<UnitChanges> & units
 			break;
 		case UnitChanges::EOperation::ADD:
 			{
-				const CStack * unit = cb->battleGetStackByID(info.id);
+				const CStack * unit = cb->getBattle(battleID)->battleGetStackByID(info.id);
 				if(!unit)
 				{
 					logGlobal->error("Invalid unit ID %d", info.id);
@@ -739,7 +739,7 @@ void CPlayerInterface::battleUnitsChanged(const std::vector<UnitChanges> & units
 	}
 }
 
-void CPlayerInterface::battleObstaclesChanged(const std::vector<ObstacleChanges> & obstacles)
+void CPlayerInterface::battleObstaclesChanged(const BattleID & battleID, const std::vector<ObstacleChanges> & obstacles)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
@@ -751,7 +751,7 @@ void CPlayerInterface::battleObstaclesChanged(const std::vector<ObstacleChanges>
 	{
 		if(change.operation == BattleChanges::EOperation::ADD)
 		{
-			auto instance = cb->battleGetObstacleByID(change.id);
+			auto instance = cb->getBattle(battleID)->battleGetObstacleByID(change.id);
 			if(instance)
 				newObstacles.push_back(instance);
 			else
@@ -770,7 +770,7 @@ void CPlayerInterface::battleObstaclesChanged(const std::vector<ObstacleChanges>
 	battleInt->fieldController->redrawBackgroundWithHexes();
 }
 
-void CPlayerInterface::battleCatapultAttacked(const CatapultAttack & ca)
+void CPlayerInterface::battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
@@ -778,15 +778,15 @@ void CPlayerInterface::battleCatapultAttacked(const CatapultAttack & ca)
 	battleInt->stackIsCatapulting(ca);
 }
 
-void CPlayerInterface::battleNewRound(int round) //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn
+void CPlayerInterface::battleNewRound(const BattleID & battleID) //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
 
-	battleInt->newRound(round);
+	battleInt->newRound();
 }
 
-void CPlayerInterface::actionStarted(const BattleAction &action)
+void CPlayerInterface::actionStarted(const BattleID & battleID, const BattleAction &action)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
@@ -794,7 +794,7 @@ void CPlayerInterface::actionStarted(const BattleAction &action)
 	battleInt->startAction(action);
 }
 
-void CPlayerInterface::actionFinished(const BattleAction &action)
+void CPlayerInterface::actionFinished(const BattleID & battleID, const BattleAction &action)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
@@ -802,17 +802,17 @@ void CPlayerInterface::actionFinished(const BattleAction &action)
 	battleInt->endAction(action);
 }
 
-void CPlayerInterface::activeStack(const CStack * stack) //called when it's turn of that stack
+void CPlayerInterface::activeStack(const BattleID & battleID, const CStack * stack) //called when it's turn of that stack
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	logGlobal->trace("Awaiting command for %s", stack->nodeName());
 
-	assert(!cb->battleIsFinished());
-	if (cb->battleIsFinished())
+	assert(!cb->getBattle(battleID)->battleIsFinished());
+	if (cb->getBattle(battleID)->battleIsFinished())
 	{
 		logGlobal->error("Received CPlayerInterface::activeStack after battle is finished!");
 
-		cb->battleMakeUnitAction(BattleAction::makeDefend(stack));
+		cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack));
 		return ;
 	}
 
@@ -823,7 +823,7 @@ void CPlayerInterface::activeStack(const CStack * stack) //called when it's turn
 			//FIXME: we want client rendering to proceed while AI is making actions
 			// so unlock mutex while AI is busy since this might take quite a while, especially if hero has many spells
 			auto unlockPim = vstd::makeUnlockGuard(*pim);
-			autofightingAI->activeStack(stack);
+			autofightingAI->activeStack(battleID, stack);
 			return;
 		}
 
@@ -835,7 +835,7 @@ void CPlayerInterface::activeStack(const CStack * stack) //called when it's turn
 	if(!battleInt)
 	{
 		// probably battle is finished already
-		cb->battleMakeUnitAction(BattleAction::makeDefend(stack));
+		cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack));
 	}
 
 	{
@@ -845,7 +845,7 @@ void CPlayerInterface::activeStack(const CStack * stack) //called when it's turn
 	}
 }
 
-void CPlayerInterface::battleEnd(const BattleResult *br, QueryID queryID)
+void CPlayerInterface::battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	if(isAutoFightOn || autofightingAI)
@@ -880,7 +880,7 @@ void CPlayerInterface::battleEnd(const BattleResult *br, QueryID queryID)
 	battleInt->battleFinished(*br, queryID);
 }
 
-void CPlayerInterface::battleLogMessage(const std::vector<MetaString> & lines)
+void CPlayerInterface::battleLogMessage(const BattleID & battleID, const std::vector<MetaString> & lines)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
@@ -888,28 +888,28 @@ void CPlayerInterface::battleLogMessage(const std::vector<MetaString> & lines)
 	battleInt->displayBattleLog(lines);
 }
 
-void CPlayerInterface::battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport)
+void CPlayerInterface::battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
 
 	battleInt->stackMoved(stack, dest, distance, teleport);
 }
-void CPlayerInterface::battleSpellCast( const BattleSpellCast *sc )
+void CPlayerInterface::battleSpellCast(const BattleID & battleID, const BattleSpellCast * sc)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
 
 	battleInt->spellCast(sc);
 }
-void CPlayerInterface::battleStacksEffectsSet( const SetStackEffect & sse )
+void CPlayerInterface::battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
 
 	battleInt->battleStacksEffectsSet(sse);
 }
-void CPlayerInterface::battleTriggerEffect (const BattleTriggerEffect & bte)
+void CPlayerInterface::battleTriggerEffect(const BattleID & battleID, const BattleTriggerEffect & bte)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
@@ -923,7 +923,7 @@ void CPlayerInterface::battleTriggerEffect (const BattleTriggerEffect & bte)
 		battleInt->windowObject->heroManaPointsChanged(manaDrainedHero);
 	}
 }
-void CPlayerInterface::battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged)
+void CPlayerInterface::battleStacksAttacked(const BattleID & battleID, const std::vector<BattleStackAttacked> & bsa, bool ranged)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
@@ -931,8 +931,8 @@ void CPlayerInterface::battleStacksAttacked(const std::vector<BattleStackAttacke
 	std::vector<StackAttackedInfo> arg;
 	for(auto & elem : bsa)
 	{
-		const CStack * defender = cb->battleGetStackByID(elem.stackAttacked, false);
-		const CStack * attacker = cb->battleGetStackByID(elem.attackerID, false);
+		const CStack * defender = cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked, false);
+		const CStack * attacker = cb->getBattle(battleID)->battleGetStackByID(elem.attackerID, false);
 
 		assert(defender);
 
@@ -955,13 +955,13 @@ void CPlayerInterface::battleStacksAttacked(const std::vector<BattleStackAttacke
 	}
 	battleInt->stacksAreAttacked(arg);
 }
-void CPlayerInterface::battleAttack(const BattleAttack * ba)
+void CPlayerInterface::battleAttack(const BattleID & battleID, const BattleAttack * ba)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
 
 	StackAttackInfo info;
-	info.attacker = cb->battleGetStackByID(ba->stackAttacking);
+	info.attacker = cb->getBattle(battleID)->battleGetStackByID(ba->stackAttacking);
 	info.defender = nullptr;
 	info.indirectAttack = ba->shot();
 	info.lucky = ba->lucky();
@@ -979,11 +979,11 @@ void CPlayerInterface::battleAttack(const BattleAttack * ba)
 		if(!elem.isSecondary())
 		{
 			assert(info.defender == nullptr);
-			info.defender = cb->battleGetStackByID(elem.stackAttacked);
+			info.defender = cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked);
 		}
 		else
 		{
-			info.secondaryDefender.push_back(cb->battleGetStackByID(elem.stackAttacked));
+			info.secondaryDefender.push_back(cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked));
 		}
 	}
 	assert(info.defender != nullptr);
@@ -992,7 +992,7 @@ void CPlayerInterface::battleAttack(const BattleAttack * ba)
 	battleInt->stackAttacking(info);
 }
 
-void CPlayerInterface::battleGateStateChanged(const EGateState state)
+void CPlayerInterface::battleGateStateChanged(const BattleID & battleID, const EGateState state)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
@@ -1000,7 +1000,7 @@ void CPlayerInterface::battleGateStateChanged(const EGateState state)
 	battleInt->gateStateChanged(state);
 }
 
-void CPlayerInterface::yourTacticPhase(int distance)
+void CPlayerInterface::yourTacticPhase(const BattleID & battleID, int distance)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 }
@@ -1705,12 +1705,12 @@ void CPlayerInterface::tryDigging(const CGHeroInstance * h)
 		showInfoDialog(CGI->generaltexth->allTexts[msgToShow]);
 }
 
-void CPlayerInterface::battleNewRoundFirst( int round )
+void CPlayerInterface::battleNewRoundFirst(const BattleID & battleID)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
 
-	battleInt->newRoundFirst(round);
+	battleInt->newRoundFirst();
 }
 
 void CPlayerInterface::stopMovement()
@@ -2126,7 +2126,7 @@ void CPlayerInterface::showWorldViewEx(const std::vector<ObjectPosInfo>& objectP
 	adventureInt->openWorldView(objectPositions, showTerrain );
 }
 
-std::optional<BattleAction> CPlayerInterface::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState)
+std::optional<BattleAction> CPlayerInterface::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState)
 {
 	return std::nullopt;
 }

+ 21 - 21
client/CPlayerInterface.h

@@ -152,27 +152,27 @@ protected: // Call-ins from server, should not be called directly, but only via
 	void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
 
 	//for battles
-	void actionFinished(const BattleAction& action) override;//occurs AFTER action taken by active stack or by the hero
-	void actionStarted(const BattleAction& action) override;//occurs BEFORE action taken by active stack or by the hero
-	void activeStack(const CStack * stack) override; //called when it's turn of that stack
-	void battleAttack(const BattleAttack *ba) override; //stack performs attack
-	void battleEnd(const BattleResult *br, QueryID queryID) override; //end of battle
-	void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied; used for HP regen handling
-	void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn
-	void battleLogMessage(const std::vector<MetaString> & lines) override;
-	void battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport) override;
-	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; //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, 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
-	void battleGateStateChanged(const EGateState state) override;
-	void yourTacticPhase(int distance) override;
-	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override;
+	void actionFinished(const BattleID & battleID, const BattleAction& action) override;//occurs AFTER action taken by active stack or by the hero
+	void actionStarted(const BattleID & battleID, const BattleAction& action) override;//occurs BEFORE action taken by active stack or by the hero
+	void activeStack(const BattleID & battleID, const CStack * stack) override; //called when it's turn of that stack
+	void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; //stack performs attack
+	void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override; //end of battle
+	void battleNewRoundFirst(const BattleID & battleID) override; //called at the beginning of each turn before changes are applied; used for HP regen handling
+	void battleNewRound(const BattleID & battleID) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn
+	void battleLogMessage(const BattleID & battleID, const std::vector<MetaString> & lines) override;
+	void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport) override;
+	void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override;
+	void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override; //called when a specific effect is set to stacks
+	void battleTriggerEffect(const BattleID & battleID, const BattleTriggerEffect & bte) override; //various one-shot effect
+	void battleStacksAttacked(const BattleID & battleID, const std::vector<BattleStackAttacked> & bsa, bool ranged) override;
+	void battleStartBefore(const BattleID & battleID, 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 BattleID & battleID, 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 BattleID & battleID, const std::vector<UnitChanges> & units) override;
+	void battleObstaclesChanged(const BattleID & battleID, const std::vector<ObstacleChanges> & obstacles) override;
+	void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack
+	void battleGateStateChanged(const BattleID & battleID, const EGateState state) override;
+	void yourTacticPhase(const BattleID & battleID, int distance) override;
+	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override;
 
 public: // public interface for use by client via LOCPLINT access
 

+ 20 - 30
client/Client.cpp

@@ -122,9 +122,9 @@ events::EventBus * CPlayerEnvironment::eventBus() const
 	return cl->eventBus();//always get actual value
 }
 
-const CPlayerEnvironment::BattleCb * CPlayerEnvironment::battle() const
+const CPlayerEnvironment::BattleCb * CPlayerEnvironment::battle(const BattleID & battleID) const
 {
-	return mainCallback.get();
+	return mainCallback->getBattle(battleID).get();
 }
 
 const CPlayerEnvironment::GameCb * CPlayerEnvironment::game() const
@@ -153,9 +153,9 @@ const Services * CClient::services() const
 	return VLC; //todo: this should be CGI
 }
 
-const CClient::BattleCb * CClient::battle() const
+const CClient::BattleCb * CClient::battle(const BattleID & battleID) const
 {
-	return this;
+	return nullptr; //todo?
 }
 
 const CClient::GameCb * CClient::game() const
@@ -345,7 +345,7 @@ void CClient::serialize(BinaryDeserializer & h, const int version)
 
 void CClient::save(const std::string & fname)
 {
-	if(gs->curB)
+	if(!gs->currentBattles.empty())
 	{
 		logNetwork->error("Game cannot be saved during battle!");
 		return;
@@ -565,14 +565,12 @@ int CClient::sendRequest(const CPackForServer * request, PlayerColor player)
 
 void CClient::battleStarted(const BattleInfo * info)
 {
-	setBattle(info);
-
 	for(auto & battleCb : battleCallbacks)
 	{
 		if(vstd::contains_if(info->sides, [&](const SideInBattle& side) {return side.color == battleCb.first; })
 			|| !battleCb.first.isValidPlayer())
 		{
-			battleCb.second->setBattle(info);
+			battleCb.second->onBattleStarted(info);
 		}
 	}
 
@@ -583,7 +581,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, info->replayAllowed);
+			battleints[color]->battleStart(info->battleID, leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side, info->replayAllowed);
 	};
 	
 	callBattleStart(leftSide.color, 0);
@@ -601,11 +599,11 @@ void CClient::battleStarted(const BattleInfo * info)
 	//Remove player interfaces for auto battle (quickCombat option)
 	if(att && att->isAutoFightOn)
 	{
-		if (att->cb->battleGetTacticDist())
+		if (att->cb->getBattle(info->battleID)->battleGetTacticDist())
 		{
-			auto side = att->cb->playerToSide(att->playerID);
+			auto side = att->cb->getBattle(info->battleID)->playerToSide(att->playerID);
 			auto action = BattleAction::makeEndOFTacticPhase(*side);
-			att->cb->battleMakeTacticAction(action);
+			att->cb->battleMakeTacticAction(info->battleID, action);
 		}
 
 		att.reset();
@@ -617,15 +615,15 @@ void CClient::battleStarted(const BattleInfo * info)
 		if(att || def)
 		{
 			boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
-			CPlayerInterface::battleInt = std::make_shared<BattleInterface>(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def);
+			CPlayerInterface::battleInt = std::make_shared<BattleInterface>(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def);
 		}
 		else if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool())
 		{
 			//TODO: This certainly need improvement
 			auto spectratorInt = std::dynamic_pointer_cast<CPlayerInterface>(playerint[PlayerColor::SPECTATOR]);
-			spectratorInt->cb->setBattle(info);
+			spectratorInt->cb->onBattleStarted(info);
 			boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
-			CPlayerInterface::battleInt = std::make_shared<BattleInterface>(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def, spectratorInt);
+			CPlayerInterface::battleInt = std::make_shared<BattleInterface>(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def, spectratorInt);
 		}
 	}
 
@@ -634,24 +632,21 @@ void CClient::battleStarted(const BattleInfo * info)
 		auto tacticianColor = info->sides[info->tacticsSide].color;
 
 		if (vstd::contains(battleints, tacticianColor))
-			battleints[tacticianColor]->yourTacticPhase(info->tacticDistance);
+			battleints[tacticianColor]->yourTacticPhase(info->battleID, info->tacticDistance);
 	}
 }
 
-void CClient::battleFinished()
+void CClient::battleFinished(const BattleID & battleID)
 {
-	for(auto & side : gs->curB->sides)
+	for(auto & side : gs->getBattle(battleID)->sides)
 		if(battleCallbacks.count(side.color))
-			battleCallbacks[side.color]->setBattle(nullptr);
+			battleCallbacks[side.color]->onBattleEnded(battleID);
 
 	if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool())
-		battleCallbacks[PlayerColor::SPECTATOR]->setBattle(nullptr);
-
-	setBattle(nullptr);
-	gs->curB.dellNull();
+		battleCallbacks[PlayerColor::SPECTATOR]->onBattleEnded(battleID);
 }
 
-void CClient::startPlayerBattleAction(PlayerColor color)
+void CClient::startPlayerBattleAction(const BattleID & battleID, PlayerColor color)
 {
 	assert(vstd::contains(battleints, color));
 
@@ -661,7 +656,7 @@ void CClient::startPlayerBattleAction(PlayerColor color)
 		auto unlock = vstd::makeUnlockGuardIf(*CPlayerInterface::pim, !battleints[color]->human);
 
 		assert(vstd::contains(battleints, color));
-		battleints[color]->activeStack(gs->curB->battleGetStackByID(gs->curB->activeStack, false));
+		battleints[color]->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false));
 	}
 }
 
@@ -698,11 +693,6 @@ scripting::Pool * CClient::getGlobalContextPool() const
 {
 	return clientScripts.get();
 }
-
-scripting::Pool * CClient::getContextPool() const
-{
-	return clientScripts.get();
-}
 #endif
 
 void CClient::reinitScripting()

+ 6 - 7
client/Client.h

@@ -13,7 +13,6 @@
 #include <vcmi/Environment.h>
 
 #include "../lib/IGameCallback.h"
-#include "../lib/battle/CBattleInfoCallback.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -25,6 +24,7 @@ class CGameInterface;
 class BinaryDeserializer;
 class BinarySerializer;
 class BattleAction;
+class BattleInfo;
 
 template<typename T> class CApplier;
 
@@ -105,12 +105,12 @@ public:
 	const Services * services() const override;
 	vstd::CLoggerBase * logger() const override;
 	events::EventBus * eventBus() const override;
-	const BattleCb * battle() const override;
+	const BattleCb * battle(const BattleID & battle) const override;
 	const GameCb * game() const override;
 };
 
 /// Class which handles client - server logic
-class CClient : public IGameCallback, public CBattleInfoCallback, public Environment
+class CClient : public IGameCallback, public Environment
 {
 public:
 	std::map<PlayerColor, std::shared_ptr<CGameInterface>> playerint;
@@ -124,7 +124,7 @@ public:
 	~CClient();
 
 	const Services * services() const override;
-	const BattleCb * battle() const override;
+	const BattleCb * battle(const BattleID & battle) const override;
 	const GameCb * game() const override;
 	vstd::CLoggerBase * logger() const override;
 	events::EventBus * eventBus() const override;
@@ -151,8 +151,8 @@ public:
 	int sendRequest(const CPackForServer * request, PlayerColor player); //returns ID given to that request
 
 	void battleStarted(const BattleInfo * info);
-	void battleFinished();
-	void startPlayerBattleAction(PlayerColor color);
+	void battleFinished(const BattleID & battleID);
+	void startPlayerBattleAction(const BattleID & battleID, PlayerColor color);
 
 	void invalidatePaths();
 	std::shared_ptr<const CPathsInfo> getPathsInfo(const CGHeroInstance * h);
@@ -220,7 +220,6 @@ public:
 
 #if SCRIPTING_ENABLED
 	scripting::Pool * getGlobalContextPool() const override;
-	scripting::Pool * getContextPool() const override;
 #endif
 
 private:

+ 32 - 32
client/NetPacksClient.cpp

@@ -95,18 +95,18 @@ void callAllInterfaces(CClient & cl, void (T::*ptr)(Args...), Args2 && ...args)
 
 //calls all normal interfaces and privileged ones, playerints may be updated when iterating over it, so we need a copy
 template<typename T, typename ... Args, typename ... Args2>
-void callBattleInterfaceIfPresentForBothSides(CClient & cl, void (T::*ptr)(Args...), Args2 && ...args)
+void callBattleInterfaceIfPresentForBothSides(CClient & cl, const BattleID & battleID, void (T::*ptr)(Args...), Args2 && ...args)
 {
-	assert(cl.gameState()->curB);
+	assert(cl.gameState()->getBattle(battleID));
 
-	if (!cl.gameState()->curB)
+	if (!cl.gameState()->getBattle(battleID))
 	{
 		logGlobal->error("Attempt to call battle interface without ongoing battle!");
 		return;
 	}
 
-	callOnlyThatBattleInterface(cl, cl.gameState()->curB->sides[0].color, ptr, std::forward<Args2>(args)...);
-	callOnlyThatBattleInterface(cl, cl.gameState()->curB->sides[1].color, ptr, std::forward<Args2>(args)...);
+	callOnlyThatBattleInterface(cl, cl.gameState()->getBattle(battleID)->sides[0].color, ptr, std::forward<Args2>(args)...);
+	callOnlyThatBattleInterface(cl, cl.gameState()->getBattle(battleID)->sides[1].color, ptr, std::forward<Args2>(args)...);
 	if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool() && LOCPLINT->battleInt)
 	{
 		callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, ptr, std::forward<Args2>(args)...);
@@ -714,11 +714,11 @@ void ApplyClientNetPackVisitor::visitMapObjectSelectDialog(MapObjectSelectDialog
 void ApplyFirstClientNetPackVisitor::visitBattleStart(BattleStart & pack)
 {
 	// Cannot use the usual code because curB is not set yet
-	callOnlyThatBattleInterface(cl, pack.info->sides[0].color, &IBattleEventsReceiver::battleStartBefore, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject,
+	callOnlyThatBattleInterface(cl, pack.info->sides[0].color, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject,
 		pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero);
-	callOnlyThatBattleInterface(cl, pack.info->sides[1].color, &IBattleEventsReceiver::battleStartBefore, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject,
+	callOnlyThatBattleInterface(cl, pack.info->sides[1].color, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject,
 		pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero);
-	callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, &IBattleEventsReceiver::battleStartBefore, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject,
+	callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject,
 		pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero);
 }
 
@@ -729,12 +729,12 @@ void ApplyClientNetPackVisitor::visitBattleStart(BattleStart & pack)
 
 void ApplyFirstClientNetPackVisitor::visitBattleNextRound(BattleNextRound & pack)
 {
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleNewRoundFirst, pack.round);
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleNewRoundFirst, pack.battleID);
 }
 
 void ApplyClientNetPackVisitor::visitBattleNextRound(BattleNextRound & pack)
 {
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleNewRound, pack.round);
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleNewRound, pack.battleID);
 }
 
 void ApplyClientNetPackVisitor::visitBattleSetActiveStack(BattleSetActiveStack & pack)
@@ -742,56 +742,56 @@ void ApplyClientNetPackVisitor::visitBattleSetActiveStack(BattleSetActiveStack &
 	if(!pack.askPlayerInterface)
 		return;
 
-	const CStack *activated = gs.curB->battleGetStackByID(pack.stack);
+	const CStack *activated = gs.getBattle(pack.battleID)->battleGetStackByID(pack.stack);
 	PlayerColor playerToCall; //pack.player that will move activated stack
 	if (activated->hasBonusOfType(BonusType::HYPNOTIZED))
 	{
-		playerToCall = (gs.curB->sides[0].color == activated->unitOwner()
-			? gs.curB->sides[1].color
-			: gs.curB->sides[0].color);
+		playerToCall = (gs.getBattle(pack.battleID)->sides[0].color == activated->unitOwner()
+			? gs.getBattle(pack.battleID)->sides[1].color
+			: gs.getBattle(pack.battleID)->sides[0].color);
 	}
 	else
 	{
 		playerToCall = activated->unitOwner();
 	}
 
-	cl.startPlayerBattleAction(playerToCall);
+	cl.startPlayerBattleAction(pack.battleID, playerToCall);
 }
 
 void ApplyClientNetPackVisitor::visitBattleLogMessage(BattleLogMessage & pack)
 {
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleLogMessage, pack.lines);
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleLogMessage, pack.battleID, pack.lines);
 }
 
 void ApplyClientNetPackVisitor::visitBattleTriggerEffect(BattleTriggerEffect & pack)
 {
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleTriggerEffect, pack);
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleTriggerEffect, pack.battleID, pack);
 }
 
 void ApplyFirstClientNetPackVisitor::visitBattleUpdateGateState(BattleUpdateGateState & pack)
 {
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleGateStateChanged, pack.state);
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleGateStateChanged, pack.battleID, pack.state);
 }
 
 void ApplyFirstClientNetPackVisitor::visitBattleResult(BattleResult & pack)
 {
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleEnd, &pack, pack.queryID);
-	cl.battleFinished();
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleEnd, pack.battleID, &pack, pack.queryID);
+	cl.battleFinished(pack.battleID);
 }
 
 void ApplyFirstClientNetPackVisitor::visitBattleStackMoved(BattleStackMoved & pack)
 {
-	const CStack * movedStack = gs.curB->battleGetStackByID(pack.stack);
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStackMoved, movedStack, pack.tilesToMove, pack.distance, pack.teleporting);
+	const CStack * movedStack = gs.getBattle(pack.battleID)->battleGetStackByID(pack.stack);
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStackMoved, pack.battleID, movedStack, pack.tilesToMove, pack.distance, pack.teleporting);
 }
 
 void ApplyFirstClientNetPackVisitor::visitBattleAttack(BattleAttack & pack)
 {
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleAttack, &pack);
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleAttack, pack.battleID, &pack);
 
 	// battleStacksAttacked should be excuted before BattleAttack.applyGs() to play animation before damaging unit
 	// so this has to be here instead of ApplyClientNetPackVisitor::visitBattleAttack()
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksAttacked, pack.bsa, pack.shot());
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStacksAttacked, pack.battleID, pack.bsa, pack.shot());
 }
 
 void ApplyClientNetPackVisitor::visitBattleAttack(BattleAttack & pack)
@@ -801,23 +801,23 @@ void ApplyClientNetPackVisitor::visitBattleAttack(BattleAttack & pack)
 void ApplyFirstClientNetPackVisitor::visitStartAction(StartAction & pack)
 {
 	cl.currentBattleAction = std::make_unique<BattleAction>(pack.ba);
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::actionStarted, pack.ba);
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::actionStarted, pack.battleID, pack.ba);
 }
 
 void ApplyClientNetPackVisitor::visitBattleSpellCast(BattleSpellCast & pack)
 {
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleSpellCast, &pack);
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleSpellCast, pack.battleID, &pack);
 }
 
 void ApplyClientNetPackVisitor::visitSetStackEffect(SetStackEffect & pack)
 {
 	//informing about effects
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksEffectsSet, pack);
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStacksEffectsSet, pack.battleID, pack);
 }
 
 void ApplyClientNetPackVisitor::visitStacksInjured(StacksInjured & pack)
 {
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksAttacked, pack.stacks, false);
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStacksAttacked, pack.battleID, pack.stacks, false);
 }
 
 void ApplyClientNetPackVisitor::visitBattleResultsApplied(BattleResultsApplied & pack)
@@ -829,24 +829,24 @@ void ApplyClientNetPackVisitor::visitBattleResultsApplied(BattleResultsApplied &
 
 void ApplyClientNetPackVisitor::visitBattleUnitsChanged(BattleUnitsChanged & pack)
 {
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleUnitsChanged, pack.changedStacks);
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleUnitsChanged, pack.battleID, pack.changedStacks);
 }
 
 void ApplyClientNetPackVisitor::visitBattleObstaclesChanged(BattleObstaclesChanged & pack)
 {
 	//inform interfaces about removed obstacles
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleObstaclesChanged, pack.changes);
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleObstaclesChanged, pack.battleID, pack.changes);
 }
 
 void ApplyClientNetPackVisitor::visitCatapultAttack(CatapultAttack & pack)
 {
 	//inform interfaces about catapult attack
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleCatapultAttacked, pack);
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleCatapultAttacked, pack.battleID, pack);
 }
 
 void ApplyClientNetPackVisitor::visitEndAction(EndAction & pack)
 {
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::actionFinished, *cl.currentBattleAction);
+	callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::actionFinished, pack.battleID, *cl.currentBattleAction);
 	cl.currentBattleAction.reset();
 }
 

+ 19 - 19
client/battle/BattleActionsController.cpp

@@ -171,7 +171,7 @@ void BattleActionsController::enterCreatureCastingMode()
 		spells::Target target;
 		target.emplace_back();
 
-		spells::BattleCast cast(owner.curInt->cb.get(), caster, spells::Mode::CREATURE_ACTIVE, spell);
+		spells::BattleCast cast(owner.getBattle().get(), caster, spells::Mode::CREATURE_ACTIVE, spell);
 
 		auto m = spell->battleMechanics(&cast);
 		spells::detail::ProblemImpl ignored;
@@ -207,7 +207,7 @@ std::vector<PossiblePlayerBattleAction> BattleActionsController::getPossibleActi
 		data.creatureSpellsToCast.push_back(spell->id);
 
 	data.tacticsMode = owner.tacticsMode;
-	auto allActions = owner.curInt->cb->getClientActionsForStack(stack, data);
+	auto allActions = owner.getBattle()->getClientActionsForStack(stack, data);
 
 	allActions.push_back(PossiblePlayerBattleAction::HERO_INFO);
 	allActions.push_back(PossiblePlayerBattleAction::CREATURE_INFO);
@@ -231,7 +231,7 @@ void BattleActionsController::reorderPossibleActionsPriority(const CStack * stac
 			case PossiblePlayerBattleAction::OBSTACLE:
 				if(!stack->hasBonusOfType(BonusType::NO_SPELLCAST_BY_DEFAULT) && targetStack != nullptr)
 				{
-					PlayerColor stackOwner = owner.curInt->cb->battleGetOwner(targetStack);
+					PlayerColor stackOwner = owner.getBattle()->battleGetOwner(targetStack);
 					bool enemyTargetingPositiveSpellcast = item.spell().toSpell()->isPositive() && stackOwner != LOCPLINT->playerID;
 					bool friendTargetingNegativeSpellcast = item.spell().toSpell()->isNegative() && stackOwner == LOCPLINT->playerID;
 
@@ -300,12 +300,12 @@ void BattleActionsController::castThisSpell(SpellID spellID)
 	//choosing possible targets
 	const CGHeroInstance *castingHero = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? owner.attackingHeroInstance : owner.defendingHeroInstance;
 	assert(castingHero); // code below assumes non-null hero
-	PossiblePlayerBattleAction spellSelMode = owner.curInt->cb->getCasterAction(spellID.toSpell(), castingHero, spells::Mode::HERO);
+	PossiblePlayerBattleAction spellSelMode = owner.getBattle()->getCasterAction(spellID.toSpell(), castingHero, spells::Mode::HERO);
 
 	if (spellSelMode.get() == PossiblePlayerBattleAction::NO_LOCATION) //user does not have to select location
 	{
 		heroSpellToCast->aimToHex(BattleHex::INVALID);
-		owner.curInt->cb->battleMakeSpellAction(*heroSpellToCast);
+		owner.curInt->cb->battleMakeSpellAction(owner.getBattleID(), *heroSpellToCast);
 		endCastingSpell();
 	}
 	else
@@ -353,10 +353,10 @@ const CSpell * BattleActionsController::getCurrentSpell(BattleHex hoveredHex)
 
 const CStack * BattleActionsController::getStackForHex(BattleHex hoveredHex)
 {
-	const CStack * shere = owner.curInt->cb->battleGetStackByPos(hoveredHex, true);
+	const CStack * shere = owner.getBattle()->battleGetStackByPos(hoveredHex, true);
 	if(shere)
 		return shere;
-	return owner.curInt->cb->battleGetStackByPos(hoveredHex, false);
+	return owner.getBattle()->battleGetStackByPos(hoveredHex, false);
 }
 
 void BattleActionsController::actionSetCursor(PossiblePlayerBattleAction action, BattleHex targetHex)
@@ -400,7 +400,7 @@ void BattleActionsController::actionSetCursor(PossiblePlayerBattleAction action,
 		}
 
 		case PossiblePlayerBattleAction::SHOOT:
-			if (owner.curInt->cb->battleHasShootingPenalty(owner.stacksController->getActiveStack(), targetHex))
+			if (owner.getBattle()->battleHasShootingPenalty(owner.stacksController->getActiveStack(), targetHex))
 				CCS->curh->set(Cursor::Combat::SHOOT_PENALTY);
 			else
 				CCS->curh->set(Cursor::Combat::SHOOT);
@@ -482,7 +482,7 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle
 		case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return
 			{
 				BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex);
-				DamageEstimation estimation = owner.curInt->cb->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex);
+				DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex);
 				estimation.kills.max = std::min<int64_t>(estimation.kills.max, targetStack->getCount());
 				estimation.kills.min = std::min<int64_t>(estimation.kills.min, targetStack->getCount());
 
@@ -493,7 +493,7 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle
 		{
 			const auto * shooter = owner.stacksController->getActiveStack();
 
-			DamageEstimation estimation = owner.curInt->cb->battleEstimateDamage(shooter, targetStack, shooter->getPosition());
+			DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(shooter, targetStack, shooter->getPosition());
 			estimation.kills.max = std::min<int64_t>(estimation.kills.max, targetStack->getCount());
 			estimation.kills.min = std::min<int64_t>(estimation.kills.min, targetStack->getCount());
 
@@ -593,7 +593,7 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B
 		case PossiblePlayerBattleAction::ATTACK:
 		case PossiblePlayerBattleAction::WALK_AND_ATTACK:
 		case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
-			if(owner.curInt->cb->battleCanAttack(owner.stacksController->getActiveStack(), targetStack, targetHex))
+			if(owner.getBattle()->battleCanAttack(owner.stacksController->getActiveStack(), targetStack, targetHex))
 			{
 				if (owner.fieldController->isTileAttackable(targetHex)) // move isTileAttackable to be part of battleCanAttack?
 					return true;
@@ -601,7 +601,7 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B
 			return false;
 
 		case PossiblePlayerBattleAction::SHOOT:
-			return owner.curInt->cb->battleCanShoot(owner.stacksController->getActiveStack(), targetHex);
+			return owner.getBattle()->battleCanShoot(owner.stacksController->getActiveStack(), targetHex);
 
 		case PossiblePlayerBattleAction::NO_LOCATION:
 			return false;
@@ -615,7 +615,7 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B
 		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
 			if(targetStack && targetStackOwned && targetStack != owner.stacksController->getActiveStack() && targetStack->alive()) //only positive spells for other allied creatures
 			{
-				int spellID = owner.curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), targetStack, CBattleInfoCallback::RANDOM_GENIE);
+				int spellID = owner.getBattle()->battleGetRandomStackSpell(CRandomGenerator::getDefault(), targetStack, CBattleInfoCallback::RANDOM_GENIE);
 				return spellID > -1;
 			}
 			return false;
@@ -658,7 +658,7 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, B
 		{
 			if(owner.stacksController->getActiveStack()->doubleWide())
 			{
-				std::vector<BattleHex> acc = owner.curInt->cb->battleGetAvailableHexes(owner.stacksController->getActiveStack(), false);
+				std::vector<BattleHex> acc = owner.getBattle()->battleGetAvailableHexes(owner.stacksController->getActiveStack(), false);
 				BattleHex shiftedDest = targetHex.cloneInDirection(owner.stacksController->getActiveStack()->destShiftDir(), false);
 				if(vstd::contains(acc, targetHex))
 					owner.giveCommand(EActionType::WALK, targetHex);
@@ -770,7 +770,7 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, B
 						heroSpellToCast->aimToHex(targetHex);
 						break;
 				}
-				owner.curInt->cb->battleMakeSpellAction(*heroSpellToCast);
+				owner.curInt->cb->battleMakeSpellAction(owner.getBattleID(), *heroSpellToCast);
 				endCastingSpell();
 			}
 			selectedStack = nullptr;
@@ -886,7 +886,7 @@ void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterS
 	{
 		// faerie dragon can cast only one, randomly selected spell until their next move
 		//TODO: faerie dragon type spell should be selected by server
-		const auto * spellToCast = owner.curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), casterStack, CBattleInfoCallback::RANDOM_AIMED).toSpell();
+		const auto * spellToCast = owner.getBattle()->battleGetRandomStackSpell(CRandomGenerator::getDefault(), casterStack, CBattleInfoCallback::RANDOM_AIMED).toSpell();
 
 		if (spellToCast)
 			creatureSpells.push_back(spellToCast);
@@ -933,7 +933,7 @@ bool BattleActionsController::isCastingPossibleHere(const CSpell * currentSpell,
 		target.emplace_back(targetStack);
 	target.emplace_back(targetHex);
 
-	spells::BattleCast cast(owner.curInt->cb.get(), caster, mode, currentSpell);
+	spells::BattleCast cast(owner.getBattle().get(), caster, mode, currentSpell);
 
 	auto m = currentSpell->battleMechanics(&cast);
 	spells::detail::ProblemImpl problem; //todo: display problem in status bar
@@ -943,7 +943,7 @@ bool BattleActionsController::isCastingPossibleHere(const CSpell * currentSpell,
 
 bool BattleActionsController::canStackMoveHere(const CStack * stackToMove, BattleHex myNumber) const
 {
-	std::vector<BattleHex> acc = owner.curInt->cb->battleGetAvailableHexes(stackToMove, false);
+	std::vector<BattleHex> acc = owner.getBattle()->battleGetAvailableHexes(stackToMove, false);
 	BattleHex shiftedDest = myNumber.cloneInDirection(stackToMove->destShiftDir(), false);
 
 	if (vstd::contains(acc, myNumber))
@@ -1006,7 +1006,7 @@ void BattleActionsController::onHexRightClicked(BattleHex clickedHex)
 		return;
 	}
 
-	auto selectedStack = owner.curInt->cb->battleGetStackByPos(clickedHex, true);
+	auto selectedStack = owner.getBattle()->battleGetStackByPos(clickedHex, true);
 
 	if (selectedStack != nullptr)
 		GH.windows().createAndPushWindow<CStackWindow>(selectedStack, true);

+ 1 - 1
client/battle/BattleAnimationClasses.cpp

@@ -970,7 +970,7 @@ bool EffectAnimation::init()
 		}
 		else
 		{
-			const auto * destStack = owner.getCurrentPlayerInterface()->cb->battleGetUnitByPos(battlehexes[i], false);
+			const auto * destStack = owner.getBattle()->battleGetUnitByPos(battlehexes[i], false);
 			Rect tilePos = owner.fieldController->hexPositionLocal(battlehexes[i]);
 
 			be.pos.x = tilePos.x + tilePos.w/2 - first->width()/2;

+ 2 - 2
client/battle/BattleEffectsController.cpp

@@ -59,7 +59,7 @@ void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bt
 {
 	owner.checkForAnimations();
 
-	const CStack * stack = owner.curInt->cb->battleGetStackByID(bte.stackID);
+	const CStack * stack = owner.getBattle()->battleGetStackByID(bte.stackID);
 	if(!stack)
 	{
 		logGlobal->error("Invalid stack ID %d", bte.stackID);
@@ -98,7 +98,7 @@ void BattleEffectsController::startAction(const BattleAction & action)
 {
 	owner.checkForAnimations();
 
-	const CStack *stack = owner.curInt->cb->battleGetStackByID(action.stackNumber);
+	const CStack *stack = owner.getBattle()->battleGetStackByID(action.stackNumber);
 
 	switch(action.actionType)
 	{

+ 11 - 11
client/battle/BattleFieldController.cpp

@@ -141,7 +141,7 @@ BattleFieldController::BattleFieldController(BattleInterface & owner):
 
 	if(!owner.siegeController)
 	{
-		auto bfieldType = owner.curInt->cb->battleGetBattlefieldType();
+		auto bfieldType = owner.getBattle()->battleGetBattlefieldType();
 
 		if(bfieldType == BattleField::NONE)
 			logGlobal->error("Invalid battlefield returned for current battle");
@@ -284,7 +284,7 @@ void BattleFieldController::redrawBackgroundWithHexes()
 	const CStack *activeStack = owner.stacksController->getActiveStack();
 	std::vector<BattleHex> attackableHexes;
 	if(activeStack)
-		occupiableHexes = owner.curInt->cb->battleGetAvailableHexes(activeStack, false, true, &attackableHexes);
+		occupiableHexes = owner.getBattle()->battleGetAvailableHexes(activeStack, false, true, &attackableHexes);
 
 	// prepare background graphic with hexes and shaded hexes
 	backgroundWithHexes->draw(background, Point(0,0));
@@ -339,7 +339,7 @@ std::set<BattleHex> BattleFieldController::getHighlightedHexesForActiveStack()
 
 	auto hoveredHex = getHoveredHex();
 
-	std::set<BattleHex> set = owner.curInt->cb->battleGetAttackedHexes(owner.stacksController->getActiveStack(), hoveredHex);
+	std::set<BattleHex> set = owner.getBattle()->battleGetAttackedHexes(owner.stacksController->getActiveStack(), hoveredHex);
 	for(BattleHex hex : set)
 		result.insert(hex);
 
@@ -359,10 +359,10 @@ std::set<BattleHex> BattleFieldController::getMovementRangeForHoveredStack()
 	auto hoveredHex = getHoveredHex();
 
 	// add possible movement hexes for stack under mouse
-	const CStack * const hoveredStack = owner.curInt->cb->battleGetStackByPos(hoveredHex, true);
+	const CStack * const hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true);
 	if(hoveredStack)
 	{
-		std::vector<BattleHex> v = owner.curInt->cb->battleGetAvailableHexes(hoveredStack, true, true, nullptr);
+		std::vector<BattleHex> v = owner.getBattle()->battleGetAvailableHexes(hoveredStack, true, true, nullptr);
 		for(BattleHex hex : v)
 			result.insert(hex);
 	}
@@ -387,7 +387,7 @@ std::set<BattleHex> BattleFieldController::getHighlightedHexesForSpellRange()
 	if(caster && spell) //when casting spell
 	{
 		// printing shaded hex(es)
-		spells::BattleCast event(owner.curInt->cb.get(), caster, mode, spell);
+		spells::BattleCast event(owner.getBattle().get(), caster, mode, spell);
 		auto shadedHexes = spell->battleMechanics(&event)->rangeInHexes(hoveredHex);
 
 		for(BattleHex shadedHex : shadedHexes)
@@ -407,10 +407,10 @@ std::set<BattleHex> BattleFieldController::getHighlightedHexesForMovementTarget(
 	if(!stack)
 		return {};
 
-	std::vector<BattleHex> availableHexes = owner.curInt->cb->battleGetAvailableHexes(stack, false, false, nullptr);
+	std::vector<BattleHex> availableHexes = owner.getBattle()->battleGetAvailableHexes(stack, false, false, nullptr);
 
-	auto hoveredStack = owner.curInt->cb->battleGetStackByPos(hoveredHex, true);
-	if(owner.curInt->cb->battleCanAttack(stack, hoveredStack, hoveredHex))
+	auto hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true);
+	if(owner.getBattle()->battleCanAttack(stack, hoveredStack, hoveredHex))
 	{
 		if(isTileAttackable(hoveredHex))
 		{
@@ -670,7 +670,7 @@ BattleHex BattleFieldController::getHoveredHex()
 const CStack* BattleFieldController::getHoveredStack()
 {
 	auto hoveredHex = getHoveredHex();
-	const CStack* hoveredStack = owner.curInt->cb->battleGetStackByPos(hoveredHex, true);
+	const CStack* hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true);
 
 	return hoveredStack;
 }
@@ -856,7 +856,7 @@ bool BattleFieldController::isTileAttackable(const BattleHex & number) const
 
 void BattleFieldController::updateAccessibleHexes()
 {
-	auto accessibility = owner.curInt->cb->getAccesibility();
+	auto accessibility = owner.getBattle()->getAccesibility();
 
 	for(int i = 0; i < accessibility.size(); i++)
 		stackCountOutsideHexes[i] = (accessibility[i] == EAccessibility::ACCESSIBLE || (accessibility[i] == EAccessibility::SIDE_COLUMN));

+ 34 - 23
client/battle/BattleInterface.cpp

@@ -45,7 +45,7 @@
 #include "../../lib/TerrainHandler.h"
 #include "../../lib/CThreadHelper.h"
 
-BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet *army2,
+BattleInterface::BattleInterface(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2,
 		const CGHeroInstance *hero1, const CGHeroInstance *hero2,
 		std::shared_ptr<CPlayerInterface> att,
 		std::shared_ptr<CPlayerInterface> defen,
@@ -55,6 +55,7 @@ BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet *
 	, attackerInt(att)
 	, defenderInt(defen)
 	, curInt(att)
+	, battleID(battleID)
 	, battleOpeningDelayActive(true)
 {
 	if(spectatorInt)
@@ -68,9 +69,9 @@ BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet *
 	}
 
 	//hot-seat -> check tactics for both players (defender may be local human)
-	if(attackerInt && attackerInt->cb->battleGetTacticDist())
+	if(attackerInt && attackerInt->cb->getBattle(getBattleID())->battleGetTacticDist())
 		tacticianInterface = attackerInt;
-	else if(defenderInt && defenderInt->cb->battleGetTacticDist())
+	else if(defenderInt && defenderInt->cb->getBattle(getBattleID())->battleGetTacticDist())
 		tacticianInterface = defenderInt;
 
 	//if we found interface of player with tactics, then enter tactics mode
@@ -80,7 +81,7 @@ BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet *
 	this->army1 = army1;
 	this->army2 = army2;
 
-	const CGTownInstance *town = curInt->cb->battleGetDefendedTown();
+	const CGTownInstance *town = getBattle()->battleGetDefendedTown();
 	if(town && town->hasFort())
 		siegeController.reset(new BattleSiegeController(*this, town));
 
@@ -223,12 +224,12 @@ void BattleInterface::stackAttacking( const StackAttackInfo & attackInfo )
 	stacksController->stackAttacking(attackInfo);
 }
 
-void BattleInterface::newRoundFirst( int round )
+void BattleInterface::newRoundFirst()
 {
 	waitForAnimations();
 }
 
-void BattleInterface::newRound(int number)
+void BattleInterface::newRound()
 {
 	console->addText(CGI->generaltexth->allTexts[412]);
 }
@@ -241,7 +242,7 @@ void BattleInterface::giveCommand(EActionType action, BattleHex tile, SpellID sp
 		actor = stacksController->getActiveStack();
 	}
 
-	auto side = curInt->cb->playerToSide(curInt->playerID);
+	auto side = getBattle()->playerToSide(curInt->playerID);
 	if(!side)
 	{
 		logGlobal->error("Player %s is not in battle", curInt->playerID.toString());
@@ -265,11 +266,11 @@ void BattleInterface::sendCommand(BattleAction command, const CStack * actor)
 	{
 		logGlobal->trace("Setting command for %s", (actor ? actor->nodeName() : "hero"));
 		stacksController->setActiveStack(nullptr);
-		curInt->cb->battleMakeUnitAction(command);
+		curInt->cb->battleMakeUnitAction(battleID, command);
 	}
 	else
 	{
-		curInt->cb->battleMakeTacticAction(command);
+		curInt->cb->battleMakeTacticAction(battleID, command);
 		stacksController->setActiveStack(nullptr);
 		//next stack will be activated when action ends
 	}
@@ -368,13 +369,13 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
 
 	if ( sc->activeCast )
 	{
-		const CStack * casterStack = curInt->cb->battleGetStackByID(sc->casterStack);
+		const CStack * casterStack = getBattle()->battleGetStackByID(sc->casterStack);
 
 		if(casterStack != nullptr )
 		{
 			addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]()
 			{
-				stacksController->addNewAnim(new CastAnimation(*this, casterStack, targetedTile, curInt->cb->battleGetStackByPos(targetedTile), spell));
+				stacksController->addNewAnim(new CastAnimation(*this, casterStack, targetedTile, getBattle()->battleGetStackByPos(targetedTile), spell));
 				displaySpellCast(spell, casterStack->getPosition());
 			});
 		}
@@ -385,7 +386,7 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
 
 			addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]()
 			{
-				stacksController->addNewAnim(new HeroCastAnimation(*this, hero, targetedTile, curInt->cb->battleGetStackByPos(targetedTile), spell));
+				stacksController->addNewAnim(new HeroCastAnimation(*this, hero, targetedTile, getBattle()->battleGetStackByPos(targetedTile), spell));
 			});
 		}
 	}
@@ -397,7 +398,7 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
 	//queuing affect animation
 	for(auto & elem : sc->affectedCres)
 	{
-		auto stack = curInt->cb->battleGetStackByID(elem, false);
+		auto stack = getBattle()->battleGetStackByID(elem, false);
 		assert(stack);
 		if(stack)
 		{
@@ -409,7 +410,7 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
 
 	for(auto & elem : sc->reflectedCres)
 	{
-		auto stack = curInt->cb->battleGetStackByID(elem, false);
+		auto stack = getBattle()->battleGetStackByID(elem, false);
 		assert(stack);
 		addToAnimationStage(EAnimationEvents::HIT, [=](){
 			effectsController->displayEffect(EBattleEffect::MAGIC_MIRROR, stack->getPosition());
@@ -425,7 +426,7 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
 
 	for(auto & elem : sc->resistedCres)
 	{
-		auto stack = curInt->cb->battleGetStackByID(elem, false);
+		auto stack = getBattle()->battleGetStackByID(elem, false);
 		assert(stack);
 		addToAnimationStage(EAnimationEvents::HIT, [=](){
 			effectsController->displayEffect(EBattleEffect::RESISTANCE, stack->getPosition());
@@ -487,7 +488,7 @@ void BattleInterface::displaySpellAnimationQueue(const CSpell * spell, const CSp
 
 		if (!animation.effectName.empty())
 		{
-			const CStack * destStack = getCurrentPlayerInterface()->cb->battleGetStackByPos(destinationTile, false);
+			const CStack * destStack = getBattle()->battleGetStackByPos(destinationTile, false);
 
 			if (destStack)
 				stacksController->addNewAnim(new ColorTransformAnimation(*this, destStack, animation.effectName, spell ));
@@ -566,12 +567,22 @@ bool BattleInterface::makingTurn() const
 	return stacksController->getActiveStack() != nullptr;
 }
 
+BattleID BattleInterface::getBattleID() const
+{
+	return battleID;
+}
+
+std::shared_ptr<CPlayerBattleCallback> BattleInterface::getBattle() const
+{
+	return curInt->cb->getBattle(battleID);
+}
+
 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 = getBattle()->battleGetStackByID(action.stackNumber);
 
 	// Activate stack from stackToActivate because this might have been temporary disabled, e.g., during spell cast
 	activateStack();
@@ -606,7 +617,7 @@ void BattleInterface::startAction(const BattleAction & action)
 	if (!action.isUnitAction())
 		return;
 
-	assert(curInt->cb->battleGetStackByID(action.stackNumber));
+	assert(getBattle()->battleGetStackByID(action.stackNumber));
 	windowObject->updateQueue();
 	effectsController->startAction(action);
 }
@@ -616,10 +627,10 @@ void BattleInterface::tacticPhaseEnd()
 	stacksController->setActiveStack(nullptr);
 	tacticsMode = false;
 
-	auto side = tacticianInterface->cb->playerToSide(tacticianInterface->playerID);
+	auto side = tacticianInterface->cb->getBattle(battleID)->playerToSide(tacticianInterface->playerID);
 	auto action = BattleAction::makeEndOFTacticPhase(*side);
 
-	tacticianInterface->cb->battleMakeTacticAction(action);
+	tacticianInterface->cb->battleMakeTacticAction(battleID, action);
 }
 
 static bool immobile(const CStack *s)
@@ -635,7 +646,7 @@ void BattleInterface::tacticNextStack(const CStack * current)
 	//no switching stacks when the current one is moving
 	checkForAnimations();
 
-	TStacks stacksOfMine = tacticianInterface->cb->battleGetStacks(CBattleCallback::ONLY_MINE);
+	TStacks stacksOfMine = tacticianInterface->cb->getBattle(battleID)->battleGetStacks(CPlayerBattleCallback::ONLY_MINE);
 	vstd::erase_if (stacksOfMine, &immobile);
 	if (stacksOfMine.empty())
 	{
@@ -687,7 +698,7 @@ void BattleInterface::requestAutofightingAIToTakeAction()
 {
 	assert(curInt->isAutoFightOn);
 
-	if(curInt->cb->battleIsFinished())
+	if(getBattle()->battleIsFinished())
 	{
 		return; // battle finished with spellcast
 	}
@@ -716,7 +727,7 @@ void BattleInterface::requestAutofightingAIToTakeAction()
 			boost::thread aiThread([this, activeStack]()
 			{
 				setThreadName("autofightingAI");
-				curInt->autofightingAI->activeStack(activeStack);
+				curInt->autofightingAI->activeStack(battleID, activeStack);
 			});
 			aiThread.detach();
 		}

+ 10 - 3
client/battle/BattleInterface.h

@@ -30,6 +30,7 @@ struct BattleTriggerEffect;
 struct BattleHex;
 struct InfoAboutHero;
 class ObstacleChanges;
+class CPlayerBattleCallback;
 
 VCMI_LIB_NAMESPACE_END
 
@@ -115,6 +116,9 @@ class BattleInterface
 	/// if set to true, battle is still starting and waiting for intro sound to end / key press from player
 	bool battleOpeningDelayActive;
 
+	/// ID of ongoing battle
+	BattleID battleID;
+
 	void playIntroSoundAndUnlockInterface();
 	void onIntroSoundPlayed();
 public:
@@ -149,7 +153,10 @@ public:
 
 	bool makingTurn() const;
 
-	BattleInterface(const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, std::shared_ptr<CPlayerInterface> att, std::shared_ptr<CPlayerInterface> defen, std::shared_ptr<CPlayerInterface> spectatorInt = nullptr);
+	BattleID getBattleID() const;
+	std::shared_ptr<CPlayerBattleCallback> getBattle() const;
+
+	BattleInterface(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, std::shared_ptr<CPlayerInterface> att, std::shared_ptr<CPlayerInterface> defen, std::shared_ptr<CPlayerInterface> spectatorInt = nullptr);
 	~BattleInterface();
 
 	void trySetActivePlayer( PlayerColor player ); // if in hotseat, will activate interface of chosen player
@@ -196,8 +203,8 @@ public:
 	void stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance, bool teleport); //stack with id number moved to destHex
 	void stacksAreAttacked(std::vector<StackAttackedInfo> attackedInfos); //called when a certain amount of stacks has been attacked
 	void stackAttacking(const StackAttackInfo & attackInfo); //called when stack with id ID is attacking something on hex dest
-	void newRoundFirst( int round );
-	void newRound(int number); //caled when round is ended; number is the number of round
+	void newRoundFirst();
+	void newRound(); //caled when round is ended;
 	void stackIsCatapulting(const CatapultAttack & ca); //called when a stack is attacking walls
 	void battleFinished(const BattleResult& br, QueryID queryID); //called when battle is finished - battleresult window should be printed
 	void spellCast(const BattleSpellCast *sc); //called when a hero casts a spell

+ 6 - 6
client/battle/BattleInterfaceClasses.cpp

@@ -289,7 +289,7 @@ void BattleHero::heroLeftClicked()
 	if(!hero || !owner.makingTurn())
 		return;
 
-	if(owner.getCurrentPlayerInterface()->cb->battleCanCastSpell(hero, spells::Mode::HERO) == ESpellCastProblem::OK) //check conditions
+	if(owner.getBattle()->battleCanCastSpell(hero, spells::Mode::HERO) == ESpellCastProblem::OK) //check conditions
 	{
 		CCS->curh->set(Cursor::Map::POINTER);
 		GH.windows().createAndPushWindow<CSpellWindow>(hero, owner.getCurrentPlayerInterface());
@@ -502,7 +502,7 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface
 
 	for(int i = 0; i < 2; i++)
 	{
-		auto heroInfo = owner.cb->battleGetHeroInfo(i);
+		auto heroInfo = owner.cb->getBattle(br.battleID)->battleGetHeroInfo(i);
 		const int xs[] = {21, 392};
 
 		if(heroInfo.portrait >= 0) //attacking hero
@@ -512,7 +512,7 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface
 		}
 		else
 		{
-			auto stacks = owner.cb->battleGetAllStacks();
+			auto stacks = owner.cb->getBattle(br.battleID)->battleGetAllStacks();
 			vstd::erase_if(stacks, [i](const CStack * stack) //erase stack of other side and not coming from garrison
 			{
 				return stack->unitSide() != i || !stack->base;
@@ -561,7 +561,7 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface
 		}
 	}
 	//printing result description
-	bool weAreAttacker = !(owner.cb->battleGetMySide());
+	bool weAreAttacker = !(owner.cb->getBattle(br.battleID)->battleGetMySide());
 	if((br.winner == 0 && weAreAttacker) || (br.winner == 1 && !weAreAttacker)) //we've won
 	{
 		int text = 304;
@@ -584,7 +584,7 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface
 		CCS->videoh->open("WIN3.BIK");
 		std::string str = CGI->generaltexth->allTexts[text];
 
-		const CGHeroInstance * ourHero = owner.cb->battleGetMyHero();
+		const CGHeroInstance * ourHero = owner.cb->getBattle(br.battleID)->battleGetMyHero();
 		if (ourHero)
 		{
 			str += CGI->generaltexth->allTexts[305];
@@ -714,7 +714,7 @@ void StackQueue::update()
 {
 	std::vector<battle::Units> queueData;
 
-	owner.getCurrentPlayerInterface()->cb->battleGetTurnOrder(queueData, stackBoxes.size(), 0);
+	owner.getBattle()->battleGetTurnOrder(queueData, stackBoxes.size(), 0);
 
 	size_t boxIndex = 0;
 

+ 5 - 5
client/battle/BattleObstacleController.cpp

@@ -31,7 +31,7 @@ BattleObstacleController::BattleObstacleController(BattleInterface & owner):
 	owner(owner),
 	timePassed(0.f)
 {
-	auto obst = owner.curInt->cb->battleGetAllObstacles();
+	auto obst = owner.getBattle()->battleGetAllObstacles();
 	for(auto & elem : obst)
 	{
 		if ( elem->obstacleType == CObstacleInstance::MOAT )
@@ -99,9 +99,9 @@ void BattleObstacleController::obstaclePlaced(const std::vector<std::shared_ptr<
 {
 	for(const auto & oi : obstacles)
 	{
-		auto side = owner.curInt->cb->playerToSide(owner.curInt->playerID);
+		auto side = owner.getBattle()->playerToSide(owner.curInt->playerID);
 
-		if(!oi->visibleForSide(side.value(), owner.curInt->cb->battleHasNativeStack(side.value())))
+		if(!oi->visibleForSide(side.value(), owner.getBattle()->battleHasNativeStack(side.value())))
 			continue;
 
 		auto animation = std::make_shared<CAnimation>(oi->getAppearAnimation());
@@ -127,7 +127,7 @@ void BattleObstacleController::obstaclePlaced(const std::vector<std::shared_ptr<
 void BattleObstacleController::showAbsoluteObstacles(Canvas & canvas)
 {
 	//Blit absolute obstacles
-	for(auto & obstacle : owner.curInt->cb->battleGetAllObstacles())
+	for(auto & obstacle : owner.getBattle()->battleGetAllObstacles())
 	{
 		if(obstacle->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE)
 		{
@@ -153,7 +153,7 @@ void BattleObstacleController::showAbsoluteObstacles(Canvas & canvas)
 
 void BattleObstacleController::collectRenderableObjects(BattleRenderer & renderer)
 {
-	for (auto obstacle : owner.curInt->cb->battleGetAllObstacles())
+	for (auto obstacle : owner.getBattle()->battleGetAllObstacles())
 	{
 		if (obstacle->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE)
 			continue;

+ 9 - 9
client/battle/BattleSiegeController.cpp

@@ -133,9 +133,9 @@ bool BattleSiegeController::getWallPieceExistance(EWallVisual::EWallVisual what)
 	{
 	case EWallVisual::MOAT:              return town->hasBuilt(BuildingID::CITADEL) && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT).isValid();
 	case EWallVisual::MOAT_BANK:         return town->hasBuilt(BuildingID::CITADEL) && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT_BANK).isValid();
-	case EWallVisual::KEEP_BATTLEMENT:   return town->hasBuilt(BuildingID::CITADEL) && owner.curInt->cb->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED;
-	case EWallVisual::UPPER_BATTLEMENT:  return town->hasBuilt(BuildingID::CASTLE) && owner.curInt->cb->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED;
-	case EWallVisual::BOTTOM_BATTLEMENT: return town->hasBuilt(BuildingID::CASTLE) && owner.curInt->cb->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED;
+	case EWallVisual::KEEP_BATTLEMENT:   return town->hasBuilt(BuildingID::CITADEL) && owner.getBattle()->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED;
+	case EWallVisual::UPPER_BATTLEMENT:  return town->hasBuilt(BuildingID::CASTLE) && owner.getBattle()->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED;
+	case EWallVisual::BOTTOM_BATTLEMENT: return town->hasBuilt(BuildingID::CASTLE) && owner.getBattle()->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED;
 	default:                             return true;
 	}
 }
@@ -220,7 +220,7 @@ Point BattleSiegeController::getTurretCreaturePosition( BattleHex position ) con
 
 void BattleSiegeController::gateStateChanged(const EGateState state)
 {
-	auto oldState = owner.curInt->cb->battleGetGateState();
+	auto oldState = owner.getBattle()->battleGetGateState();
 	bool playSound = false;
 	auto stateId = EWallState::NONE;
 	switch(state)
@@ -275,7 +275,7 @@ BattleHex BattleSiegeController::getTurretBattleHex(EWallVisual::EWallVisual wal
 
 const CStack * BattleSiegeController::getTurretStack(EWallVisual::EWallVisual wallPiece) const
 {
-	for (auto & stack : owner.curInt->cb->battleGetAllStacks(true))
+	for (auto & stack : owner.getBattle()->battleGetAllStacks(true))
 	{
 		if ( stack->initialPosition == getTurretBattleHex(wallPiece))
 			return stack;
@@ -318,15 +318,15 @@ bool BattleSiegeController::isAttackableByCatapult(BattleHex hex) const
 	if (owner.tacticsMode)
 		return false;
 
-	auto wallPart = owner.curInt->cb->battleHexToWallPart(hex);
-	return owner.curInt->cb->isWallPartAttackable(wallPart);
+	auto wallPart = owner.getBattle()->battleHexToWallPart(hex);
+	return owner.getBattle()->isWallPartAttackable(wallPart);
 }
 
 void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca)
 {
 	if (ca.attacker != -1)
 	{
-		const CStack *stack = owner.curInt->cb->battleGetStackByID(ca.attacker);
+		const CStack *stack = owner.getBattle()->battleGetStackByID(ca.attacker);
 		for (auto attackInfo : ca.attackedParts)
 		{
 			owner.stacksController->addNewAnim(new CatapultAnimation(owner, stack, attackInfo.destinationTile, nullptr, attackInfo.damageDealt));
@@ -353,7 +353,7 @@ void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca)
 		if (wallId == EWallVisual::GATE)
 			continue;
 
-		auto wallState = EWallState(owner.curInt->cb->battleGetWallState(attackInfo.attackedPart));
+		auto wallState = EWallState(owner.getBattle()->battleGetWallState(attackInfo.attackedPart));
 
 		wallPieceImages[wallId] = IImage::createFromFile(getWallPieceImageName(EWallVisual::EWallVisual(wallId), wallState));
 	}

+ 8 - 10
client/battle/BattleStacksController.cpp

@@ -94,7 +94,7 @@ BattleStacksController::BattleStacksController(BattleInterface & owner):
 	amountNegative->adjustPalette(shifterNegative, ignoredMask);
 	amountEffNeutral->adjustPalette(shifterNeutral, ignoredMask);
 
-	std::vector<const CStack*> stacks = owner.curInt->cb->battleGetAllStacks(true);
+	std::vector<const CStack*> stacks = owner.getBattle()->battleGetAllStacks(true);
 	for(const CStack * s : stacks)
 	{
 		stackAdded(s, true);
@@ -126,7 +126,7 @@ BattleHex BattleStacksController::getStackCurrentPosition(const CStack * stack)
 
 void BattleStacksController::collectRenderableObjects(BattleRenderer & renderer)
 {
-	auto stacks = owner.curInt->cb->battleGetAllStacks(false);
+	auto stacks = owner.getBattle()->battleGetAllStacks(false);
 
 	for (auto stack : stacks)
 	{
@@ -359,7 +359,7 @@ void BattleStacksController::initializeBattleAnimations()
 
 void BattleStacksController::tickFrameBattleAnimations(uint32_t msPassed)
 {
-	for (auto stack : owner.curInt->cb->battleGetAllStacks(true))
+	for (auto stack : owner.getBattle()->battleGetAllStacks(true))
 	{
 		if (stackAnimation.find(stack->unitId()) == stackAnimation.end()) //e.g. for summoned but not yet handled stacks
 			continue;
@@ -552,9 +552,7 @@ void BattleStacksController::stackMoved(const CStack *stack, std::vector<BattleH
 
 bool BattleStacksController::shouldAttackFacingRight(const CStack * attacker, const CStack * defender)
 {
-	bool mustReverse = owner.curInt->cb->isToReverse(
-				attacker,
-				defender);
+	bool mustReverse = owner.getBattle()->isToReverse(attacker, defender);
 
 	if (attacker->unitSide() == BattleSide::ATTACKER)
 		return !mustReverse;
@@ -670,7 +668,7 @@ void BattleStacksController::endAction(const BattleAction & action)
 	owner.checkForAnimations();
 
 	//check if we should reverse stacks
-	TStacks stacks = owner.curInt->cb->battleGetStacks(CBattleCallback::MINE_AND_ENEMY);
+	TStacks stacks = owner.getBattle()->battleGetStacks(CPlayerBattleCallback::MINE_AND_ENEMY);
 
 	for (const CStack *s : stacks)
 	{
@@ -847,7 +845,7 @@ std::vector<const CStack *> BattleStacksController::selectHoveredStacks()
 	auto hoveredQueueUnitId = owner.windowObject->getQueueHoveredUnitId();
 	if(hoveredQueueUnitId.has_value())
 	{
-		return { owner.curInt->cb->battleGetStackByID(hoveredQueueUnitId.value(), true) };
+		return { owner.getBattle()->battleGetStackByID(hoveredQueueUnitId.value(), true) };
 	}
 
 	auto hoveredHex = owner.fieldController->getHoveredHex();
@@ -867,14 +865,14 @@ std::vector<const CStack *> BattleStacksController::selectHoveredStacks()
 		spells::Target target;
 		target.emplace_back(hoveredHex);
 
-		spells::BattleCast event(owner.curInt->cb.get(), caster, mode, spell);
+		spells::BattleCast event(owner.getBattle().get(), caster, mode, spell);
 		auto mechanics = spell->battleMechanics(&event);
 		return mechanics->getAffectedStacks(target);
 	}
 
 	if(hoveredHex.isValid())
 	{
-		const CStack * const stack = owner.curInt->cb->battleGetStackByPos(hoveredHex, true);
+		const CStack * const stack = owner.getBattle()->battleGetStackByPos(hoveredHex, true);
 
 		if (stack)
 			return {stack};

+ 12 - 12
client/battle/BattleWindow.cpp

@@ -367,10 +367,10 @@ void BattleWindow::bSurrenderf()
 	if (owner.actionsController->spellcastingModeActive())
 		return;
 
-	int cost = owner.curInt->cb->battleGetSurrenderCost();
+	int cost = owner.getBattle()->battleGetSurrenderCost();
 	if(cost >= 0)
 	{
-		std::string enemyHeroName = owner.curInt->cb->battleGetEnemyHero().name;
+		std::string enemyHeroName = owner.getBattle()->battleGetEnemyHero().name;
 		if(enemyHeroName.empty())
 		{
 			logGlobal->warn("Surrender performed without enemy hero, should not happen!");
@@ -387,7 +387,7 @@ void BattleWindow::bFleef()
 	if (owner.actionsController->spellcastingModeActive())
 		return;
 
-	if ( owner.curInt->cb->battleCanFlee() )
+	if ( owner.getBattle()->battleCanFlee() )
 	{
 		CFunctionList<void()> ony = std::bind(&BattleWindow::reallyFlee,this);
 		owner.curInt->showYesNoDialog(CGI->generaltexth->allTexts[28], ony, nullptr); //Are you sure you want to retreat?
@@ -398,10 +398,10 @@ void BattleWindow::bFleef()
 		std::string heroName;
 		//calculating fleeing hero's name
 		if (owner.attackingHeroInstance)
-			if (owner.attackingHeroInstance->tempOwner == owner.curInt->cb->getMyColor())
+			if (owner.attackingHeroInstance->tempOwner == owner.curInt->cb->getPlayerID())
 				heroName = owner.attackingHeroInstance->getNameTranslated();
 		if (owner.defendingHeroInstance)
-			if (owner.defendingHeroInstance->tempOwner == owner.curInt->cb->getMyColor())
+			if (owner.defendingHeroInstance->tempOwner == owner.curInt->cb->getPlayerID())
 				heroName = owner.defendingHeroInstance->getNameTranslated();
 		//calculating text
 		auto txt = boost::format(CGI->generaltexth->allTexts[340]) % heroName; //The Shackles of War are present.  %s can not retreat!
@@ -419,7 +419,7 @@ void BattleWindow::reallyFlee()
 
 void BattleWindow::reallySurrender()
 {
-	if (owner.curInt->cb->getResourceAmount(EGameResID::GOLD) < owner.curInt->cb->battleGetSurrenderCost())
+	if (owner.curInt->cb->getResourceAmount(EGameResID::GOLD) < owner.getBattle()->battleGetSurrenderCost())
 	{
 		owner.curInt->showInfoDialog(CGI->generaltexth->allTexts[29]); //You don't have enough gold!
 	}
@@ -509,7 +509,7 @@ void BattleWindow::bAutofightf()
 		autocombatPreferences.enableSpellsUsage = settings["battle"]["enableAutocombatSpells"].Bool();
 
 		ai->initBattleInterface(owner.curInt->env, owner.curInt->cb, autocombatPreferences);
-		ai->battleStart(owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.curInt->cb->battleGetMySide(), false);
+		ai->battleStart(owner.getBattleID(), owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.getBattle()->battleGetMySide(), false);
 		owner.curInt->autofightingAI = ai;
 		owner.curInt->cb->registerBattleInterface(ai);
 
@@ -531,7 +531,7 @@ void BattleWindow::bSpellf()
 
 	CCS->curh->set(Cursor::Map::POINTER);
 
-	ESpellCastProblem spellCastProblem = owner.curInt->cb->battleCanCastSpell(myHero, spells::Mode::HERO);
+	ESpellCastProblem spellCastProblem = owner.getBattle()->battleCanCastSpell(myHero, spells::Mode::HERO);
 
 	if(spellCastProblem == ESpellCastProblem::OK)
 	{
@@ -633,11 +633,11 @@ void BattleWindow::bTacticPhaseEnd()
 void BattleWindow::blockUI(bool on)
 {
 	bool canCastSpells = false;
-	auto hero = owner.curInt->cb->battleGetMyHero();
+	auto hero = owner.getBattle()->battleGetMyHero();
 
 	if(hero)
 	{
-		ESpellCastProblem spellcastingProblem = owner.curInt->cb->battleCanCastSpell(hero, spells::Mode::HERO);
+		ESpellCastProblem spellcastingProblem = owner.getBattle()->battleCanCastSpell(hero, spells::Mode::HERO);
 
 		//if magic is blocked, we leave button active, so the message can be displayed after button click
 		canCastSpells = spellcastingProblem == ESpellCastProblem::OK || spellcastingProblem == ESpellCastProblem::MAGIC_IS_BLOCKED;
@@ -646,8 +646,8 @@ void BattleWindow::blockUI(bool on)
 	bool canWait = owner.stacksController->getActiveStack() ? !owner.stacksController->getActiveStack()->waitedThisTurn : false;
 
 	setShortcutBlocked(EShortcut::GLOBAL_OPTIONS, on);
-	setShortcutBlocked(EShortcut::BATTLE_RETREAT, on || !owner.curInt->cb->battleCanFlee());
-	setShortcutBlocked(EShortcut::BATTLE_SURRENDER, on || owner.curInt->cb->battleGetSurrenderCost() < 0);
+	setShortcutBlocked(EShortcut::BATTLE_RETREAT, on || !owner.getBattle()->battleCanFlee());
+	setShortcutBlocked(EShortcut::BATTLE_SURRENDER, on || owner.getBattle()->battleGetSurrenderCost() < 0);
 	setShortcutBlocked(EShortcut::BATTLE_CAST_SPELL, on || owner.tacticsMode || !canCastSpells);
 	setShortcutBlocked(EShortcut::BATTLE_WAIT, on || owner.tacticsMode || !canWait);
 	setShortcutBlocked(EShortcut::BATTLE_DEFEND, on || owner.tacticsMode);

+ 1 - 1
client/windows/CSpellWindow.cpp

@@ -503,7 +503,7 @@ void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition)
 		else if(combatSpell)
 		{
 			spells::detail::ProblemImpl problem;
-			if(mySpell->canBeCast(problem, owner->myInt->cb.get(), spells::Mode::HERO, owner->myHero))
+			if(mySpell->canBeCast(problem, owner->myInt->battleInt->getBattle().get(), spells::Mode::HERO, owner->myHero))
 			{
 				owner->myInt->battleInt->castThisSpell(mySpell->id);
 				owner->fexitb();

+ 6 - 6
client/windows/GUIClasses.cpp

@@ -677,8 +677,8 @@ std::function<void()> CExchangeController::onSwapArmy()
 {
 	return [&]()
 	{
-		if(left->tempOwner != cb->getMyColor()
-		   || right->tempOwner != cb->getMyColor())
+		if(left->tempOwner != cb->getPlayerID()
+		   || right->tempOwner != cb->getPlayerID())
 		{
 			return;
 		}
@@ -720,7 +720,7 @@ std::function<void()> CExchangeController::onMoveStackToLeft(SlotID slotID)
 {
 	return [=]()
 	{
-		if(right->tempOwner != cb->getMyColor())
+		if(right->tempOwner != cb->getPlayerID())
 		{
 			return;
 		}
@@ -733,7 +733,7 @@ std::function<void()> CExchangeController::onMoveStackToRight(SlotID slotID)
 {
 	return [=]()
 	{
-		if(left->tempOwner != cb->getMyColor())
+		if(left->tempOwner != cb->getPlayerID())
 		{
 			return;
 		}
@@ -778,7 +778,7 @@ void CExchangeController::moveArmy(bool leftToRight)
 	const CGarrisonSlot * selection =  this->view->getSelectedSlotID();
 	SlotID slot;
 
-	if(source->tempOwner != cb->getMyColor())
+	if(source->tempOwner != cb->getPlayerID())
 	{
 		return;
 	}
@@ -807,7 +807,7 @@ void CExchangeController::moveArtifacts(bool leftToRight)
 	const CGHeroInstance * source = leftToRight ? left : right;
 	const CGHeroInstance * target = leftToRight ? right : left;
 
-	if(source->tempOwner != cb->getMyColor())
+	if(source->tempOwner != cb->getPlayerID())
 	{
 		return;
 	}

+ 0 - 2
cmake_modules/VCMI_lib.cmake

@@ -15,7 +15,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 		${MAIN_LIB_DIR}/battle/BattleStateInfoForRetreat.cpp
 		${MAIN_LIB_DIR}/battle/CBattleInfoCallback.cpp
 		${MAIN_LIB_DIR}/battle/CBattleInfoEssentials.cpp
-		${MAIN_LIB_DIR}/battle/CCallbackBase.cpp
 		${MAIN_LIB_DIR}/battle/CObstacleInstance.cpp
 		${MAIN_LIB_DIR}/battle/CPlayerBattleCallback.cpp
 		${MAIN_LIB_DIR}/battle/CUnitState.cpp
@@ -339,7 +338,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 		${MAIN_LIB_DIR}/battle/BattleProxy.h
 		${MAIN_LIB_DIR}/battle/CBattleInfoCallback.h
 		${MAIN_LIB_DIR}/battle/CBattleInfoEssentials.h
-		${MAIN_LIB_DIR}/battle/CCallbackBase.h
 		${MAIN_LIB_DIR}/battle/CObstacleInstance.h
 		${MAIN_LIB_DIR}/battle/CPlayerBattleCallback.h
 		${MAIN_LIB_DIR}/battle/CUnitState.h

+ 2 - 1
include/vcmi/Environment.h

@@ -16,6 +16,7 @@ class Services;
 
 class IGameInfoCallback;
 class IBattleInfoCallback;
+class BattleID;
 
 namespace events
 {
@@ -31,7 +32,7 @@ public:
 	virtual ~Environment() = default;
 
 	virtual const Services * services() const = 0;
-	virtual const BattleCb * battle() const = 0;
+	virtual const BattleCb * battle(const BattleID & battleID) const = 0;
 	virtual const GameCb * game() const = 0;
 	virtual vstd::CLoggerBase * logger() const = 0;
 	virtual events::EventBus * eventBus() const = 0;

+ 47 - 44
lib/CGameInfoCallback.cpp

@@ -66,6 +66,11 @@ bool CGameInfoCallback::isAllowed(int32_t type, int32_t id) const
 	}
 }
 
+std::optional<PlayerColor> CGameInfoCallback::getPlayerID() const
+{
+	return std::nullopt;
+}
+
 const Player * CGameInfoCallback::getPlayer(PlayerColor color) const
 {
 	return getPlayerState(color, false);
@@ -151,7 +156,7 @@ const CGObjectInstance* CGameInfoCallback::getObj(ObjectInstanceID objid, bool v
 		return nullptr;
 	}
 
-	if(!isVisible(ret, player) && ret->tempOwner != player)
+	if(!isVisible(ret, getPlayerID()) && ret->tempOwner != getPlayerID())
 	{
 		if(verbose)
 			logGlobal->error("Cannot get object with id %d. Object is not visible.", oid);
@@ -201,8 +206,10 @@ int32_t CGameInfoCallback::getSpellCost(const spells::Spell * sp, const CGHeroIn
 	//boost::shared_lock<boost::shared_mutex> lock(*gs->mx);
 	ERROR_RET_VAL_IF(!canGetFullInfo(caster), "Cannot get info about caster!", -1);
 	//if there is a battle
-	if(gs->curB)
-		return gs->curB->battleGetSpellCost(sp, caster);
+	auto casterBattle = gs->getBattle(caster->getOwner());
+
+	if(casterBattle)
+		return casterBattle->battleGetSpellCost(sp, caster);
 
 	//if there is no battle
 	return caster->getSpellCost(sp);
@@ -230,7 +237,7 @@ void CGameInfoCallback::getThievesGuildInfo(SThievesGuildInfo & thi, const CGObj
 	if(obj->ID == Obj::TOWN  ||  obj->ID == Obj::TAVERN)
 	{
 		int taverns = 0;
-		for(auto town : gs->players[*player].towns)
+		for(auto town : gs->players[*getPlayerID()].towns)
 		{
 			if(town->hasBuilt(BuildingID::TAVERN))
 				taverns++;
@@ -252,7 +259,7 @@ int CGameInfoCallback::howManyTowns(PlayerColor Player) const
 
 bool CGameInfoCallback::getTownInfo(const CGObjectInstance * town, InfoAboutTown & dest, const CGObjectInstance * selectedObject) const
 {
-	ERROR_RET_VAL_IF(!isVisible(town, player), "Town is not visible!", false);  //it's not a town or it's not visible for layer
+	ERROR_RET_VAL_IF(!isVisible(town, getPlayerID()), "Town is not visible!", false);  //it's not a town or it's not visible for layer
 	bool detailed = hasAccess(town->tempOwner);
 
 	if(town->ID == Obj::TOWN)
@@ -303,7 +310,9 @@ bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero
 
 	if (infoLevel == InfoAboutHero::EInfoLevel::BASIC)
 	{
-		if(gs->curB && gs->curB->playerHasAccessToHeroInfo(*player, h)) //if it's battle we can get enemy hero full data
+		auto ourBattle = gs->getBattle(*getPlayerID());
+
+		if(ourBattle && ourBattle->playerHasAccessToHeroInfo(*getPlayerID(), h)) //if it's battle we can get enemy hero full data
 			infoLevel = InfoAboutHero::EInfoLevel::INBATTLE;
 		else
 			ERROR_RET_VAL_IF(!isVisible(h->visitablePos()), "That hero is not visible!", false);
@@ -320,7 +329,7 @@ bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero
 	dest.initFromHero(h, infoLevel);
 
 	//DISGUISED bonus implementation
-	if(getPlayerRelations(*player, hero->tempOwner) == PlayerRelations::ENEMIES)
+	if(getPlayerRelations(*getPlayerID(), hero->tempOwner) == PlayerRelations::ENEMIES)
 	{
 		//todo: bonus cashing
 		int disguiseLevel = h->valOfBonuses(Selector::typeSubtype(BonusType::DISGUISED, 0));
@@ -418,7 +427,7 @@ bool CGameInfoCallback::isVisible(int3 pos, const std::optional<PlayerColor> & P
 
 bool CGameInfoCallback::isVisible(int3 pos) const
 {
-	return isVisible(pos, player);
+	return isVisible(pos, getPlayerID());
 }
 
 bool CGameInfoCallback::isVisible(const CGObjectInstance * obj, const std::optional<PlayerColor> & Player) const
@@ -428,7 +437,7 @@ bool CGameInfoCallback::isVisible(const CGObjectInstance * obj, const std::optio
 
 bool CGameInfoCallback::isVisible(const CGObjectInstance *obj) const
 {
-	return isVisible(obj, player);
+	return isVisible(obj, getPlayerID());
 }
 // const CCreatureSet* CInfoCallback::getGarrison(const CGObjectInstance *obj) const
 // {
@@ -460,7 +469,7 @@ std::vector <const CGObjectInstance * > CGameInfoCallback::getVisitableObjs(int3
 
 	for(const CGObjectInstance * obj : t->visitableObjects)
 	{
-		if(player || obj->ID != Obj::EVENT) //hide events from players
+		if(getPlayerID() || obj->ID != Obj::EVENT) //hide events from players
 			ret.push_back(obj);
 	}
 
@@ -496,7 +505,7 @@ std::vector<const CGHeroInstance *> CGameInfoCallback::getAvailableHeroes(const
 	const CGTownInstance * town = getTown(townOrTavern->id);
 
 	if(townOrTavern->ID == Obj::TAVERN || (town && town->hasBuilt(BuildingID::TAVERN)))
-		return gs->heroesPool->getHeroesFor(*player);
+		return gs->heroesPool->getHeroesFor(*getPlayerID());
 
 	return ret;
 }
@@ -527,8 +536,8 @@ EDiggingStatus CGameInfoCallback::getTileDigStatus(int3 tile, bool verbose) cons
 //TODO: typedef?
 std::shared_ptr<const boost::multi_array<TerrainTile*, 3>> CGameInfoCallback::getAllVisibleTiles() const
 {
-	assert(player.has_value());
-	const auto * team = getPlayerTeam(player.value());
+	assert(getPlayerID().has_value());
+	const auto * team = getPlayerTeam(getPlayerID().value());
 
 	size_t width = gs->map->width;
 	size_t height = gs->map->height;
@@ -627,7 +636,7 @@ const CMapHeader * CGameInfoCallback::getMapHeader() const
 
 bool CGameInfoCallback::hasAccess(std::optional<PlayerColor> playerId) const
 {
-	return !player || player->isSpectator() || gs->getPlayerRelations(*playerId, *player) != PlayerRelations::ENEMIES;
+	return !getPlayerID() || getPlayerID()->isSpectator() || gs->getPlayerRelations(*playerId, *getPlayerID()) != PlayerRelations::ENEMIES;
 }
 
 EPlayerStatus CGameInfoCallback::getPlayerStatus(PlayerColor player, bool verbose) const
@@ -709,23 +718,22 @@ bool CGameInfoCallback::isPlayerMakingTurn(PlayerColor player) const
 	return gs->actingPlayers.count(player);
 }
 
-CGameInfoCallback::CGameInfoCallback(CGameState * GS, std::optional<PlayerColor> Player):
+CGameInfoCallback::CGameInfoCallback(CGameState * GS):
 	gs(GS)
 {
-	player = std::move(Player);
 }
 
 std::shared_ptr<const boost::multi_array<ui8, 3>> CPlayerSpecificInfoCallback::getVisibilityMap() const
 {
 	//boost::shared_lock<boost::shared_mutex> lock(*gs->mx);
-	return gs->getPlayerTeam(*player)->fogOfWarMap;
+	return gs->getPlayerTeam(*getPlayerID())->fogOfWarMap;
 }
 
 int CPlayerSpecificInfoCallback::howManyTowns() const
 {
 	//boost::shared_lock<boost::shared_mutex> lock(*gs->mx);
-	ERROR_RET_VAL_IF(!player, "Applicable only for player callbacks", -1);
-	return CGameInfoCallback::howManyTowns(*player);
+	ERROR_RET_VAL_IF(!getPlayerID(), "Applicable only for player callbacks", -1);
+	return CGameInfoCallback::howManyTowns(*getPlayerID());
 }
 
 std::vector < const CGTownInstance *> CPlayerSpecificInfoCallback::getTownsInfo(bool onlyOur) const
@@ -736,7 +744,7 @@ std::vector < const CGTownInstance *> CPlayerSpecificInfoCallback::getTownsInfo(
 	{
 		for(const auto & town : i.second.towns)
 		{
-			if(i.first == player || (!onlyOur && isVisible(town, player)))
+			if(i.first == getPlayerID() || (!onlyOur && isVisible(town, getPlayerID())))
 			{
 				ret.push_back(town);
 			}
@@ -751,8 +759,8 @@ std::vector < const CGHeroInstance *> CPlayerSpecificInfoCallback::getHeroesInfo
 	for(auto hero : gs->map->heroesOnMap)
 	{
 		// !player || // - why would we even get access to hero not owned by any player?
-		if((hero->tempOwner == *player) ||
-			(isVisible(hero->visitablePos(), player) && !onlyOur)	)
+		if((hero->tempOwner == *getPlayerID()) ||
+			(isVisible(hero->visitablePos(), getPlayerID()) && !onlyOur)	)
 		{
 			ret.push_back(hero);
 		}
@@ -760,18 +768,13 @@ std::vector < const CGHeroInstance *> CPlayerSpecificInfoCallback::getHeroesInfo
 	return ret;
 }
 
-std::optional<PlayerColor> CPlayerSpecificInfoCallback::getMyColor() const
-{
-	return player;
-}
-
 int CPlayerSpecificInfoCallback::getHeroSerial(const CGHeroInstance * hero, bool includeGarrisoned) const
 {
 	if (hero->inTownGarrison && !includeGarrisoned)
 		return -1;
 
 	size_t index = 0;
-	auto & heroes = gs->players[*player].heroes;
+	auto & heroes = gs->players[*getPlayerID()].heroes;
 
 	for (auto & heroe : heroes)
 	{
@@ -786,13 +789,13 @@ int CPlayerSpecificInfoCallback::getHeroSerial(const CGHeroInstance * hero, bool
 
 int3 CPlayerSpecificInfoCallback::getGrailPos( double *outKnownRatio )
 {
-	if (!player || CGObelisk::obeliskCount == 0)
+	if (!getPlayerID() || CGObelisk::obeliskCount == 0)
 	{
 		*outKnownRatio = 0.0;
 	}
 	else
 	{
-		TeamID t = gs->getPlayerTeam(*player)->id;
+		TeamID t = gs->getPlayerTeam(*getPlayerID())->id;
 		double visited = 0.0;
 		if(CGObelisk::visited.count(t))
 			visited = static_cast<double>(CGObelisk::visited[t]);
@@ -807,7 +810,7 @@ std::vector < const CGObjectInstance * > CPlayerSpecificInfoCallback::getMyObjec
 	std::vector < const CGObjectInstance * > ret;
 	for(const CGObjectInstance * obj : gs->map->objects)
 	{
-		if(obj && obj->tempOwner == player)
+		if(obj && obj->tempOwner == getPlayerID())
 			ret.push_back(obj);
 	}
 	return ret;
@@ -817,7 +820,7 @@ std::vector < const CGDwelling * > CPlayerSpecificInfoCallback::getMyDwellings()
 {
 	ASSERT_IF_CALLED_WITH_PLAYER
 	std::vector < const CGDwelling * > ret;
-	for(CGDwelling * dw : gs->getPlayerState(*player)->dwellings)
+	for(CGDwelling * dw : gs->getPlayerState(*getPlayerID())->dwellings)
 	{
 		ret.push_back(dw);
 	}
@@ -827,7 +830,7 @@ std::vector < const CGDwelling * > CPlayerSpecificInfoCallback::getMyDwellings()
 std::vector <QuestInfo> CPlayerSpecificInfoCallback::getMyQuests() const
 {
 	std::vector <QuestInfo> ret;
-	for(const auto & quest : gs->getPlayerState(*player)->quests)
+	for(const auto & quest : gs->getPlayerState(*getPlayerID())->quests)
 	{
 		ret.push_back (quest);
 	}
@@ -837,14 +840,14 @@ std::vector <QuestInfo> CPlayerSpecificInfoCallback::getMyQuests() const
 int CPlayerSpecificInfoCallback::howManyHeroes(bool includeGarrisoned) const
 {
 	//boost::shared_lock<boost::shared_mutex> lock(*gs->mx);
-	ERROR_RET_VAL_IF(!player, "Applicable only for player callbacks", -1);
-	return getHeroCount(*player,includeGarrisoned);
+	ERROR_RET_VAL_IF(!getPlayerID(), "Applicable only for player callbacks", -1);
+	return getHeroCount(*getPlayerID(), includeGarrisoned);
 }
 
 const CGHeroInstance* CPlayerSpecificInfoCallback::getHeroBySerial(int serialId, bool includeGarrisoned) const
 {
 	ASSERT_IF_CALLED_WITH_PLAYER
-	const PlayerState *p = getPlayerState(*player);
+	const PlayerState *p = getPlayerState(*getPlayerID());
 	ERROR_RET_VAL_IF(!p, "No player info", nullptr);
 
 	if (!includeGarrisoned)
@@ -860,7 +863,7 @@ const CGHeroInstance* CPlayerSpecificInfoCallback::getHeroBySerial(int serialId,
 const CGTownInstance* CPlayerSpecificInfoCallback::getTownBySerial(int serialId) const
 {
 	ASSERT_IF_CALLED_WITH_PLAYER
-	const PlayerState *p = getPlayerState(*player);
+	const PlayerState *p = getPlayerState(*getPlayerID());
 	ERROR_RET_VAL_IF(!p, "No player info", nullptr);
 	ERROR_RET_VAL_IF(serialId < 0 || serialId >= p->towns.size(), "No player info", nullptr);
 	return p->towns[serialId];
@@ -869,15 +872,15 @@ const CGTownInstance* CPlayerSpecificInfoCallback::getTownBySerial(int serialId)
 int CPlayerSpecificInfoCallback::getResourceAmount(GameResID type) const
 {
 	//boost::shared_lock<boost::shared_mutex> lock(*gs->mx);
-	ERROR_RET_VAL_IF(!player, "Applicable only for player callbacks", -1);
-	return getResource(*player, type);
+	ERROR_RET_VAL_IF(!getPlayerID(), "Applicable only for player callbacks", -1);
+	return getResource(*getPlayerID(), type);
 }
 
 TResources CPlayerSpecificInfoCallback::getResourceAmount() const
 {
 	//boost::shared_lock<boost::shared_mutex> lock(*gs->mx);
-	ERROR_RET_VAL_IF(!player, "Applicable only for player callbacks", TResources());
-	return gs->players[*player].resources;
+	ERROR_RET_VAL_IF(!getPlayerID(), "Applicable only for player callbacks", TResources());
+	return gs->players[*getPlayerID()].resources;
 }
 
 const TeamState * CGameInfoCallback::getTeam( TeamID teamID ) const
@@ -888,11 +891,11 @@ const TeamState * CGameInfoCallback::getTeam( TeamID teamID ) const
 	if (team != gs->teams.end())
 	{
 		const TeamState *ret = &team->second;
-		if(!player.has_value()) //neutral (or invalid) player
+		if(!getPlayerID().has_value()) //neutral (or invalid) player
 			return ret;
 		else
 		{
-			if (vstd::contains(ret->players, *player)) //specific player
+			if (vstd::contains(ret->players, *getPlayerID())) //specific player
 				return ret;
 			else
 			{
@@ -938,7 +941,7 @@ bool CGameInfoCallback::isInTheMap(const int3 &pos) const
 
 void CGameInfoCallback::getVisibleTilesInRange(std::unordered_set<int3> &tiles, int3 pos, int radious, int3::EDistanceFormula distanceFormula) const
 {
-	gs->getTilesInRange(tiles, pos, radious, *player, -1, distanceFormula);
+	gs->getTilesInRange(tiles, pos, radious, *getPlayerID(), -1, distanceFormula);
 }
 
 void CGameInfoCallback::calculatePaths(const std::shared_ptr<PathfinderConfig> & config)

+ 7 - 5
lib/CGameInfoCallback.h

@@ -11,7 +11,8 @@
 
 #include "int3.h"
 #include "ResourceSet.h" // for Res
-#include "battle/CCallbackBase.h"
+
+#define ASSERT_IF_CALLED_WITH_PLAYER if(!getPlayerID()) {logGlobal->error(BOOST_CURRENT_FUNCTION); assert(0);}
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -46,7 +47,7 @@ class CGDwelling;
 class CGTeleport;
 class CGTownInstance;
 
-class DLL_LINKAGE IGameInfoCallback
+class DLL_LINKAGE IGameInfoCallback : boost::noncopyable
 {
 public:
 	//TODO: all other public methods of CGameInfoCallback
@@ -57,6 +58,7 @@ public:
 	virtual bool isAllowed(int32_t type, int32_t id) const = 0; //type: 0 - spell; 1- artifact; 2 - secondary skill
 
 	//player
+	virtual std::optional<PlayerColor> getPlayerID() const = 0;
 	virtual const Player * getPlayer(PlayerColor color) const = 0;
 //	virtual int getResource(PlayerColor Player, EGameResID which) const = 0;
 //	bool isVisible(int3 pos) const;
@@ -123,13 +125,13 @@ public:
 //	bool isTeleportEntrancePassable(const CGTeleport * obj, PlayerColor player) const;
 };
 
-class DLL_LINKAGE CGameInfoCallback : public virtual CCallbackBase, public IGameInfoCallback
+class DLL_LINKAGE CGameInfoCallback : public IGameInfoCallback
 {
 protected:
 	CGameState * gs;//todo: replace with protected const getter, only actual Server and Client objects should hold game state
 
 	CGameInfoCallback() = default;
-	CGameInfoCallback(CGameState * GS, std::optional<PlayerColor> Player);
+	CGameInfoCallback(CGameState * GS);
 	bool hasAccess(std::optional<PlayerColor> playerId) const;
 
 	bool canGetFullInfo(const CGObjectInstance *obj) const; //true we player owns obj or ally owns obj or privileged mode
@@ -142,6 +144,7 @@ public:
 	bool isAllowed(int32_t type, int32_t id) const override; //type: 0 - spell; 1- artifact; 2 - secondary skill
 
 	//player
+	std::optional<PlayerColor> getPlayerID() const override;
 	const Player * getPlayer(PlayerColor color) const override;
 	virtual const PlayerState * getPlayerState(PlayerColor color, bool verbose = true) const;
 	virtual int getResource(PlayerColor Player, GameResID which) const;
@@ -229,7 +232,6 @@ public:
 	virtual int howManyTowns() const;
 	virtual int howManyHeroes(bool includeGarrisoned = true) const;
 	virtual int3 getGrailPos(double *outKnownRatio);
-	virtual std::optional<PlayerColor> getMyColor() const;
 
 	virtual std::vector <const CGTownInstance *> getTownsInfo(bool onlyOur = true) const; //true -> only owned; false -> all visible
 	virtual int getHeroSerial(const CGHeroInstance * hero, bool includeGarrisoned=true) const;

+ 32 - 32
lib/CGameInterface.cpp

@@ -157,90 +157,90 @@ CGlobalAI::CGlobalAI()
 	human = false;
 }
 
-void CAdventureAI::battleNewRound(int round)
+void CAdventureAI::battleNewRound(const BattleID & battleID)
 {
-	battleAI->battleNewRound(round);
+	battleAI->battleNewRound(battleID);
 }
 
-void CAdventureAI::battleCatapultAttacked(const CatapultAttack & ca)
+void CAdventureAI::battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca)
 {
-	battleAI->battleCatapultAttacked(ca);
+	battleAI->battleCatapultAttacked(battleID, ca);
 }
 
-void CAdventureAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile,
+void CAdventureAI::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile,
 							   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, replayAllowed);
+	battleAI->battleStart(battleID, army1, army2, tile, hero1, hero2, side, replayAllowed);
 }
 
-void CAdventureAI::battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged)
+void CAdventureAI::battleStacksAttacked(const BattleID & battleID, const std::vector<BattleStackAttacked> & bsa, bool ranged)
 {
-	battleAI->battleStacksAttacked(bsa, ranged);
+	battleAI->battleStacksAttacked(battleID, bsa, ranged);
 }
 
-void CAdventureAI::actionStarted(const BattleAction & action)
+void CAdventureAI::actionStarted(const BattleID & battleID, const BattleAction & action)
 {
-	battleAI->actionStarted(action);
+	battleAI->actionStarted(battleID, action);
 }
 
-void CAdventureAI::battleNewRoundFirst(int round)
+void CAdventureAI::battleNewRoundFirst(const BattleID & battleID)
 {
-	battleAI->battleNewRoundFirst(round);
+	battleAI->battleNewRoundFirst(battleID);
 }
 
-void CAdventureAI::actionFinished(const BattleAction & action)
+void CAdventureAI::actionFinished(const BattleID & battleID, const BattleAction & action)
 {
-	battleAI->actionFinished(action);
+	battleAI->actionFinished(battleID, action);
 }
 
-void CAdventureAI::battleStacksEffectsSet(const SetStackEffect & sse)
+void CAdventureAI::battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse)
 {
-	battleAI->battleStacksEffectsSet(sse);
+	battleAI->battleStacksEffectsSet(battleID, sse);
 }
 
-void CAdventureAI::battleObstaclesChanged(const std::vector<ObstacleChanges> & obstacles)
+void CAdventureAI::battleObstaclesChanged(const BattleID & battleID, const std::vector<ObstacleChanges> & obstacles)
 {
-	battleAI->battleObstaclesChanged(obstacles);
+	battleAI->battleObstaclesChanged(battleID, obstacles);
 }
 
-void CAdventureAI::battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport)
+void CAdventureAI::battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport)
 {
-	battleAI->battleStackMoved(stack, dest, distance, teleport);
+	battleAI->battleStackMoved(battleID, stack, dest, distance, teleport);
 }
 
-void CAdventureAI::battleAttack(const BattleAttack * ba)
+void CAdventureAI::battleAttack(const BattleID & battleID, const BattleAttack * ba)
 {
-	battleAI->battleAttack(ba);
+	battleAI->battleAttack(battleID, ba);
 }
 
-void CAdventureAI::battleSpellCast(const BattleSpellCast * sc)
+void CAdventureAI::battleSpellCast(const BattleID & battleID, const BattleSpellCast * sc)
 {
-	battleAI->battleSpellCast(sc);
+	battleAI->battleSpellCast(battleID, sc);
 }
 
-void CAdventureAI::battleEnd(const BattleResult * br, QueryID queryID)
+void CAdventureAI::battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID)
 {
-	battleAI->battleEnd(br, queryID);
+	battleAI->battleEnd(battleID, br, queryID);
 	battleAI.reset();
 }
 
-void CAdventureAI::battleUnitsChanged(const std::vector<UnitChanges> & units)
+void CAdventureAI::battleUnitsChanged(const BattleID & battleID, const std::vector<UnitChanges> & units)
 {
-	battleAI->battleUnitsChanged(units);
+	battleAI->battleUnitsChanged(battleID, units);
 }
 
-void CAdventureAI::activeStack(const CStack * stack)
+void CAdventureAI::activeStack(const BattleID & battleID, const CStack * stack)
 {
-	battleAI->activeStack(stack);
+	battleAI->activeStack(battleID, stack);
 }
 
-void CAdventureAI::yourTacticPhase(int distance)
+void CAdventureAI::yourTacticPhase(const BattleID & battleID, int distance)
 {
-	battleAI->yourTacticPhase(distance);
+	battleAI->yourTacticPhase(battleID, distance);
 }
 
 void CAdventureAI::saveGame(BinarySerializer & h, const int version) /*saving */

+ 20 - 19
lib/CGameInterface.h

@@ -80,8 +80,8 @@ public:
 	virtual void initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB, AutocombatPreferences autocombatPreferences){};
 
 	//battle call-ins
-	virtual void activeStack(const CStack * stack)=0; //called when it's turn of that stack
-	virtual void yourTacticPhase(int distance)=0; //called when interface has opportunity to use Tactics skill -> use cb->battleMakeTacticAction from this function
+	virtual void activeStack(const BattleID & battleID, const CStack * stack)=0; //called when it's turn of that stack
+	virtual void yourTacticPhase(const BattleID & battleID, int distance)=0; //called when interface has opportunity to use Tactics skill -> use cb->battleMakeTacticAction from this function
 };
 
 /// Central class for managing human player / AI interface logic
@@ -109,7 +109,7 @@ public:
 
 	virtual void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain){};
 
-	virtual std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) = 0;
+	virtual std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) = 0;
 
 	virtual void saveGame(BinarySerializer & h, const int version) = 0;
 	virtual void loadGame(BinaryDeserializer & h, const int version) = 0;
@@ -144,22 +144,23 @@ public:
 	virtual std::string getBattleAIName() const = 0; //has to return name of the battle AI to be used
 
 	//battle interface
-	virtual void activeStack(const CStack * stack) override;
-	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, 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;
-	virtual void actionFinished(const BattleAction &action) override;
-	virtual void battleStacksEffectsSet(const SetStackEffect & sse) override;
-	virtual void battleObstaclesChanged(const std::vector<ObstacleChanges> & obstacles) override;
-	virtual void battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport) override;
-	virtual void battleAttack(const BattleAttack *ba) override;
-	virtual void battleSpellCast(const BattleSpellCast *sc) override;
-	virtual void battleEnd(const BattleResult *br, QueryID queryID) override;
-	virtual void battleUnitsChanged(const std::vector<UnitChanges> & units) override;
+	virtual void activeStack(const BattleID & battleID, const CStack * stack) override;
+	virtual void yourTacticPhase(const BattleID & battleID, int distance) override;
+
+	virtual void battleNewRound(const BattleID & battleID) override;
+	virtual void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override;
+	virtual void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override;
+	virtual void battleStacksAttacked(const BattleID & battleID, const std::vector<BattleStackAttacked> & bsa, bool ranged) override;
+	virtual void actionStarted(const BattleID & battleID, const BattleAction &action) override;
+	virtual void battleNewRoundFirst(const BattleID & battleID) override;
+	virtual void actionFinished(const BattleID & battleID, const BattleAction &action) override;
+	virtual void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override;
+	virtual void battleObstaclesChanged(const BattleID & battleID, const std::vector<ObstacleChanges> & obstacles) override;
+	virtual void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport) override;
+	virtual void battleAttack(const BattleID & battleID, const BattleAttack *ba) override;
+	virtual void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override;
+	virtual void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override;
+	virtual void battleUnitsChanged(const BattleID & battleID, const std::vector<UnitChanges> & units) override;
 
 	virtual void saveGame(BinarySerializer & h, const int version) override;
 	virtual void loadGame(BinaryDeserializer & h, const int version) override;

+ 18 - 18
lib/IGameEventsReceiver.h

@@ -56,24 +56,24 @@ class UnitChanges;
 class DLL_LINKAGE IBattleEventsReceiver
 {
 public:
-	virtual void actionFinished(const BattleAction &action){};//occurs AFTER every action taken by any stack or by the hero
-	virtual void actionStarted(const BattleAction &action){};//occurs BEFORE every action taken by any stack or by the hero
-	virtual void battleAttack(const BattleAttack *ba){}; //called when stack is performing attack
-	virtual void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged){}; //called when stack receives damage (after battleAttack())
-	virtual void battleEnd(const BattleResult *br, QueryID queryID){};
-	virtual void battleNewRoundFirst(int round){}; //called at the beginning of each turn before changes are applied;
-	virtual void battleNewRound(int round){}; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn
-	virtual void battleLogMessage(const std::vector<MetaString> & lines){};
-	virtual void battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport){};
-	virtual void battleSpellCast(const BattleSpellCast *sc){};
-	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, 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
-	virtual void battleGateStateChanged(const EGateState state){};
+	virtual void actionFinished(const BattleID & battleID, const BattleAction &action){};//occurs AFTER every action taken by any stack or by the hero
+	virtual void actionStarted(const BattleID & battleID, const BattleAction &action){};//occurs BEFORE every action taken by any stack or by the hero
+	virtual void battleAttack(const BattleID & battleID, const BattleAttack *ba){}; //called when stack is performing attack
+	virtual void battleStacksAttacked(const BattleID & battleID, const std::vector<BattleStackAttacked> & bsa, bool ranged){}; //called when stack receives damage (after battleAttack())
+	virtual void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID){};
+	virtual void battleNewRoundFirst(const BattleID & battleID){}; //called at the beginning of each turn before changes are applied;
+	virtual void battleNewRound(const BattleID & battleID){}; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn
+	virtual void battleLogMessage(const BattleID & battleID, const std::vector<MetaString> & lines){};
+	virtual void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport){};
+	virtual void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc){};
+	virtual void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse){};//called when a specific effect is set to stacks
+	virtual void battleTriggerEffect(const BattleID & battleID, const BattleTriggerEffect & bte){}; //called for various one-shot effects
+	virtual void battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) {}; //called just before battle start
+	virtual void battleStart(const BattleID & battleID, 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 BattleID & battleID, const std::vector<UnitChanges> & units){};
+	virtual void battleObstaclesChanged(const BattleID & battleID, const std::vector<ObstacleChanges> & obstacles){};
+	virtual void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca){}; //called when catapult makes an attack
+	virtual void battleGateStateChanged(const BattleID & battleID, const EGateState state){};
 };
 
 class DLL_LINKAGE IGameEventsReceiver

+ 83 - 3
lib/NetPacks.h

@@ -39,6 +39,7 @@ struct StackLocation;
 struct ArtSlotInfo;
 struct QuestInfo;
 class IBattleState;
+class BattleInfo;
 
 // This one teleport-specific, but has to be available everywhere in callbacks and netpacks
 // For now it's will be there till teleports code refactored and moved into own file
@@ -1478,31 +1479,35 @@ struct DLL_LINKAGE MapObjectSelectDialog : public Query
 	}
 };
 
-class BattleInfo;
 struct DLL_LINKAGE BattleStart : public CPackForClient
 {
 	void applyGs(CGameState * gs) const;
 
+	BattleID battleID = BattleID::NONE;
 	BattleInfo * info = nullptr;
 
 	virtual void visitTyped(ICPackVisitor & visitor) override;
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
+		h & battleID;
 		h & info;
+		assert(battleID != BattleID::NONE);
 	}
 };
 
 struct DLL_LINKAGE BattleNextRound : public CPackForClient
 {
 	void applyGs(CGameState * gs) const;
-	si32 round = 0;
+
+	BattleID battleID = BattleID::NONE;
 
 	virtual void visitTyped(ICPackVisitor & visitor) override;
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
-		h & round;
+		h & battleID;
+		assert(battleID != BattleID::NONE);
 	}
 };
 
@@ -1510,6 +1515,7 @@ struct DLL_LINKAGE BattleSetActiveStack : public CPackForClient
 {
 	void applyGs(CGameState * gs) const;
 
+	BattleID battleID = BattleID::NONE;
 	ui32 stack = 0;
 	ui8 askPlayerInterface = true;
 
@@ -1517,8 +1523,23 @@ struct DLL_LINKAGE BattleSetActiveStack : public CPackForClient
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
+		h & battleID;
 		h & stack;
 		h & askPlayerInterface;
+		assert(battleID != BattleID::NONE);
+	}
+};
+
+struct DLL_LINKAGE BattleCancelled: public CPackForClient
+{
+	void applyGs(CGameState * gs) const;
+
+	BattleID battleID = BattleID::NONE;
+
+	template <typename Handler> void serialize(Handler & h, const int version)
+	{
+		h & battleID;
+		assert(battleID != BattleID::NONE);
 	}
 };
 
@@ -1542,13 +1563,17 @@ struct DLL_LINKAGE BattleResultAccepted : public CPackForClient
 			h & exp;
 		}
 	};
+
+	BattleID battleID = BattleID::NONE;
 	std::array<HeroBattleResults, 2> heroResult;
 	ui8 winnerSide;
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
+		h & battleID;
 		h & heroResult;
 		h & winnerSide;
+		assert(battleID != BattleID::NONE);
 	}
 };
 
@@ -1556,6 +1581,7 @@ struct DLL_LINKAGE BattleResult : public Query
 {
 	void applyFirstCl(CClient * cl);
 
+	BattleID battleID = BattleID::NONE;
 	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
@@ -1566,6 +1592,7 @@ struct DLL_LINKAGE BattleResult : public Query
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
+		h & battleID;
 		h & queryID;
 		h & result;
 		h & winner;
@@ -1573,11 +1600,13 @@ struct DLL_LINKAGE BattleResult : public Query
 		h & casualties[1];
 		h & exp;
 		h & artifacts;
+		assert(battleID != BattleID::NONE);
 	}
 };
 
 struct DLL_LINKAGE BattleLogMessage : public CPackForClient
 {
+	BattleID battleID = BattleID::NONE;
 	std::vector<MetaString> lines;
 
 	void applyGs(CGameState * gs);
@@ -1587,12 +1616,15 @@ struct DLL_LINKAGE BattleLogMessage : public CPackForClient
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
+		h & battleID;
 		h & lines;
+		assert(battleID != BattleID::NONE);
 	}
 };
 
 struct DLL_LINKAGE BattleStackMoved : public CPackForClient
 {
+	BattleID battleID = BattleID::NONE;
 	ui32 stack = 0;
 	std::vector<BattleHex> tilesToMove;
 	int distance = 0;
@@ -1605,10 +1637,12 @@ struct DLL_LINKAGE BattleStackMoved : public CPackForClient
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
+		h & battleID;
 		h & stack;
 		h & tilesToMove;
 		h & distance;
 		h & teleporting;
+		assert(battleID != BattleID::NONE);
 	}
 };
 
@@ -1617,13 +1651,16 @@ struct DLL_LINKAGE BattleUnitsChanged : public CPackForClient
 	void applyGs(CGameState * gs);
 	void applyBattle(IBattleState * battleState);
 
+	BattleID battleID = BattleID::NONE;
 	std::vector<UnitChanges> changedStacks;
 
 	virtual void visitTyped(ICPackVisitor & visitor) override;
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
+		h & battleID;
 		h & changedStacks;
+		assert(battleID != BattleID::NONE);
 	}
 };
 
@@ -1632,6 +1669,7 @@ struct BattleStackAttacked
 	DLL_LINKAGE void applyGs(CGameState * gs);
 	DLL_LINKAGE void applyBattle(IBattleState * battleState);
 
+	BattleID battleID = BattleID::NONE;
 	ui32 stackAttacked = 0, attackerID = 0;
 	ui32 killedAmount = 0;
 	int64_t damageAmount = 0;
@@ -1668,6 +1706,7 @@ struct BattleStackAttacked
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
+		h & battleID;
 		h & stackAttacked;
 		h & attackerID;
 		h & newState;
@@ -1675,6 +1714,7 @@ struct BattleStackAttacked
 		h & killedAmount;
 		h & damageAmount;
 		h & spellID;
+		assert(battleID != BattleID::NONE);
 	}
 	bool operator<(const BattleStackAttacked & b) const
 	{
@@ -1687,6 +1727,7 @@ struct DLL_LINKAGE BattleAttack : public CPackForClient
 	void applyGs(CGameState * gs);
 	BattleUnitsChanged attackerChanges;
 
+	BattleID battleID = BattleID::NONE;
 	std::vector<BattleStackAttacked> bsa;
 	ui32 stackAttacking = 0;
 	ui32 flags = 0; //uses Eflags (below)
@@ -1732,12 +1773,14 @@ struct DLL_LINKAGE BattleAttack : public CPackForClient
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
+		h & battleID;
 		h & bsa;
 		h & stackAttacking;
 		h & flags;
 		h & tile;
 		h & spellID;
 		h & attackerChanges;
+		assert(battleID != BattleID::NONE);
 	}
 };
 
@@ -1751,13 +1794,16 @@ struct DLL_LINKAGE StartAction : public CPackForClient
 	void applyFirstCl(CClient * cl);
 	void applyGs(CGameState * gs);
 
+	BattleID battleID = BattleID::NONE;
 	BattleAction ba;
 
 	virtual void visitTyped(ICPackVisitor & visitor) override;
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
+		h & battleID;
 		h & ba;
+		assert(battleID != BattleID::NONE);
 	}
 };
 
@@ -1765,14 +1811,19 @@ struct DLL_LINKAGE EndAction : public CPackForClient
 {
 	virtual void visitTyped(ICPackVisitor & visitor) override;
 
+	BattleID battleID = BattleID::NONE;
+
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
+		h & battleID;
 	}
 };
 
 struct DLL_LINKAGE BattleSpellCast : public CPackForClient
 {
 	void applyGs(CGameState * gs) const;
+
+	BattleID battleID = BattleID::NONE;
 	bool activeCast = true;
 	ui8 side = 0; //which hero did cast spell: 0 - attacker, 1 - defender
 	SpellID spellID; //id of spell
@@ -1788,6 +1839,7 @@ struct DLL_LINKAGE BattleSpellCast : public CPackForClient
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
+		h & battleID;
 		h & side;
 		h & spellID;
 		h & manaGained;
@@ -1798,6 +1850,7 @@ struct DLL_LINKAGE BattleSpellCast : public CPackForClient
 		h & casterStack;
 		h & castByHero;
 		h & activeCast;
+		assert(battleID != BattleID::NONE);
 	}
 };
 
@@ -1805,6 +1858,8 @@ struct DLL_LINKAGE SetStackEffect : public CPackForClient
 {
 	void applyGs(CGameState * gs);
 	void applyBattle(IBattleState * battleState);
+
+	BattleID battleID = BattleID::NONE;
 	std::vector<std::pair<ui32, std::vector<Bonus>>> toAdd;
 	std::vector<std::pair<ui32, std::vector<Bonus>>> toUpdate;
 	std::vector<std::pair<ui32, std::vector<Bonus>>> toRemove;
@@ -1813,9 +1868,11 @@ struct DLL_LINKAGE SetStackEffect : public CPackForClient
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
+		h & battleID;
 		h & toAdd;
 		h & toUpdate;
 		h & toRemove;
+		assert(battleID != BattleID::NONE);
 	}
 };
 
@@ -1824,25 +1881,31 @@ struct DLL_LINKAGE StacksInjured : public CPackForClient
 	void applyGs(CGameState * gs);
 	void applyBattle(IBattleState * battleState);
 
+	BattleID battleID = BattleID::NONE;
 	std::vector<BattleStackAttacked> stacks;
 
 	virtual void visitTyped(ICPackVisitor & visitor) override;
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
+		h & battleID;
 		h & stacks;
+		assert(battleID != BattleID::NONE);
 	}
 };
 
 struct DLL_LINKAGE BattleResultsApplied : public CPackForClient
 {
+	BattleID battleID = BattleID::NONE;
 	PlayerColor player1, player2;
 	virtual void visitTyped(ICPackVisitor & visitor) override;
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
+		h & battleID;
 		h & player1;
 		h & player2;
+		assert(battleID != BattleID::NONE);
 	}
 };
 
@@ -1851,13 +1914,16 @@ struct DLL_LINKAGE BattleObstaclesChanged : public CPackForClient
 	void applyGs(CGameState * gs);
 	void applyBattle(IBattleState * battleState);
 
+	BattleID battleID = BattleID::NONE;
 	std::vector<ObstacleChanges> changes;
 
 	virtual void visitTyped(ICPackVisitor & visitor) override;
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
+		h & battleID;
 		h & changes;
+		assert(battleID != BattleID::NONE);
 	}
 };
 
@@ -1883,6 +1949,7 @@ struct DLL_LINKAGE CatapultAttack : public CPackForClient
 	void applyGs(CGameState * gs);
 	void applyBattle(IBattleState * battleState);
 
+	BattleID battleID = BattleID::NONE;
 	std::vector< AttackInfo > attackedParts;
 	int attacker = -1; //if -1, then a spell caused this
 
@@ -1890,8 +1957,10 @@ struct DLL_LINKAGE CatapultAttack : public CPackForClient
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
+		h & battleID;
 		h & attackedParts;
 		h & attacker;
+		assert(battleID != BattleID::NONE);
 	}
 };
 
@@ -1901,6 +1970,7 @@ struct DLL_LINKAGE BattleSetStackProperty : public CPackForClient
 
 	void applyGs(CGameState * gs) const;
 
+	BattleID battleID = BattleID::NONE;
 	int stackID = 0;
 	BattleStackProperty which = CASTS;
 	int val = 0;
@@ -1908,10 +1978,12 @@ struct DLL_LINKAGE BattleSetStackProperty : public CPackForClient
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
+		h & battleID;
 		h & stackID;
 		h & which;
 		h & val;
 		h & absolute;
+		assert(battleID != BattleID::NONE);
 	}
 
 protected:
@@ -1923,6 +1995,7 @@ struct DLL_LINKAGE BattleTriggerEffect : public CPackForClient
 {
 	void applyGs(CGameState * gs) const; //effect
 
+	BattleID battleID = BattleID::NONE;
 	int stackID = 0;
 	int effect = 0; //use corresponding Bonus type
 	int val = 0;
@@ -1930,10 +2003,12 @@ struct DLL_LINKAGE BattleTriggerEffect : public CPackForClient
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
+		h & battleID;
 		h & stackID;
 		h & effect;
 		h & val;
 		h & additionalInfo;
+		assert(battleID != BattleID::NONE);
 	}
 
 protected:
@@ -1944,10 +2019,13 @@ struct DLL_LINKAGE BattleUpdateGateState : public CPackForClient
 {
 	void applyGs(CGameState * gs) const;
 
+	BattleID battleID = BattleID::NONE;
 	EGateState state = EGateState::NONE;
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
+		h & battleID;
 		h & state;
+		assert(battleID != BattleID::NONE);
 	}
 
 protected:
@@ -2533,6 +2611,7 @@ struct DLL_LINKAGE MakeAction : public CPackForServer
 	{
 	}
 	BattleAction ba;
+	BattleID battleID;
 
 	virtual void visitTyped(ICPackVisitor & visitor) override;
 
@@ -2540,6 +2619,7 @@ struct DLL_LINKAGE MakeAction : public CPackForServer
 	{
 		h & static_cast<CPackForServer &>(*this);
 		h & ba;
+		h & battleID;
 	}
 };
 

+ 47 - 43
lib/NetPacksLib.cpp

@@ -34,11 +34,8 @@
 #include "campaign/CampaignState.h"
 #include "GameSettings.h"
 
-
 VCMI_LIB_NAMESPACE_BEGIN
 
-#define THROW_IF_NO_BATTLE if (!gs->curB) throw std::runtime_error("Trying to apply pack when no battle!");
-
 void CPack::visit(ICPackVisitor & visitor)
 {
 	visitBasic(visitor);
@@ -962,7 +959,7 @@ void GiveBonus::applyGs(CGameState *gs)
 		break;
 	case ETarget::BATTLE:
 		assert(Bonus::OneBattle(&bonus));
-		cbsn = dynamic_cast<CBonusSystemNode*>(gs->curB.get());
+		cbsn = dynamic_cast<CBonusSystemNode*>(gs->getBattle(BattleID(id)));
 		break;
 	}
 
@@ -2115,26 +2112,29 @@ void CommanderLevelUp::applyGs(CGameState * gs) const
 
 void BattleStart::applyGs(CGameState * gs) const
 {
-	gs->curB = info;
-	gs->curB->localInit();
+	assert(battleID == gs->nextBattleID);
+
+	gs->currentBattles.emplace_back(info);
+
+	info->battleID = gs->nextBattleID;
+	info->localInit();
+
+	gs->nextBattleID = vstd::next(gs->nextBattleID, 1);
 }
 
 void BattleNextRound::applyGs(CGameState * gs) const
 {
-	THROW_IF_NO_BATTLE
-	gs->curB->nextRound(round);
+	gs->getBattle(battleID)->nextRound();
 }
 
 void BattleSetActiveStack::applyGs(CGameState * gs) const
 {
-	THROW_IF_NO_BATTLE
-	gs->curB->nextTurn(stack);
+	gs->getBattle(battleID)->nextTurn(stack);
 }
 
 void BattleTriggerEffect::applyGs(CGameState * gs) const
 {
-	THROW_IF_NO_BATTLE
-	CStack * st = gs->curB->getStack(stackID);
+	CStack * st = gs->getBattle(battleID)->getStack(stackID);
 	assert(st);
 	switch(static_cast<BonusType>(effect))
 	{
@@ -2173,8 +2173,19 @@ void BattleTriggerEffect::applyGs(CGameState * gs) const
 
 void BattleUpdateGateState::applyGs(CGameState * gs) const
 {
-	if(gs->curB)
-		gs->curB->si.gateState = state;
+	if(gs->getBattle(battleID))
+		gs->getBattle(battleID)->si.gateState = state;
+}
+
+void BattleCancelled::applyGs(CGameState * gs) const
+{
+	auto currentBattle = boost::range::find_if(gs->currentBattles, [&](const auto & battle)
+	{
+		return battle->battleID == battleID;
+	});
+
+	assert(currentBattle != gs->currentBattles.end());
+	gs->currentBattles.erase(currentBattle);
 }
 
 void BattleResultAccepted::applyGs(CGameState * gs) const
@@ -2214,7 +2225,13 @@ void BattleResultAccepted::applyGs(CGameState * gs) const
 		CBonusSystemNode::treeHasChanged();
 	}
 
-	gs->curB.dellNull();
+	auto currentBattle = boost::range::find_if(gs->currentBattles, [&](const auto & battle)
+	{
+		return battle->battleID == battleID;
+	});
+
+	assert(currentBattle != gs->currentBattles.end());
+	gs->currentBattles.erase(currentBattle);
 }
 
 void BattleLogMessage::applyGs(CGameState *gs)
@@ -2229,8 +2246,7 @@ void BattleLogMessage::applyBattle(IBattleState * battleState)
 
 void BattleStackMoved::applyGs(CGameState *gs)
 {
-	THROW_IF_NO_BATTLE
-	applyBattle(gs->curB);
+	applyBattle(gs->getBattle(battleID));
 }
 
 void BattleStackMoved::applyBattle(IBattleState * battleState)
@@ -2240,8 +2256,7 @@ void BattleStackMoved::applyBattle(IBattleState * battleState)
 
 void BattleStackAttacked::applyGs(CGameState * gs)
 {
-	THROW_IF_NO_BATTLE
-	applyBattle(gs->curB);
+	applyBattle(gs->getBattle(battleID));
 }
 
 void BattleStackAttacked::applyBattle(IBattleState * battleState)
@@ -2251,8 +2266,7 @@ void BattleStackAttacked::applyBattle(IBattleState * battleState)
 
 void BattleAttack::applyGs(CGameState * gs)
 {
-	THROW_IF_NO_BATTLE
-	CStack * attacker = gs->curB->getStack(stackAttacking);
+	CStack * attacker = gs->getBattle(battleID)->getStack(stackAttacking);
 	assert(attacker);
 
 	attackerChanges.applyGs(gs);
@@ -2265,17 +2279,15 @@ void BattleAttack::applyGs(CGameState * gs)
 
 void StartAction::applyGs(CGameState *gs)
 {
-	THROW_IF_NO_BATTLE
-
-	CStack *st = gs->curB->getStack(ba.stackNumber);
+	CStack *st = gs->getBattle(battleID)->getStack(ba.stackNumber);
 
 	if(ba.actionType == EActionType::END_TACTIC_PHASE)
 	{
-		gs->curB->tacticDistance = 0;
+		gs->getBattle(battleID)->tacticDistance = 0;
 		return;
 	}
 
-	if(gs->curB->tacticDistance)
+	if(gs->getBattle(battleID)->tacticDistance)
 	{
 		// moves in tactics phase do not affect creature status
 		// (tactics stack queue is managed by client)
@@ -2310,27 +2322,24 @@ void StartAction::applyGs(CGameState *gs)
 	else
 	{
 		if(ba.actionType == EActionType::HERO_SPELL)
-			gs->curB->sides[ba.side].usedSpellsHistory.push_back(ba.spell);
+			gs->getBattle(battleID)->sides[ba.side].usedSpellsHistory.push_back(ba.spell);
 	}
 }
 
 void BattleSpellCast::applyGs(CGameState * gs) const
 {
-	THROW_IF_NO_BATTLE
-
 	if(castByHero)
 	{
 		if(side < 2)
 		{
-			gs->curB->sides[side].castSpellsCount++;
+			gs->getBattle(battleID)->sides[side].castSpellsCount++;
 		}
 	}
 }
 
 void SetStackEffect::applyGs(CGameState *gs)
 {
-	THROW_IF_NO_BATTLE
-	applyBattle(gs->curB);
+	applyBattle(gs->getBattle(battleID));
 }
 
 void SetStackEffect::applyBattle(IBattleState * battleState)
@@ -2348,8 +2357,7 @@ void SetStackEffect::applyBattle(IBattleState * battleState)
 
 void StacksInjured::applyGs(CGameState *gs)
 {
-	THROW_IF_NO_BATTLE
-	applyBattle(gs->curB);
+	applyBattle(gs->getBattle(battleID));
 }
 
 void StacksInjured::applyBattle(IBattleState * battleState)
@@ -2360,8 +2368,7 @@ void StacksInjured::applyBattle(IBattleState * battleState)
 
 void BattleUnitsChanged::applyGs(CGameState *gs)
 {
-	THROW_IF_NO_BATTLE
-	applyBattle(gs->curB);
+	applyBattle(gs->getBattle(battleID));
 }
 
 void BattleUnitsChanged::applyBattle(IBattleState * battleState)
@@ -2391,8 +2398,7 @@ void BattleUnitsChanged::applyBattle(IBattleState * battleState)
 
 void BattleObstaclesChanged::applyGs(CGameState * gs)
 {
-	THROW_IF_NO_BATTLE;
-	applyBattle(gs->curB);
+	applyBattle(gs->getBattle(battleID));
 }
 
 void BattleObstaclesChanged::applyBattle(IBattleState * battleState)
@@ -2423,8 +2429,7 @@ CatapultAttack::~CatapultAttack() = default;
 
 void CatapultAttack::applyGs(CGameState * gs)
 {
-	THROW_IF_NO_BATTLE
-	applyBattle(gs->curB);
+	applyBattle(gs->getBattle(battleID));
 }
 
 void CatapultAttack::visitTyped(ICPackVisitor & visitor)
@@ -2450,8 +2455,7 @@ void CatapultAttack::applyBattle(IBattleState * battleState)
 
 void BattleSetStackProperty::applyGs(CGameState * gs) const
 {
-	THROW_IF_NO_BATTLE
-	CStack * stack = gs->curB->getStack(stackID);
+	CStack * stack = gs->getBattle(battleID)->getStack(stackID);
 	switch(which)
 	{
 		case CASTS:
@@ -2464,7 +2468,7 @@ void BattleSetStackProperty::applyGs(CGameState * gs) const
 		}
 		case ENCHANTER_COUNTER:
 		{
-			auto & counter = gs->curB->sides[gs->curB->whatSide(stack->unitOwner())].enchanterCounter;
+			auto & counter = gs->getBattle(battleID)->sides[gs->getBattle(battleID)->whatSide(stack->unitOwner())].enchanterCounter;
 			if(absolute)
 				counter = val;
 			else

+ 33 - 34
lib/battle/BattleInfo.cpp

@@ -26,37 +26,6 @@
 VCMI_LIB_NAMESPACE_BEGIN
 
 ///BattleInfo
-std::pair< std::vector<BattleHex>, int > BattleInfo::getPath(BattleHex start, BattleHex dest, const battle::Unit * stack)
-{
-	auto reachability = getReachability(stack);
-
-	if(reachability.predecessors[dest] == -1) //cannot reach destination
-	{
-		return std::make_pair(std::vector<BattleHex>(), 0);
-	}
-
-	//making the Path
-	std::vector<BattleHex> path;
-	BattleHex curElem = dest;
-	while(curElem != start)
-	{
-		path.push_back(curElem);
-		curElem = reachability.predecessors[curElem];
-	}
-
-	return std::make_pair(path, reachability.distances[dest]);
-}
-
-void BattleInfo::calculateCasualties(std::map<ui32,si32> * casualties) const
-{
-	for(const auto & st : stacks) //setting casualties
-	{
-		si32 killed = st->getKilled();
-		if(killed > 0)
-			casualties[st->unitSide()][st->creatureId()] += killed;
-	}
-}
-
 CStack * BattleInfo::generateNewStack(uint32_t id, const CStackInstance & base, ui8 side, const SlotID & slot, BattleHex position)
 {
 	PlayerColor owner = sides[side].color;
@@ -560,10 +529,24 @@ BattleInfo::BattleInfo():
 	tacticsSide(0),
 	tacticDistance(0)
 {
-	setBattle(this);
 	setNodeType(BATTLE);
 }
 
+BattleID BattleInfo::getBattleID() const
+{
+	return battleID;
+}
+
+const IBattleInfo * BattleInfo::getBattle() const
+{
+	return this;
+}
+
+std::optional<PlayerColor> BattleInfo::getPlayerID() const
+{
+	return std::nullopt;
+}
+
 BattleInfo::~BattleInfo()
 {
 	for (auto & elem : stacks)
@@ -689,14 +672,30 @@ int64_t BattleInfo::getActualDamage(const DamageRange & damage, int32_t attacker
 	}
 }
 
-void BattleInfo::nextRound(int32_t roundNr)
+int3 BattleInfo::getLocation() const
+{
+	return tile;
+}
+
+bool BattleInfo::isCreatureBank() const
+{
+	return creatureBank;
+}
+
+
+std::vector<SpellID> BattleInfo::getUsedSpells(ui8 side) const
+{
+	return sides.at(side).usedSpellsHistory;
+}
+
+void BattleInfo::nextRound()
 {
 	for(int i = 0; i < 2; ++i)
 	{
 		sides.at(i).castSpellsCount = 0;
 		vstd::amax(--sides.at(i).enchanterCounter, 0);
 	}
-	round = roundNr;
+	round += 1;
 
 	for(CStack * s : stacks)
 	{

+ 14 - 5
lib/battle/BattleInfo.h

@@ -26,6 +26,8 @@ class BattleField;
 class DLL_LINKAGE BattleInfo : public CBonusSystemNode, public CBattleInfoCallback, public IBattleState
 {
 public:
+	BattleID battleID = BattleID(0);
+
 	enum BattleSide
 	{
 		ATTACKER = 0,
@@ -49,6 +51,7 @@ public:
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
+		h & battleID;
 		h & sides;
 		h & round;
 		h & activeStack;
@@ -72,9 +75,14 @@ public:
 	BattleInfo();
 	virtual ~BattleInfo();
 
+	const IBattleInfo * getBattle() const override;
+	std::optional<PlayerColor> getPlayerID() const override;
+
 	//////////////////////////////////////////////////////////////////////////
 	// IBattleInfo
 
+	BattleID getBattleID() const override;
+
 	int32_t getActiveStackID() const override;
 
 	TStacks getStacksIf(TStackFilter predicate) const override;
@@ -106,10 +114,15 @@ public:
 
 	int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override;
 
+	int3 getLocation() const override;
+	bool isCreatureBank() const override;
+
+	std::vector<SpellID> getUsedSpells(ui8 side) const override;
+
 	//////////////////////////////////////////////////////////////////////////
 	// IBattleState
 
-	void nextRound(int32_t roundNr) override;
+	void nextRound() override;
 	void nextTurn(uint32_t unitId) override;
 
 	void addUnit(uint32_t id, const JsonNode & data) override;
@@ -137,10 +150,6 @@ public:
 	using CBattleInfoEssentials::battleGetFightingHero;
 	CGHeroInstance * battleGetFightingHero(ui8 side) const;
 
-	std::pair< std::vector<BattleHex>, int > getPath(BattleHex start, BattleHex dest, const battle::Unit * stack); //returned value: pair<path, length>; length may be different than number of elements in path since flying creatures jump between distant hexes
-
-	void calculateCasualties(std::map<ui32,si32> * casualties) const; //casualties are array of maps size 2 (attacker, defeneder), maps are (crid => amount)
-
 	CStack * generateNewStack(uint32_t id, const CStackInstance & base, ui8 side, const SlotID & slot, BattleHex position);
 	CStack * generateNewStack(uint32_t id, const CStackBasicDescriptor & base, ui8 side, const SlotID & slot, BattleHex position);
 

+ 10 - 3
lib/battle/BattleProxy.cpp

@@ -17,12 +17,19 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 BattleProxy::BattleProxy(Subject subject_): 
 	subject(std::move(subject_))
+{}
+
+BattleProxy::~BattleProxy() = default;
+
+const IBattleInfo * BattleProxy::getBattle() const
 {
-	setBattle(this);
-	player = subject->getPlayerID();
+	return this;
 }
 
-BattleProxy::~BattleProxy() = default;
+std::optional<PlayerColor> BattleProxy::getPlayerID() const
+{
+	return subject->getPlayerID();
+}
 
 int32_t BattleProxy::getActiveStackID() const
 {

+ 2 - 0
lib/battle/BattleProxy.h

@@ -24,6 +24,8 @@ public:
 
 	//////////////////////////////////////////////////////////////////////////
 	// IBattleInfo
+	const IBattleInfo * getBattle() const override;
+	std::optional<PlayerColor> getPlayerID() const override;
 
 	int32_t getActiveStackID() const override;
 

+ 21 - 0
lib/battle/CBattleInfoCallback.cpp

@@ -136,6 +136,27 @@ ESpellCastProblem CBattleInfoCallback::battleCanCastSpell(const spells::Caster *
 	return ESpellCastProblem::OK;
 }
 
+std::pair< std::vector<BattleHex>, int > CBattleInfoCallback::getPath(BattleHex start, BattleHex dest, const battle::Unit * stack) const
+{
+	auto reachability = getReachability(stack);
+
+	if(reachability.predecessors[dest] == -1) //cannot reach destination
+	{
+		return std::make_pair(std::vector<BattleHex>(), 0);
+	}
+
+	//making the Path
+	std::vector<BattleHex> path;
+	BattleHex curElem = dest;
+	while(curElem != start)
+	{
+		path.push_back(curElem);
+		curElem = reachability.predecessors[curElem];
+	}
+
+	return std::make_pair(path, reachability.distances[dest]);
+}
+
 bool CBattleInfoCallback::battleHasPenaltyOnLine(BattleHex from, BattleHex dest, bool checkWall, bool checkMoat) const
 {
 	auto isTileBlocked = [&](BattleHex tile)

+ 2 - 1
lib/battle/CBattleInfoCallback.h

@@ -11,7 +11,6 @@
 
 #include <vcmi/spells/Magic.h>
 
-#include "CCallbackBase.h"
 #include "ReachabilityInfo.h"
 #include "BattleAttackInfo.h"
 
@@ -89,6 +88,8 @@ public:
 	std::set<BattleHex> battleGetAttackedHexes(const battle::Unit * attacker, BattleHex destinationTile, BattleHex attackerPos = BattleHex::INVALID) const;
 	bool isEnemyUnitWithinSpecifiedRange(BattleHex attackerPosition, const battle::Unit * defenderUnit, unsigned int range) const;
 
+	std::pair< std::vector<BattleHex>, int > getPath(BattleHex start, BattleHex dest, const battle::Unit * stack) const;
+
 	bool battleCanAttack(const battle::Unit * stack, const battle::Unit * target, BattleHex dest) const; //determines if stack with given ID can attack target at the selected destination
 	bool battleCanShoot(const battle::Unit * attacker, BattleHex dest) const; //determines if stack with given ID shoot at the selected destination
 	bool battleCanShoot(const battle::Unit * attacker) const; //determines if stack with given ID shoot in principle

+ 10 - 5
lib/battle/CBattleInfoEssentials.cpp

@@ -17,6 +17,11 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
+bool CBattleInfoEssentials::duringBattle() const
+{
+	return getBattle() != nullptr;
+}
+
 TerrainId CBattleInfoEssentials::battleTerrainType() const
 {
 	RETURN_IF_NOT_BATTLE(TerrainId());
@@ -47,7 +52,7 @@ std::vector<std::shared_ptr<const CObstacleInstance>> CBattleInfoEssentials::bat
 	}
 	else
 	{
-		if(!!player && *perspective != battleGetMySide())
+		if(!!getPlayerID() && *perspective != battleGetMySide())
 			logGlobal->warn("Unauthorized obstacles access attempt, assuming massive spell");
 	}
 
@@ -157,14 +162,14 @@ const CGTownInstance * CBattleInfoEssentials::battleGetDefendedTown() const
 BattlePerspective::BattlePerspective CBattleInfoEssentials::battleGetMySide() const
 {
 	RETURN_IF_NOT_BATTLE(BattlePerspective::INVALID);
-	if(!player || player->isSpectator())
+	if(!getPlayerID() || getPlayerID()->isSpectator())
 		return BattlePerspective::ALL_KNOWING;
-	if(*player == getBattle()->getSidePlayer(BattleSide::ATTACKER))
+	if(*getPlayerID() == getBattle()->getSidePlayer(BattleSide::ATTACKER))
 		return BattlePerspective::LEFT_SIDE;
-	if(*player == getBattle()->getSidePlayer(BattleSide::DEFENDER))
+	if(*getPlayerID() == getBattle()->getSidePlayer(BattleSide::DEFENDER))
 		return BattlePerspective::RIGHT_SIDE;
 
-	logGlobal->error("Cannot find player %s in battle!", player->toString());
+	logGlobal->error("Cannot find player %s in battle!", getPlayerID()->toString());
 	return BattlePerspective::INVALID;
 }
 

+ 2 - 2
lib/battle/CBattleInfoEssentials.h

@@ -8,7 +8,6 @@
  *
  */
 #pragma once
-#include "CCallbackBase.h"
 #include "IBattleInfoCallback.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -34,7 +33,7 @@ namespace BattlePerspective
 	};
 }
 
-class DLL_LINKAGE CBattleInfoEssentials : public virtual CCallbackBase, public IBattleInfoCallback
+class DLL_LINKAGE CBattleInfoEssentials : public IBattleInfoCallback
 {
 protected:
 	bool battleDoWeKnowAbout(ui8 side) const;
@@ -45,6 +44,7 @@ public:
 		ONLY_MINE, ONLY_ENEMY, MINE_AND_ENEMY
 	};
 
+	bool duringBattle() const;
 	BattlePerspective::BattlePerspective battleGetMySide() const;
 	const IBonusBearer * getBonusBearer() const override;
 

+ 0 - 42
lib/battle/CCallbackBase.cpp

@@ -1,42 +0,0 @@
-/*
- * CCallbackBase.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 "CCallbackBase.h"
-#include "IBattleState.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-bool CCallbackBase::duringBattle() const
-{
-	return getBattle() != nullptr;
-}
-
-const IBattleInfo * CCallbackBase::getBattle() const
-{
-	return battle;
-}
-
-CCallbackBase::CCallbackBase(std::optional<PlayerColor> Player):
-	player(std::move(Player))
-{
-}
-
-void CCallbackBase::setBattle(const IBattleInfo * B)
-{
-	battle = B;
-}
-
-std::optional<PlayerColor> CCallbackBase::getPlayerID() const
-{
-	return player;
-}
-
-
-VCMI_LIB_NAMESPACE_END

+ 0 - 45
lib/battle/CCallbackBase.h

@@ -1,45 +0,0 @@
-/*
- * CCallbackBase.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 "../GameConstants.h"
-
-#define RETURN_IF_NOT_BATTLE(...) if(!duringBattle()) {logGlobal->error("%s called when no battle!", __FUNCTION__); return __VA_ARGS__; }
-#define ASSERT_IF_CALLED_WITH_PLAYER if(!player) {logGlobal->error(BOOST_CURRENT_FUNCTION); assert(0);}
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-class IBattleInfo;
-class BattleInfo;
-class CBattleInfoEssentials;
-
-
-//Basic class for various callbacks (interfaces called by players to get info about game and so forth)
-class DLL_LINKAGE CCallbackBase
-{
-	const IBattleInfo * battle = nullptr; //battle to which the player is engaged, nullptr if none or not applicable
-
-protected:
-	std::optional<PlayerColor> player; // not set gives access to all information, otherwise callback provides only information "visible" for player
-
-	CCallbackBase(std::optional<PlayerColor> Player);
-	CCallbackBase() = default;
-
-	const IBattleInfo * getBattle() const;
-	void setBattle(const IBattleInfo * B);
-	bool duringBattle() const;
-
-public:
-	std::optional<PlayerColor> getPlayerID() const;
-
-	friend class CBattleInfoEssentials;
-};
-
-
-VCMI_LIB_NAMESPACE_END

+ 29 - 4
lib/battle/CPlayerBattleCallback.cpp

@@ -11,14 +11,39 @@
 #include "CPlayerBattleCallback.h"
 #include "../CStack.h"
 #include "../gameState/InfoAboutArmy.h"
+#include "../CGameInfoCallback.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
+CPlayerBattleCallback::CPlayerBattleCallback(const IBattleInfo * battle, PlayerColor player):
+	battle(battle),
+	player(player)
+{
+
+}
+
+#if SCRIPTING_ENABLED
+scripting::Pool * CPlayerBattleCallback::getContextPool() const
+{
+	return nullptr; //TODO cl->getGlobalContextPool();
+}
+#endif
+
+const IBattleInfo * CPlayerBattleCallback::getBattle() const
+{
+	return battle;
+}
+
+std::optional<PlayerColor> CPlayerBattleCallback::getPlayerID() const
+{
+	return player;
+}
+
 bool CPlayerBattleCallback::battleCanFlee() const
 {
 	RETURN_IF_NOT_BATTLE(false);
 	ASSERT_IF_CALLED_WITH_PLAYER
-			return CBattleInfoEssentials::battleCanFlee(*player);
+			return CBattleInfoEssentials::battleCanFlee(*getPlayerID());
 }
 
 TStacks CPlayerBattleCallback::battleGetStacks(EStackOwnership whose, bool onlyAlive) const
@@ -30,8 +55,8 @@ TStacks CPlayerBattleCallback::battleGetStacks(EStackOwnership whose, bool onlyA
 
 	return battleGetStacksIf([=](const CStack * s){
 		const bool ownerMatches = (whose == MINE_AND_ENEMY)
-								|| (whose == ONLY_MINE && s->unitOwner() == player)
-								|| (whose == ONLY_ENEMY && s->unitOwner() != player);
+								|| (whose == ONLY_MINE && s->unitOwner() == getPlayerID())
+								|| (whose == ONLY_ENEMY && s->unitOwner() != getPlayerID());
 
 		return ownerMatches && s->isValidTarget(!onlyAlive);
 	});
@@ -41,7 +66,7 @@ int CPlayerBattleCallback::battleGetSurrenderCost() const
 {
 	RETURN_IF_NOT_BATTLE(-3)
 			ASSERT_IF_CALLED_WITH_PLAYER
-			return CBattleInfoCallback::battleGetSurrenderCost(*player);
+			return CBattleInfoCallback::battleGetSurrenderCost(*getPlayerID());
 }
 
 const CGHeroInstance * CPlayerBattleCallback::battleGetMyHero() const

+ 12 - 0
lib/battle/CPlayerBattleCallback.h

@@ -16,7 +16,19 @@ class CGHeroInstance;
 
 class DLL_LINKAGE CPlayerBattleCallback : public CBattleInfoCallback
 {
+	const IBattleInfo * battle;
+	PlayerColor player;
+
 public:
+	CPlayerBattleCallback(const IBattleInfo * battle, PlayerColor player);
+
+#if SCRIPTING_ENABLED
+	scripting::Pool * getContextPool() const override;
+#endif
+
+	const IBattleInfo * getBattle() const override;
+	std::optional<PlayerColor> getPlayerID() const override;
+
 	bool battleCanFlee() const; //returns true if caller can flee from the battle
 	TStacks battleGetStacks(EStackOwnership whose = MINE_AND_ENEMY, bool onlyAlive = true) const; //returns stacks on battlefield
 

+ 7 - 0
lib/battle/IBattleInfoCallback.h

@@ -15,10 +15,13 @@
 
 #include <vcmi/Entity.h>
 
+#define RETURN_IF_NOT_BATTLE(...) if(!duringBattle()) {logGlobal->error("%s called when no battle!", __FUNCTION__); return __VA_ARGS__; }
+
 VCMI_LIB_NAMESPACE_BEGIN
 
 struct CObstacleInstance;
 class BattleField;
+class IBattleInfo;
 
 namespace battle
 {
@@ -53,6 +56,10 @@ public:
 #if SCRIPTING_ENABLED
 	virtual scripting::Pool * getContextPool() const = 0;
 #endif
+	virtual ~IBattleInfoCallback() = default;
+
+	virtual const IBattleInfo * getBattle() const = 0;
+	virtual std::optional<PlayerColor> getPlayerID() const = 0;
 
 	virtual TerrainId battleTerrainType() const = 0;
 	virtual BattleField battleGetBattlefieldType() const = 0;

+ 9 - 3
lib/battle/IBattleState.h

@@ -19,6 +19,7 @@ struct Bonus;
 class JsonNode;
 class JsonSerializeFormat;
 class BattleField;
+class int3;
 
 namespace vstd
 {
@@ -37,6 +38,8 @@ public:
 
 	virtual ~IBattleInfo() = default;
 
+	virtual BattleID getBattleID() const = 0;
+
 	virtual int32_t getActiveStackID() const = 0;
 
 	virtual TStacks getStacksIf(TStackFilter predicate) const = 0;
@@ -55,6 +58,8 @@ public:
 	virtual PlayerColor getSidePlayer(ui8 side) const = 0;
 	virtual const CArmedInstance * getSideArmy(ui8 side) const = 0;
 	virtual const CGHeroInstance * getSideHero(ui8 side) const = 0;
+	/// Returns list of all spells used by specified side (and that can be learned by opposite hero)
+	virtual std::vector<SpellID> getUsedSpells(ui8 side) const = 0;
 
 	virtual uint32_t getCastSpells(ui8 side) const = 0;
 	virtual int32_t getEnchanterCounter(ui8 side) const = 0;
@@ -65,14 +70,15 @@ public:
 	virtual uint32_t nextUnitId() const = 0;
 
 	virtual int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const = 0;
+
+	virtual int3 getLocation() const = 0;
+	virtual bool isCreatureBank() const = 0;
 };
 
 class DLL_LINKAGE IBattleState : public IBattleInfo
 {
 public:
-	//TODO: add non-const API
-
-	virtual void nextRound(int32_t roundNr) = 0;
+	virtual void nextRound() = 0;
 	virtual void nextTurn(uint32_t unitId) = 0;
 
 	virtual void addUnit(uint32_t id, const JsonNode & data) = 0;

+ 1 - 0
lib/constants/EntityIdentifiers.cpp

@@ -38,6 +38,7 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
+const BattleID BattleID::NONE = BattleID(-1);
 const QueryID QueryID::NONE = QueryID(-1);
 const HeroTypeID HeroTypeID::NONE = HeroTypeID(-1);
 const ObjectInstanceID ObjectInstanceID::NONE = ObjectInstanceID(-1);

+ 6 - 0
lib/constants/EntityIdentifiers.h

@@ -180,6 +180,12 @@ public:
 	DLL_LINKAGE static const QueryID NONE;
 };
 
+class BattleID : public Identifier<BattleID>
+{
+public:
+	using Identifier<BattleID>::Identifier;
+	DLL_LINKAGE static const BattleID NONE;
+};
 class ObjectInstanceID : public Identifier<ObjectInstanceID>
 {
 public:

+ 33 - 4
lib/gameState/CGameState.cpp

@@ -395,7 +395,6 @@ CGameState::CGameState()
 
 CGameState::~CGameState()
 {
-	curB.dellNull();
 	map.dellNull();
 }
 
@@ -1218,11 +1217,41 @@ void CGameState::initVisitingAndGarrisonedHeroes()
 	}
 }
 
+const BattleInfo * CGameState::getBattle(const PlayerColor & player) const
+{
+	if (!player.isValidPlayer())
+		return nullptr;
+
+	for (const auto & battlePtr : currentBattles)
+		if (battlePtr->sides[0].color == player || battlePtr->sides[1].color == player)
+			return battlePtr.get();
+
+	return nullptr;
+}
+
+const BattleInfo * CGameState::getBattle(const BattleID & battle) const
+{
+	for (const auto & battlePtr : currentBattles)
+		if (battlePtr->battleID == battle)
+			return battlePtr.get();
+
+	return nullptr;
+}
+
+BattleInfo * CGameState::getBattle(const BattleID & battle)
+{
+	for (const auto & battlePtr : currentBattles)
+		if (battlePtr->battleID == battle)
+			return battlePtr.get();
+
+	return nullptr;
+}
+
 BattleField CGameState::battleGetBattlefieldType(int3 tile, CRandomGenerator & rand)
 {
-	if(!tile.valid() && curB)
-		tile = curB->tile;
-	else if(!tile.valid() && !curB)
+	assert(tile.valid());
+
+	if(!tile.valid())
 		return BattleField::NONE;
 
 	const TerrainTile &t = map->getTile(tile);

+ 12 - 1
lib/gameState/CGameState.h

@@ -83,6 +83,11 @@ class DLL_LINKAGE CGameState : public CNonConstInfoCallback
 	friend class CGameStateCampaign;
 
 public:
+	/// List of currently ongoing battles
+	std::vector<std::unique_ptr<BattleInfo>> currentBattles;
+	/// ID that can be allocated to next battle
+	BattleID nextBattleID = BattleID(0);
+
 	//we have here all heroes available on this map that are not hired
 	std::unique_ptr<TavernHeroesPool> heroesPool;
 
@@ -98,7 +103,6 @@ public:
 	void updateOnLoad(StartInfo * si);
 
 	ConstTransitivePtr<StartInfo> scenarioOps, initialOpts; //second one is a copy of settings received from pregame (not randomized)
-	ConstTransitivePtr<BattleInfo> curB; //current battle
 	ui32 day; //total number of days in game
 	ConstTransitivePtr<CMap> map;
 	std::map<PlayerColor, PlayerState> players;
@@ -124,6 +128,13 @@ public:
 	std::vector<CGObjectInstance*> guardingCreatures (int3 pos) const;
 	void updateRumor();
 
+	/// Returns battle in which selected player is engaged, or nullptr if none.
+	/// Can NOT be used with neutral player, use battle by ID instead
+	const BattleInfo * getBattle(const PlayerColor & player) const;
+	/// Returns battle by its unique identifier, or nullptr if not found
+	const BattleInfo * getBattle(const BattleID & battle) const;
+	BattleInfo * getBattle(const BattleID & battle);
+
 	// ----- victory, loss condition checks -----
 
 	EVictoryLossCheckResult checkForVictoryAndLoss(const PlayerColor & player) const;

+ 1 - 1
lib/pathfinder/CPathfinder.cpp

@@ -456,7 +456,7 @@ bool CPathfinderHelper::passOneTurnLimitCheck(const PathNodeInfo & source) const
 }
 
 CPathfinderHelper::CPathfinderHelper(CGameState * gs, const CGHeroInstance * Hero, const PathfinderOptions & Options):
-	CGameInfoCallback(gs, std::optional<PlayerColor>()),
+	CGameInfoCallback(gs),
 	turn(-1),
 	hero(Hero),
 	options(Options),

+ 1 - 0
lib/registerTypes/RegisterTypes.h

@@ -286,6 +286,7 @@ void registerTypesClientPacks2(Serializer &s)
 	s.template registerType<CPackForClient, BattleSetActiveStack>();
 	s.template registerType<CPackForClient, BattleResult>();
 	s.template registerType<CPackForClient, BattleResultAccepted>();
+	s.template registerType<CPackForClient, BattleCancelled>();
 	s.template registerType<CPackForClient, BattleLogMessage>();
 	s.template registerType<CPackForClient, BattleStackMoved>();
 	s.template registerType<CPackForClient, BattleAttack>();

+ 6 - 2
lib/spells/BattleSpellMechanics.cpp

@@ -252,6 +252,7 @@ void BattleSpellMechanics::cast(ServerCallback * server, const Target & target)
 
 	sc.side = casterSide;
 	sc.spellID = getSpellId();
+	sc.battleID = battle()->getBattle()->getBattleID();
 	sc.tile = target.at(0).hexValue;
 
 	sc.castByHero = mode == Mode::HERO;
@@ -299,6 +300,7 @@ void BattleSpellMechanics::cast(ServerCallback * server, const Target & target)
 	beforeCast(sc, *server->getRNG(), target);
 
 	BattleLogMessage castDescription;
+	castDescription.battleID = battle()->getBattle()->getBattleID();
 
 	switch (mode)
 	{
@@ -344,8 +346,9 @@ void BattleSpellMechanics::cast(ServerCallback * server, const Target & target)
 
 	// send empty event to client
 	// temporary(?) workaround to force animations to trigger
-	StacksInjured fake_event;
-	server->apply(&fake_event);
+	StacksInjured fakeEvent;
+	fakeEvent.battleID = battle()->getBattle()->getBattleID();
+	server->apply(&fakeEvent);
 }
 
 void BattleSpellMechanics::beforeCast(BattleSpellCast & sc, vstd::RNG & rng, const Target & target)
@@ -448,6 +451,7 @@ std::set<const battle::Unit *> BattleSpellMechanics::collectTargets() const
 void BattleSpellMechanics::doRemoveEffects(ServerCallback * server, const std::vector<const battle::Unit *> & targets, const CSelector & selector)
 {
 	SetStackEffect sse;
+	sse.battleID = battle()->getBattle()->getBattleID();
 
 	for(const auto * unit : targets)
 	{

+ 1 - 1
lib/spells/BattleSpellMechanics.h

@@ -73,7 +73,7 @@ private:
 
 	std::set<const battle::Unit *> collectTargets() const;
 
-	static void doRemoveEffects(ServerCallback * server, const std::vector<const battle::Unit *> & targets, const CSelector & selector);
+	void doRemoveEffects(ServerCallback * server, const std::vector<const battle::Unit *> & targets, const CSelector & selector);
 
 	std::set<BattleHex> spellRangeInHexes(BattleHex centralHex) const;
 

+ 2 - 0
lib/spells/effects/Catapult.cpp

@@ -137,6 +137,7 @@ void Catapult::applyTargeted(ServerCallback * server, const Mechanics * m, const
 		attack.damageDealt = getRandomDamage(server);
 
 		CatapultAttack ca; //package for clients
+		ca.battleID = m->battle()->getBattle()->getBattleID();
 		ca.attacker = m->caster->getHeroCaster() ? -1 : m->caster->getCasterUnitId();
 		ca.attackedParts.push_back(attack);
 		server->apply(&ca);
@@ -188,6 +189,7 @@ int Catapult::getRandomDamage (ServerCallback * server) const
 void Catapult::removeTowerShooters(ServerCallback * server, const Mechanics * m) const
 {
 	BattleUnitsChanged removeUnits;
+	removeUnits.battleID = m->battle()->getBattle()->getBattleID();
 
 	for (auto const wallPart : { EWallPart::KEEP, EWallPart::BOTTOM_TOWER, EWallPart::UPPER_TOWER })
 	{

+ 5 - 0
lib/spells/effects/Clone.cpp

@@ -14,6 +14,7 @@
 #include "../ISpellMechanics.h"
 #include "../../NetPacks.h"
 #include "../../battle/CBattleInfoCallback.h"
+#include "../../battle/IBattleState.h"
 #include "../../battle/CUnitState.h"
 #include "../../serializer/JsonSerializeFormat.h"
 
@@ -60,6 +61,7 @@ void Clone::apply(ServerCallback * server, const Mechanics * m, const EffectTarg
 		info.summoned = true;
 
 		BattleUnitsChanged pack;
+		pack.battleID = m->battle()->getBattle()->getBattleID();
 		pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD);
 		info.save(pack.changedStacks.back().data);
 		server->apply(&pack);
@@ -67,6 +69,7 @@ void Clone::apply(ServerCallback * server, const Mechanics * m, const EffectTarg
 		//TODO: use BattleUnitsChanged with UPDATE operation
 
 		BattleUnitsChanged cloneFlags;
+		cloneFlags.battleID = m->battle()->getBattle()->getBattleID();
 
 		const auto *cloneUnit = m->battle()->battleGetUnitByID(unitId);
 
@@ -89,6 +92,8 @@ void Clone::apply(ServerCallback * server, const Mechanics * m, const EffectTarg
 		server->apply(&cloneFlags);
 
 		SetStackEffect sse;
+		sse.battleID = m->battle()->getBattle()->getBattleID();
+
 		Bonus lifeTimeMarker(BonusDuration::N_TURNS, BonusType::NONE, BonusSource::SPELL_EFFECT, 0, SpellID::CLONE); //TODO: use special bonus type
 		lifeTimeMarker.turnsRemain = m->getEffectDuration();
 		std::vector<Bonus> buffer;

+ 4 - 0
lib/spells/effects/Damage.cpp

@@ -34,6 +34,9 @@ void Damage::apply(ServerCallback * server, const Mechanics * m, const EffectTar
 {
 	StacksInjured stacksInjured;
 	BattleLogMessage blm;
+	stacksInjured.battleID = m->battle()->getBattle()->getBattleID();
+	blm.battleID = m->battle()->getBattle()->getBattleID();
+
 	size_t targetIndex = 0;
 	const battle::Unit * firstTarget = nullptr;
 	const bool describe = server->describeChanges();
@@ -48,6 +51,7 @@ void Damage::apply(ServerCallback * server, const Mechanics * m, const EffectTar
 		if(unit && unit->alive())
 		{
 			BattleStackAttacked bsa;
+			bsa.battleID = m->battle()->getBattle()->getBattleID();
 			bsa.damageAmount = damageForTarget(targetIndex, m, unit);
 			bsa.stackAttacked = unit->unitId();
 			bsa.attackerID = -1;

+ 2 - 0
lib/spells/effects/DemonSummon.cpp

@@ -14,6 +14,7 @@
 #include "../ISpellMechanics.h"
 #include "../../NetPacks.h"
 #include "../../battle/CBattleInfoCallback.h"
+#include "../../battle/BattleInfo.h"
 #include "../../battle/CUnitState.h"
 #include "../../serializer/JsonSerializeFormat.h"
 
@@ -27,6 +28,7 @@ namespace effects
 void DemonSummon::apply(ServerCallback * server, const Mechanics * m, const EffectTarget & target) const
 {
 	BattleUnitsChanged pack;
+	pack.battleID = m->battle()->getBattle()->getBattleID();
 
 	for(const Destination & dest : target)
 	{

+ 3 - 0
lib/spells/effects/Dispel.cpp

@@ -18,6 +18,7 @@
 
 #include "../../NetPacks.h"
 #include "../../battle/IBattleState.h"
+#include "../../battle/CBattleInfoCallback.h"
 #include "../../battle/Unit.h"
 #include "../../serializer/JsonSerializeFormat.h"
 
@@ -33,6 +34,8 @@ void Dispel::apply(ServerCallback * server, const Mechanics * m, const EffectTar
 	const bool describe = server->describeChanges();
 	SetStackEffect sse;
 	BattleLogMessage blm;
+	blm.battleID = m->battle()->getBattle()->getBattleID();
+	sse.battleID = m->battle()->getBattle()->getBattleID();
 
 	for(const auto & t : target)
 	{

+ 4 - 0
lib/spells/effects/Heal.cpp

@@ -35,7 +35,11 @@ void Heal::apply(ServerCallback * server, const Mechanics * m, const EffectTarge
 void Heal::apply(int64_t value, ServerCallback * server, const Mechanics * m, const EffectTarget & target) const
 {
 	BattleLogMessage logMessage;
+	logMessage.battleID = m->battle()->getBattle()->getBattleID();
+
 	BattleUnitsChanged pack;
+	pack.battleID = m->battle()->getBattle()->getBattleID();
+
 	prepareHealEffect(value, pack, logMessage, *server->getRNG(), m, target);
 	if(!pack.changedStacks.empty())
 		server->apply(&pack);

+ 1 - 0
lib/spells/effects/Sacrifice.cpp

@@ -123,6 +123,7 @@ void Sacrifice::apply(ServerCallback * server, const Mechanics * m, const Effect
 	Heal::apply(calculateHealEffectValue(m, victim), server, m, healTarget);
 
 	BattleUnitsChanged removeUnits;
+	removeUnits.battleID = m->battle()->getBattle()->getBattleID();
 	removeUnits.changedStacks.emplace_back(victim->unitId(), UnitChanges::EOperation::REMOVE);
 	server->apply(&removeUnits);
 }

+ 2 - 0
lib/spells/effects/Summon.cpp

@@ -14,6 +14,7 @@
 
 #include "../ISpellMechanics.h"
 #include "../../battle/CBattleInfoCallback.h"
+#include "../../battle/BattleInfo.h"
 #include "../../battle/Unit.h"
 #include "../../NetPacks.h"
 #include "../../serializer/JsonSerializeFormat.h"
@@ -87,6 +88,7 @@ void Summon::apply(ServerCallback * server, const Mechanics * m, const EffectTar
 	auto valueWithBonus = m->applySpecificSpellBonus(m->calculateRawEffectValue(0, m->getEffectPower()));//TODO: consider use base power too
 
 	BattleUnitsChanged pack;
+	pack.battleID = m->battle()->getBattle()->getBattleID();
 
 	for(const auto & dest : target)
 	{

+ 3 - 0
lib/spells/effects/Timed.cpp

@@ -15,6 +15,7 @@
 
 #include "../../NetPacks.h"
 #include "../../battle/IBattleState.h"
+#include "../../battle/CBattleInfoCallback.h"
 #include "../../battle/Unit.h"
 #include "../../serializer/JsonSerializeFormat.h"
 
@@ -116,6 +117,8 @@ void Timed::apply(ServerCallback * server, const Mechanics * m, const EffectTarg
 
 	SetStackEffect sse;
 	BattleLogMessage blm;
+	blm.battleID = m->battle()->getBattle()->getBattleID();
+	sse.battleID = m->battle()->getBattle()->getBattleID();
 
 	for(const auto & t : target)
 	{

+ 1 - 1
scripting/lua/LuaScriptingContext.cpp

@@ -75,7 +75,7 @@ LuaContext::LuaContext(const Script * source, const Environment * env_):
 	S.push(env->game());
 	lua_setglobal(L, "GAME");
 
-	S.push(env->battle());
+	S.push(env->battle(BattleID::NONE));
 	lua_setglobal(L, "BATTLE");
 
 	S.push(env->eventBus());

+ 8 - 6
server/CGameHandler.cpp

@@ -40,6 +40,7 @@
 #include "../lib/VCMI_Lib.h"
 #include "../lib/int3.h"
 
+#include "../lib/battle/BattleInfo.h"
 #include "../lib/filesystem/FileInfo.h"
 #include "../lib/filesystem/Filesystem.h"
 #include "../lib/gameState/CGameState.h"
@@ -135,9 +136,9 @@ const Services * CGameHandler::services() const
 	return VLC;
 }
 
-const CGameHandler::BattleCb * CGameHandler::battle() const
+const CGameHandler::BattleCb * CGameHandler::battle(const BattleID & battleID) const
 {
-	return this;
+	return gs->getBattle(battleID);
 }
 
 const CGameHandler::GameCb * CGameHandler::game() const
@@ -1007,6 +1008,7 @@ void CGameHandler::run(bool resume)
 		clockLast += clockDuration;
 		turnTimerHandler.update(timePassed);
 		boost::this_thread::sleep_for(boost::chrono::milliseconds(100));
+
 	}
 }
 
@@ -4092,10 +4094,10 @@ scripting::Pool * CGameHandler::getGlobalContextPool() const
 	return serverScripts.get();
 }
 
-scripting::Pool * CGameHandler::getContextPool() const
-{
-	return serverScripts.get();
-}
+//scripting::Pool * CGameHandler::getContextPool() const
+//{
+//	return serverScripts.get();
+//}
 #endif
 
 void CGameHandler::createObject(const int3 & visitablePosition, Obj type, int32_t subtype)

+ 3 - 6
server/CGameHandler.h

@@ -12,7 +12,6 @@
 #include <vcmi/Environment.h>
 
 #include "../lib/IGameCallback.h"
-#include "../lib/battle/CBattleInfoCallback.h"
 #include "../lib/LoadProgress.h"
 #include "../lib/ScriptHandler.h"
 #include "TurnTimerHandler.h"
@@ -53,14 +52,12 @@ class TurnOrderProcessor;
 class QueriesProcessor;
 class CObjectVisitQuery;
 
-class CGameHandler : public IGameCallback, public CBattleInfoCallback, public Environment
+class CGameHandler : public IGameCallback, public Environment
 {
 	CVCMIServer * lobby;
 	std::shared_ptr<CApplier<CBaseForGHApply>> applier;
 
 public:
-	using CCallbackBase::setBattle;
-
 	std::unique_ptr<HeroPoolProcessor> heroPool;
 	std::unique_ptr<BattleProcessor> battles;
 	std::unique_ptr<QueriesProcessor> queries;
@@ -84,7 +81,7 @@ public:
 	TurnTimerHandler turnTimerHandler;
 
 	const Services * services() const override;
-	const BattleCb * battle() const override;
+	const BattleCb * battle(const BattleID & battleID) const override;
 	const GameCb * game() const override;
 	vstd::CLoggerBase * logger() const override;
 	events::EventBus * eventBus() const override;
@@ -273,7 +270,7 @@ public:
 
 #if SCRIPTING_ENABLED
 	scripting::Pool * getGlobalContextPool() const override;
-	scripting::Pool * getContextPool() const override;
+//	scripting::Pool * getContextPool() const override;
 #endif
 
 	friend class CVCMIServer;

+ 2 - 2
server/NetPacksServer.cpp

@@ -20,7 +20,7 @@
 #include "../lib/IGameCallback.h"
 #include "../lib/mapObjects/CGTownInstance.h"
 #include "../lib/gameState/CGameState.h"
-#include "../lib/battle/BattleInfo.h"
+#include "../lib/battle/IBattleState.h"
 #include "../lib/battle/BattleAction.h"
 #include "../lib/battle/Unit.h"
 #include "../lib/serializer/Connection.h"
@@ -276,7 +276,7 @@ void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack)
 {
 	gh.throwIfWrongPlayer(&pack);
 
-	result = gh.battles->makePlayerBattleAction(pack.player, pack.ba);
+	result = gh.battles->makePlayerBattleAction(pack.battleID, pack.player, pack.ba);
 }
 
 void ApplyGhNetPackVisitor::visitDigWithHero(DigWithHero & pack)

+ 46 - 27
server/TurnTimerHandler.cpp

@@ -91,8 +91,8 @@ void TurnTimerHandler::update(int waitTime)
 			if(gs->isPlayerMakingTurn(player))
 				onPlayerMakingTurn(player, waitTime);
 		
-		if(gs->curB)
-			onBattleLoop(waitTime);
+		for (auto & battle : gs->currentBattles)
+			onBattleLoop(battle->battleID, waitTime);
 	}
 }
 
@@ -140,11 +140,11 @@ void TurnTimerHandler::onPlayerMakingTurn(PlayerColor player, int waitTime)
 	}
 }
 
-bool TurnTimerHandler::isPvpBattle() const
+bool TurnTimerHandler::isPvpBattle(const BattleID & battleID) const
 {
 	const auto * gs = gameHandler.gameState();
-	auto attacker = gs->curB->getSidePlayer(BattleSide::ATTACKER);
-	auto defender = gs->curB->getSidePlayer(BattleSide::DEFENDER);
+	auto attacker = gs->getBattle(battleID)->getSidePlayer(BattleSide::ATTACKER);
+	auto defender = gs->getBattle(battleID)->getSidePlayer(BattleSide::DEFENDER);
 	if(attacker.isValidPlayer() && defender.isValidPlayer())
 	{
 		const auto * attackerState = gameHandler.getPlayerState(attacker);
@@ -155,18 +155,18 @@ bool TurnTimerHandler::isPvpBattle() const
 	return false;
 }
 
-void TurnTimerHandler::onBattleStart()
+void TurnTimerHandler::onBattleStart(const BattleID & battleID)
 {
 	std::lock_guard<std::recursive_mutex> guard(mx);
 	const auto * gs = gameHandler.gameState();
 	const auto * si = gameHandler.getStartInfo();
-	if(!si || !gs || !gs->curB)
+	if(!si || !gs)
 		return;
 
-	auto attacker = gs->curB->getSidePlayer(BattleSide::ATTACKER);
-	auto defender = gs->curB->getSidePlayer(BattleSide::DEFENDER);
+	auto attacker = gs->getBattle(battleID)->getSidePlayer(BattleSide::ATTACKER);
+	auto defender = gs->getBattle(battleID)->getSidePlayer(BattleSide::DEFENDER);
 	
-	bool pvpBattle = isPvpBattle();
+	bool pvpBattle = isPvpBattle(battleID);
 	
 	for(auto i : {attacker, defender})
 	{
@@ -183,18 +183,24 @@ void TurnTimerHandler::onBattleStart()
 	}
 }
 
-void TurnTimerHandler::onBattleEnd()
+void TurnTimerHandler::onBattleEnd(const BattleID & battleID)
 {
 	std::lock_guard<std::recursive_mutex> guard(mx);
 	const auto * gs = gameHandler.gameState();
 	const auto * si = gameHandler.getStartInfo();
-	if(!si || !gs || !gs->curB)
+	if(!si || !gs)
+	{
+		assert(0);
 		return;
+	}
 
-	auto attacker = gs->curB->getSidePlayer(BattleSide::ATTACKER);
-	auto defender = gs->curB->getSidePlayer(BattleSide::DEFENDER);
+	if (!si->turnTimerInfo.isBattleEnabled())
+		return;
 	
-	bool pvpBattle = isPvpBattle();
+	auto attacker = gs->getBattle(battleID)->getSidePlayer(BattleSide::ATTACKER);
+	auto defender = gs->getBattle(battleID)->getSidePlayer(BattleSide::DEFENDER);
+	
+	bool pvpBattle = isPvpBattle(battleID);
 	
 	for(auto i : {attacker, defender})
 	{
@@ -216,15 +222,21 @@ void TurnTimerHandler::onBattleEnd()
 	}
 }
 
-void TurnTimerHandler::onBattleNextStack(const CStack & stack)
+void TurnTimerHandler::onBattleNextStack(const BattleID & battleID, const CStack & stack)
 {
 	std::lock_guard<std::recursive_mutex> guard(mx);
 	const auto * gs = gameHandler.gameState();
 	const auto * si = gameHandler.getStartInfo();
-	if(!si || !gs || !gs->curB || !si->turnTimerInfo.isBattleEnabled())
+	if(!si || !gs || !gs->getBattle(battleID))
+	{
+		assert(0);
+		return;
+	}
+	
+	if (!si->turnTimerInfo.isBattleEnabled())
 		return;
 	
-	if(isPvpBattle())
+	if(isPvpBattle(battleID))
 	{
 		auto player = stack.getOwner();
 		
@@ -237,40 +249,47 @@ void TurnTimerHandler::onBattleNextStack(const CStack & stack)
 	}
 }
 
-void TurnTimerHandler::onBattleLoop(int waitTime)
+void TurnTimerHandler::onBattleLoop(const BattleID & battleID, int waitTime)
 {
 	std::lock_guard<std::recursive_mutex> guard(mx);
 	const auto * gs = gameHandler.gameState();
 	const auto * si = gameHandler.getStartInfo();
-	if(!si || !gs || !gs->curB || !si->turnTimerInfo.isBattleEnabled())
+	if(!si || !gs)
+	{
+		assert(0);
 		return;
+	}
 	
+	if (!si->turnTimerInfo.isBattleEnabled())
+		return;
+
 	ui8 side = 0;
 	const CStack * stack = nullptr;
-	bool isTactisPhase = gs->curB->battleTacticDist() > 0;
+	bool isTactisPhase = gs->getBattle(battleID)->battleTacticDist() > 0;
 	
 	if(isTactisPhase)
-		side = gs->curB->battleGetTacticsSide();
+		side = gs->getBattle(battleID)->battleGetTacticsSide();
 	else
 	{
-		stack = gs->curB->battleGetStackByID(gs->curB->getActiveStackID());
+		stack = gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->getActiveStackID());
 		if(!stack || !stack->getOwner().isValidPlayer())
 			return;
 		side = stack->unitSide();
 	}
 	
-	auto player = gs->curB->getSidePlayer(side);
+	auto player = gs->getBattle(battleID)->getSidePlayer(side);
 	if(!player.isValidPlayer())
 		return;
 	
 	const auto * state = gameHandler.getPlayerState(player);
+	assert(state && state->status != EPlayerStatus::INGAME);
 	if(!state || state->status != EPlayerStatus::INGAME || !state->human)
 		return;
 	
 	auto & timer = timers[player];
 	if(timer.isActive && timer.isBattle && !timerCountDown(timer.creatureTimer, si->turnTimerInfo.creatureTimer, player, waitTime))
 	{
-		if(isPvpBattle())
+		if(isPvpBattle(battleID))
 		{
 			if(timer.battleTimer > 0)
 			{
@@ -289,7 +308,7 @@ void TurnTimerHandler::onBattleLoop(int waitTime)
 					doNothing.actionType = EActionType::DEFEND;
 					doNothing.stackNumber = stack->unitId();
 				}
-				gameHandler.battles->makePlayerBattleAction(player, doNothing);
+				gameHandler.battles->makePlayerBattleAction(battleID, player, doNothing);
 			}
 		}
 		else
@@ -311,7 +330,7 @@ void TurnTimerHandler::onBattleLoop(int waitTime)
 				BattleAction retreat;
 				retreat.side = side;
 				retreat.actionType = EActionType::RETREAT; //harsh punishment
-				gameHandler.battles->makePlayerBattleAction(player, retreat);
+				gameHandler.battles->makePlayerBattleAction(battleID, player, retreat);
 			}
 		}
 	}

+ 6 - 5
server/TurnTimerHandler.h

@@ -16,6 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 class CStack;
 class PlayerColor;
+class BattleID;
 
 VCMI_LIB_NAMESPACE_END
 
@@ -33,10 +34,10 @@ class TurnTimerHandler
 	std::recursive_mutex mx;
 	
 	void onPlayerMakingTurn(PlayerColor player, int waitTime);
-	void onBattleLoop(int waitTime);
+	void onBattleLoop(const BattleID & battleID, int waitTime);
 	
 	bool timerCountDown(int & timerToApply, int initialTimer, PlayerColor player, int waitTime);
-	bool isPvpBattle() const;
+	bool isPvpBattle(const BattleID & battleID) const;
 	void sendTimerUpdate(PlayerColor player);
 	
 public:
@@ -44,9 +45,9 @@ public:
 	
 	void onGameplayStart(PlayerColor player);
 	void onPlayerGetTurn(PlayerColor player);
-	void onBattleStart();
-	void onBattleNextStack(const CStack & stack);
-	void onBattleEnd();
+	void onBattleStart(const BattleID & battle);
+	void onBattleNextStack(const BattleID & battle, const CStack & stack);
+	void onBattleEnd(const BattleID & battleID);
 	void update(int waitTime);
 	void setTimerEnabled(PlayerColor player, bool enabled);
 	void setEndTurnAllowed(PlayerColor player, bool enabled);

+ 161 - 144
server/battles/BattleActionProcessor.cpp

@@ -17,7 +17,8 @@
 #include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/CStack.h"
 #include "../../lib/GameSettings.h"
-#include "../../lib/battle/BattleInfo.h"
+#include "../../lib/battle/CBattleInfoCallback.h"
+#include "../../lib/battle/IBattleState.h"
 #include "../../lib/battle/BattleAction.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/NetPacks.h"
@@ -37,42 +38,42 @@ void BattleActionProcessor::setGameHandler(CGameHandler * newGameHandler)
 	gameHandler = newGameHandler;
 }
 
-bool BattleActionProcessor::doEmptyAction(const BattleAction & ba)
+bool BattleActionProcessor::doEmptyAction(const CBattleInfoCallback & battle, const BattleAction & ba)
 {
 	return true;
 }
 
-bool BattleActionProcessor::doEndTacticsAction(const BattleAction & ba)
+bool BattleActionProcessor::doEndTacticsAction(const CBattleInfoCallback & battle, const BattleAction & ba)
 {
 	return true;
 }
 
-bool BattleActionProcessor::doWaitAction(const BattleAction & ba)
+bool BattleActionProcessor::doWaitAction(const CBattleInfoCallback & battle, const BattleAction & ba)
 {
-	const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber);
+	const CStack * stack = battle.battleGetStackByID(ba.stackNumber);
 
-	if (!canStackAct(stack))
+	if (!canStackAct(battle, stack))
 		return false;
 
 	return true;
 }
 
-bool BattleActionProcessor::doRetreatAction(const BattleAction & ba)
+bool BattleActionProcessor::doRetreatAction(const CBattleInfoCallback & battle, const BattleAction & ba)
 {
-	if (!gameHandler->gameState()->curB->battleCanFlee(gameHandler->gameState()->curB->sides.at(ba.side).color))
+	if (!battle.battleCanFlee(battle.sideToPlayer(ba.side)))
 	{
 		gameHandler->complain("Cannot retreat!");
 		return false;
 	}
 
-	owner->setBattleResult(EBattleResult::ESCAPE, !ba.side);
+	owner->setBattleResult(battle, EBattleResult::ESCAPE, !ba.side);
 	return true;
 }
 
-bool BattleActionProcessor::doSurrenderAction(const BattleAction & ba)
+bool BattleActionProcessor::doSurrenderAction(const CBattleInfoCallback & battle, const BattleAction & ba)
 {
-	PlayerColor player = gameHandler->gameState()->curB->sides.at(ba.side).color;
-	int cost = gameHandler->gameState()->curB->battleGetSurrenderCost(player);
+	PlayerColor player = battle.sideToPlayer(ba.side);
+	int cost = battle.battleGetSurrenderCost(player);
 	if (cost < 0)
 	{
 		gameHandler->complain("Cannot surrender!");
@@ -86,13 +87,13 @@ bool BattleActionProcessor::doSurrenderAction(const BattleAction & ba)
 	}
 
 	gameHandler->giveResource(player, EGameResID::GOLD, -cost);
-	owner->setBattleResult(EBattleResult::SURRENDER, !ba.side);
+	owner->setBattleResult(battle, EBattleResult::SURRENDER, !ba.side);
 	return true;
 }
 
-bool BattleActionProcessor::doHeroSpellAction(const BattleAction & ba)
+bool BattleActionProcessor::doHeroSpellAction(const CBattleInfoCallback & battle, const BattleAction & ba)
 {
-	const CGHeroInstance *h = gameHandler->gameState()->curB->battleGetFightingHero(ba.side);
+	const CGHeroInstance *h = battle.battleGetFightingHero(ba.side);
 	if (!h)
 	{
 		logGlobal->error("Wrong caster!");
@@ -106,7 +107,7 @@ bool BattleActionProcessor::doHeroSpellAction(const BattleAction & ba)
 		return false;
 	}
 
-	spells::BattleCast parameters(gameHandler->gameState()->curB, h, spells::Mode::HERO, s);
+	spells::BattleCast parameters(&battle, h, spells::Mode::HERO, s);
 
 	spells::detail::ProblemImpl problem;
 
@@ -122,17 +123,17 @@ bool BattleActionProcessor::doHeroSpellAction(const BattleAction & ba)
 		return false;
 	}
 
-	parameters.cast(gameHandler->spellEnv, ba.getTarget(gameHandler->gameState()->curB));
+	parameters.cast(gameHandler->spellEnv, ba.getTarget(&battle));
 
 	return true;
 }
 
-bool BattleActionProcessor::doWalkAction(const BattleAction & ba)
+bool BattleActionProcessor::doWalkAction(const CBattleInfoCallback & battle, const BattleAction & ba)
 {
-	const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber);
-	battle::Target target = ba.getTarget(gameHandler->gameState()->curB);
+	const CStack * stack = battle.battleGetStackByID(ba.stackNumber);
+	battle::Target target = ba.getTarget(&battle);
 
-	if (!canStackAct(stack))
+	if (!canStackAct(battle, stack))
 		return false;
 
 	if(target.size() < 1)
@@ -141,7 +142,7 @@ bool BattleActionProcessor::doWalkAction(const BattleAction & ba)
 		return false;
 	}
 
-	int walkedTiles = moveStack(ba.stackNumber, target.at(0).hexValue); //move
+	int walkedTiles = moveStack(battle, ba.stackNumber, target.at(0).hexValue); //move
 	if (!walkedTiles)
 	{
 		gameHandler->complain("Stack failed movement!");
@@ -150,15 +151,17 @@ bool BattleActionProcessor::doWalkAction(const BattleAction & ba)
 	return true;
 }
 
-bool BattleActionProcessor::doDefendAction(const BattleAction & ba)
+bool BattleActionProcessor::doDefendAction(const CBattleInfoCallback & battle, const BattleAction & ba)
 {
-	const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber);
+	const CStack * stack = battle.battleGetStackByID(ba.stackNumber);
 
-	if (!canStackAct(stack))
+	if (!canStackAct(battle, stack))
 		return false;
 
 	//defensive stance, TODO: filter out spell boosts from bonus (stone skin etc.)
 	SetStackEffect sse;
+	sse.battleID = battle.getBattle()->getBattleID();
+
 	Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, -1, static_cast<int32_t>(PrimarySkill::DEFENSE), BonusValueType::PERCENT_TO_ALL);
 	Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), -1, static_cast<int32_t>(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE);
 	Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, -1, static_cast<int32_t>(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE);
@@ -187,6 +190,7 @@ bool BattleActionProcessor::doDefendAction(const BattleAction & ba)
 	gameHandler->sendAndApply(&sse);
 
 	BattleLogMessage message;
+	message.battleID = battle.getBattle()->getBattleID();
 
 	MetaString text;
 	stack->addText(text, EMetaText::GENERAL_TXT, 120);
@@ -199,12 +203,12 @@ bool BattleActionProcessor::doDefendAction(const BattleAction & ba)
 	return true;
 }
 
-bool BattleActionProcessor::doAttackAction(const BattleAction & ba)
+bool BattleActionProcessor::doAttackAction(const CBattleInfoCallback & battle, const BattleAction & ba)
 {
-	const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber);
-	battle::Target target = ba.getTarget(gameHandler->gameState()->curB);
+	const CStack * stack = battle.battleGetStackByID(ba.stackNumber);
+	battle::Target target = ba.getTarget(&battle);
 
-	if (!canStackAct(stack))
+	if (!canStackAct(battle, stack))
 		return false;
 
 	if(target.size() < 2)
@@ -215,7 +219,7 @@ bool BattleActionProcessor::doAttackAction(const BattleAction & ba)
 
 	BattleHex attackPos = target.at(0).hexValue;
 	BattleHex destinationTile = target.at(1).hexValue;
-	const CStack * destinationStack = gameHandler->gameState()->curB->battleGetStackByPos(destinationTile, true);
+	const CStack * destinationStack = battle.battleGetStackByPos(destinationTile, true);
 
 	if(!destinationStack)
 	{
@@ -224,7 +228,7 @@ bool BattleActionProcessor::doAttackAction(const BattleAction & ba)
 	}
 
 	BattleHex startingPos = stack->getPosition();
-	int distance = moveStack(ba.stackNumber, attackPos);
+	int distance = moveStack(battle, ba.stackNumber, attackPos);
 
 	logGlobal->trace("%s will attack %s", stack->nodeName(), destinationStack->nodeName());
 
@@ -256,7 +260,7 @@ bool BattleActionProcessor::doAttackAction(const BattleAction & ba)
 	int totalAttacks = stack->totalAttacks.getMeleeValue();
 
 	//TODO: move to CUnitState
-	const auto * attackingHero = gameHandler->gameState()->curB->battleGetFightingHero(ba.side);
+	const auto * attackingHero = battle.battleGetFightingHero(ba.side);
 	if(attackingHero)
 	{
 		totalAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex());
@@ -269,13 +273,13 @@ bool BattleActionProcessor::doAttackAction(const BattleAction & ba)
 		//first strike
 		if(i == 0 && firstStrike && retaliation)
 		{
-			makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true);
+			makeAttack(battle, 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
+			makeAttack(battle, stack, destinationStack, (i ? 0 : distance), destinationTile, i==0, false, false);//no distance travelled on second attack
 		}
 
 		//counterattack
@@ -285,7 +289,7 @@ bool BattleActionProcessor::doAttackAction(const BattleAction & ba)
 			&& (i == 0 && !firstStrike)
 			&& retaliation && destinationStack->ableToRetaliate())
 		{
-			makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true);
+			makeAttack(battle, destinationStack, stack, 0, stack->getPosition(), true, false, true);
 		}
 	}
 
@@ -296,18 +300,18 @@ bool BattleActionProcessor::doAttackAction(const BattleAction & ba)
 		&& startingPos == target.at(2).hexValue
 		&& stack->alive())
 	{
-		moveStack(ba.stackNumber, startingPos);
+		moveStack(battle, ba.stackNumber, startingPos);
 		//NOTE: curStack->unitId() == ba.stackNumber (rev 1431)
 	}
 	return true;
 }
 
-bool BattleActionProcessor::doShootAction(const BattleAction & ba)
+bool BattleActionProcessor::doShootAction(const CBattleInfoCallback & battle, const BattleAction & ba)
 {
-	const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber);
-	battle::Target target = ba.getTarget(gameHandler->gameState()->curB);
+	const CStack * stack = battle.battleGetStackByID(ba.stackNumber);
+	battle::Target target = ba.getTarget(&battle);
 
-	if (!canStackAct(stack))
+	if (!canStackAct(battle, stack))
 		return false;
 
 	if(target.size() < 1)
@@ -318,9 +322,9 @@ bool BattleActionProcessor::doShootAction(const BattleAction & ba)
 
 	auto destination = target.at(0).hexValue;
 
-	const CStack * destinationStack = gameHandler->gameState()->curB->battleGetStackByPos(destination);
+	const CStack * destinationStack = battle.battleGetStackByPos(destination);
 
-	if (!gameHandler->gameState()->curB->battleCanShoot(stack, destination))
+	if (!battle.battleCanShoot(stack, destination))
 	{
 		gameHandler->complain("Cannot shoot!");
 		return false;
@@ -332,23 +336,23 @@ bool BattleActionProcessor::doShootAction(const BattleAction & ba)
 		return false;
 	}
 
-	makeAttack(stack, destinationStack, 0, destination, true, true, false);
+	makeAttack(battle, 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())
+		&& battle.battleCanShoot(destinationStack, stack->getPosition())
 		&& stack->alive()) //attacker may have died (fire shield)
 	{
-		makeAttack(destinationStack, stack, 0, stack->getPosition(), true, true, true);
+		makeAttack(battle, 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);
+	const auto * attackingHero = battle.battleGetFightingHero(ba.side);
 	if(attackingHero)
 	{
 		totalRangedAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex());
@@ -362,19 +366,19 @@ bool BattleActionProcessor::doShootAction(const BattleAction & ba)
 			&& stack->shots.canUse()
 			)
 		{
-			makeAttack(stack, destinationStack, 0, destination, false, true, false);
+			makeAttack(battle, stack, destinationStack, 0, destination, false, true, false);
 		}
 	}
 
 	return true;
 }
 
-bool BattleActionProcessor::doCatapultAction(const BattleAction & ba)
+bool BattleActionProcessor::doCatapultAction(const CBattleInfoCallback & battle, const BattleAction & ba)
 {
-	const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber);
-	battle::Target target = ba.getTarget(gameHandler->gameState()->curB);
+	const CStack * stack = battle.battleGetStackByID(ba.stackNumber);
+	battle::Target target = ba.getTarget(&battle);
 
-	if (!canStackAct(stack))
+	if (!canStackAct(battle, stack))
 		return false;
 
 	std::shared_ptr<const Bonus> catapultAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::CATAPULT));
@@ -385,7 +389,7 @@ bool BattleActionProcessor::doCatapultAction(const BattleAction & ba)
 	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
+		spells::BattleCast parameters(&battle, 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);
@@ -393,28 +397,28 @@ bool BattleActionProcessor::doCatapultAction(const BattleAction & ba)
 	return true;
 }
 
-bool BattleActionProcessor::doUnitSpellAction(const BattleAction & ba)
+bool BattleActionProcessor::doUnitSpellAction(const CBattleInfoCallback & battle, const BattleAction & ba)
 {
-	const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber);
-	battle::Target target = ba.getTarget(gameHandler->gameState()->curB);
+	const CStack * stack = battle.battleGetStackByID(ba.stackNumber);
+	battle::Target target = ba.getTarget(&battle);
 	SpellID spellID = ba.spell;
 
-	if (!canStackAct(stack))
+	if (!canStackAct(battle, 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) == SpellID::NONE)
-		spellID = gameHandler->battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_GENIE);
+	if (randSpellcaster && battle.battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_AIMED) == SpellID::NONE)
+		spellID = battle.battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_GENIE);
 
 	if (spellID == SpellID::NONE)
 		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);
+		spells::BattleCast parameters(&battle, stack, spells::Mode::CREATURE_ACTIVE, spell);
 		int32_t spellLvl = 0;
 		if(spellcaster)
 			vstd::amax(spellLvl, spellcaster->val);
@@ -426,12 +430,12 @@ bool BattleActionProcessor::doUnitSpellAction(const BattleAction & ba)
 	return true;
 }
 
-bool BattleActionProcessor::doHealAction(const BattleAction & ba)
+bool BattleActionProcessor::doHealAction(const CBattleInfoCallback & battle, const BattleAction & ba)
 {
-	const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber);
-	battle::Target target = ba.getTarget(gameHandler->gameState()->curB);
+	const CStack * stack = battle.battleGetStackByID(ba.stackNumber);
+	battle::Target target = ba.getTarget(&battle);
 
-	if (!canStackAct(stack))
+	if (!canStackAct(battle, stack))
 		return false;
 
 	if(target.size() < 1)
@@ -446,7 +450,7 @@ bool BattleActionProcessor::doHealAction(const BattleAction & ba)
 	if(target.at(0).unitValue)
 		destStack = target.at(0).unitValue;
 	else
-		destStack = gameHandler->gameState()->curB->battleGetUnitByPos(target.at(0).hexValue);
+		destStack = battle.battleGetUnitByPos(target.at(0).hexValue);
 
 	if(stack == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype < 0)
 	{
@@ -455,7 +459,7 @@ bool BattleActionProcessor::doHealAction(const BattleAction & ba)
 	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
+		spells::BattleCast parameters(&battle, 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});
@@ -463,7 +467,7 @@ bool BattleActionProcessor::doHealAction(const BattleAction & ba)
 	return true;
 }
 
-bool BattleActionProcessor::canStackAct(const CStack * stack)
+bool BattleActionProcessor::canStackAct(const CBattleInfoCallback & battle, const CStack * stack)
 {
 	if (!stack)
 	{
@@ -476,9 +480,9 @@ bool BattleActionProcessor::canStackAct(const CStack * stack)
 		return false;
 	}
 
-	if (gameHandler->battleTacticDist())
+	if (battle.battleTacticDist())
 	{
-		if (stack && stack->unitSide() != gameHandler->battleGetTacticsSide())
+		if (stack && stack->unitSide() != battle.battleGetTacticsSide())
 		{
 			gameHandler->complain("This is not a stack of side that has tactics!");
 			return false;
@@ -486,7 +490,7 @@ bool BattleActionProcessor::canStackAct(const CStack * stack)
 	}
 	else
 	{
-		if (stack->unitId() != gameHandler->gameState()->curB->getActiveStackID())
+		if (stack != battle.battleActiveUnit())
 		{
 			gameHandler->complain("Action has to be about active stack!");
 			return false;
@@ -495,81 +499,83 @@ bool BattleActionProcessor::canStackAct(const CStack * stack)
 	return true;
 }
 
-bool BattleActionProcessor::dispatchBattleAction(const BattleAction & ba)
+bool BattleActionProcessor::dispatchBattleAction(const CBattleInfoCallback & battle, const BattleAction & ba)
 {
 	switch(ba.actionType)
 	{
 		case EActionType::BAD_MORALE:
 		case EActionType::NO_ACTION:
-			return doEmptyAction(ba);
+			return doEmptyAction(battle, ba);
 		case EActionType::END_TACTIC_PHASE:
-			return doEndTacticsAction(ba);
+			return doEndTacticsAction(battle, ba);
 		case EActionType::RETREAT:
-			return doRetreatAction(ba);
+			return doRetreatAction(battle, ba);
 		case EActionType::SURRENDER:
-			return doSurrenderAction(ba);
+			return doSurrenderAction(battle, ba);
 		case EActionType::HERO_SPELL:
-			return doHeroSpellAction(ba);
+			return doHeroSpellAction(battle, ba);
 		case EActionType::WALK:
-			return doWalkAction(ba);
+			return doWalkAction(battle, ba);
 		case EActionType::WAIT:
-			return doWaitAction(ba);
+			return doWaitAction(battle, ba);
 		case EActionType::DEFEND:
-			return doDefendAction(ba);
+			return doDefendAction(battle, ba);
 		case EActionType::WALK_AND_ATTACK:
-			return doAttackAction(ba);
+			return doAttackAction(battle, ba);
 		case EActionType::SHOOT:
-			return doShootAction(ba);
+			return doShootAction(battle, ba);
 		case EActionType::CATAPULT:
-			return doCatapultAction(ba);
+			return doCatapultAction(battle, ba);
 		case EActionType::MONSTER_SPELL:
-			return doUnitSpellAction(ba);
+			return doUnitSpellAction(battle, ba);
 		case EActionType::STACK_HEAL:
-			return doHealAction(ba);
+			return doHealAction(battle, ba);
 	}
 	gameHandler->complain("Unrecognized action type received!!");
 	return false;
 }
 
-bool BattleActionProcessor::makeBattleActionImpl(const BattleAction &ba)
+bool BattleActionProcessor::makeBattleActionImpl(const CBattleInfoCallback & battle, const BattleAction &ba)
 {
 	logGlobal->trace("Making action: %s", ba.toString());
-	const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber);
+	const CStack * stack = battle.battleGetStackByID(ba.stackNumber);
 
 	// for these events client does not expects StartAction/EndAction wrapper
 	if (!ba.isBattleEndAction())
 	{
 		StartAction startAction(ba);
+		startAction.battleID = battle.getBattle()->getBattleID();
 		gameHandler->sendAndApply(&startAction);
 	}
 
-	bool result = dispatchBattleAction(ba);
+	bool result = dispatchBattleAction(battle, ba);
 
 	if (!ba.isBattleEndAction())
 	{
 		EndAction endAction;
+		endAction.battleID = battle.getBattle()->getBattleID();
 		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);
+		battle.handleObstacleTriggersForUnit(*gameHandler->spellEnv, *stack);
 
 	return result;
 }
 
-int BattleActionProcessor::moveStack(int stack, BattleHex dest)
+int BattleActionProcessor::moveStack(const CBattleInfoCallback & battle, int stack, BattleHex dest)
 {
 	int ret = 0;
 
-	const CStack *curStack = gameHandler->battleGetStackByID(stack);
-	const CStack *stackAtEnd = gameHandler->gameState()->curB->battleGetStackByPos(dest);
+	const CStack *curStack = battle.battleGetStackByID(stack);
+	const CStack *stackAtEnd = battle.battleGetStackByPos(dest);
 
 	assert(curStack);
 	assert(dest < GameConstants::BFIELD_SIZE);
 
-	if (gameHandler->gameState()->curB->tacticDistance)
+	if (battle.battleGetTacticDist())
 	{
-		assert(gameHandler->gameState()->curB->isInTacticRange(dest));
+		assert(battle.isInTacticRange(dest));
 	}
 
 	auto start = curStack->getPosition();
@@ -577,7 +583,7 @@ int BattleActionProcessor::moveStack(int stack, BattleHex dest)
 		return 0;
 
 	//initing necessary tables
-	auto accessibility = gameHandler->getAccesibility(curStack);
+	auto accessibility = battle.getAccesibility(curStack);
 	std::set<BattleHex> passed;
 	//Ignore obstacles on starting position
 	passed.insert(curStack->getPosition());
@@ -600,24 +606,24 @@ int BattleActionProcessor::moveStack(int stack, BattleHex dest)
 	}
 
 	bool canUseGate = false;
-	auto dbState = gameHandler->gameState()->curB->si.gateState;
-	if(gameHandler->battleGetSiegeLevel() > 0 && curStack->unitSide() == BattleSide::DEFENDER &&
+	auto dbState = battle.battleGetGateState();
+	if(battle.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);
+	std::pair< std::vector<BattleHex>, int > path = battle.getPath(start, dest, curStack);
 
 	ret = path.second;
 
 	int creSpeed = curStack->speed(0, true);
 
-	if (gameHandler->gameState()->curB->tacticDistance > 0 && creSpeed > 0)
+	if (battle.battleGetTacticDist() > 0 && creSpeed > 0)
 		creSpeed = GameConstants::BFIELD_SIZE;
 
-	bool hasWideMoat = vstd::contains_if(gameHandler->battleGetAllObstaclesOnPos(BattleHex(BattleHex::GATE_BRIDGE), false), [](const std::shared_ptr<const CObstacleInstance> & obst)
+	bool hasWideMoat = vstd::contains_if(battle.battleGetAllObstaclesOnPos(BattleHex(BattleHex::GATE_BRIDGE), false), [](const std::shared_ptr<const CObstacleInstance> & obst)
 	{
 		return obst->obstacleType == CObstacleInstance::MOAT;
 	});
@@ -657,12 +663,14 @@ int BattleActionProcessor::moveStack(int stack, BattleHex dest)
 				occupyGateDrawbridgeHex(dest))
 			{
 				BattleUpdateGateState db;
+				db.battleID = battle.getBattle()->getBattleID();
 				db.state = EGateState::OPENED;
 				gameHandler->sendAndApply(&db);
 			}
 
 			//inform clients about move
 			BattleStackMoved sm;
+			sm.battleID = battle.getBattle()->getBattleID();
 			sm.stack = curStack->unitId();
 			std::vector<BattleHex> tiles;
 			tiles.push_back(path.first[0]);
@@ -772,14 +780,14 @@ int BattleActionProcessor::moveStack(int stack, BattleHex dest)
 					}
 
 					//if we walked onto something, finalize this portion of stack movement check into obstacle
-					if(!gameHandler->battleGetAllObstaclesOnPos(hex, false).empty())
+					if(!battle.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);
+						auto obstacle2 = battle.battleGetAllObstaclesOnPos(otherHex, false);
 						if(otherHex.isValid() && !obstacle2.empty())
 							obstacleHit = true;
 					}
@@ -792,6 +800,7 @@ int BattleActionProcessor::moveStack(int stack, BattleHex dest)
 			{
 				//commit movement
 				BattleStackMoved sm;
+				sm.battleID = battle.getBattle()->getBattleID();
 				sm.stack = curStack->unitId();
 				sm.distance = path.second;
 				sm.teleporting = false;
@@ -805,7 +814,7 @@ int BattleActionProcessor::moveStack(int stack, BattleHex dest)
 			{
 				if(stackIsMoving && start != curStack->getPosition())
 				{
-					stackIsMoving = gameHandler->handleObstacleTriggersForUnit(*gameHandler->spellEnv, *curStack, passed);
+					stackIsMoving = battle.handleObstacleTriggersForUnit(*gameHandler->spellEnv, *curStack, passed);
 					passed.insert(curStack->getPosition());
 					if(curStack->doubleWide())
 						passed.insert(curStack->occupiedHex());
@@ -819,6 +828,7 @@ int BattleActionProcessor::moveStack(int stack, BattleHex dest)
 						if (curStack->alive())
 						{
 							BattleUpdateGateState db;
+							db.battleID = battle.getBattle()->getBattleID();
 							db.state = EGateState::OPENED;
 							gameHandler->sendAndApply(&db);
 						}
@@ -826,7 +836,7 @@ int BattleActionProcessor::moveStack(int stack, BattleHex dest)
 					else if (curStack->getPosition() == gateMayCloseAtHex)
 					{
 						gateMayCloseAtHex = BattleHex();
-						owner->updateGateState();
+						owner->updateGateState(battle);
 					}
 				}
 			}
@@ -843,19 +853,22 @@ int BattleActionProcessor::moveStack(int stack, BattleHex dest)
 			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);
+	battle.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)
+void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter)
 {
 	if(first && !counter)
-		handleAttackBeforeCasting(ranged, attacker, defender);
+		handleAttackBeforeCasting(battle, ranged, attacker, defender);
 
 	FireShieldInfo fireShield;
 	BattleAttack bat;
 	BattleLogMessage blm;
+	blm.battleID = battle.getBattle()->getBattleID();
+	bat.battleID = battle.getBattle()->getBattleID();
+	bat.attackerChanges.battleID = battle.getBattle()->getBattleID();
 	bat.stackAttacking = attacker->unitId();
 	bat.tile = targetHex;
 
@@ -891,7 +904,7 @@ void BattleActionProcessor::makeAttack(const CStack * attacker, const CStack * d
 		bat.flags |= BattleAttack::DEATH_BLOW;
 	}
 
-	const auto * owner = gameHandler->gameState()->curB->getHero(attacker->unitOwner());
+	const auto * owner = battle.battleGetFightingHero(attacker->unitSide());
 	if(owner)
 	{
 		int chance = owner->valOfBonuses(BonusType::BONUS_DAMAGE_CHANCE, attacker->creatureIndex());
@@ -903,14 +916,14 @@ void BattleActionProcessor::makeAttack(const CStack * attacker, const CStack * d
 
 	// only primary target
 	if(defender->alive())
-		drainedLife += applyBattleEffects(bat, attackerState, fireShield, defender, distance, false);
+		drainedLife += applyBattleEffects(battle, 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
+	std::set<const CStack*> attackedCreatures = battle.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);
+			drainedLife += applyBattleEffects(battle, bat, attackerState, fireShield, stack, distance, true);
 	}
 
 	std::shared_ptr<const Bonus> bonus = attacker->getBonusLocalFirst(Selector::type()(BonusType::SPELL_LIKE_ATTACK));
@@ -927,7 +940,7 @@ void BattleActionProcessor::makeAttack(const CStack * attacker, const CStack * d
 		battle::Target target;
 		target.emplace_back(defender, targetHex);
 
-		spells::BattleCast event(gameHandler->gameState()->curB, attacker, spells::Mode::SPELL_LIKE_ATTACK, spell);
+		spells::BattleCast event(&battle, attacker, spells::Mode::SPELL_LIKE_ATTACK, spell);
 		event.setSpellLevel(bonus->val);
 
 		auto attackedCreatures = spell->battleMechanics(&event)->getAffectedStacks(target);
@@ -938,7 +951,7 @@ void BattleActionProcessor::makeAttack(const CStack * attacker, const CStack * d
 		{
 			if(stack != defender && stack->alive()) //do not hit same stack twice
 			{
-				drainedLife += applyBattleEffects(bat, attackerState, fireShield, stack, distance, true);
+				drainedLife += applyBattleEffects(battle, bat, attackerState, fireShield, stack, distance, true);
 			}
 		}
 
@@ -965,6 +978,9 @@ void BattleActionProcessor::makeAttack(const CStack * attacker, const CStack * d
 	if (drainedLife > 0)
 		bat.flags |= BattleAttack::LIFE_DRAIN;
 
+	for (BattleStackAttacked & bsa : bat.bsa)
+		bsa.battleID = battle.getBattle()->getBattleID();
+
 	gameHandler->sendAndApply(&bat);
 
 	{
@@ -1012,7 +1028,7 @@ void BattleActionProcessor::makeAttack(const CStack * attacker, const CStack * d
 			const CStack * actor = item.first;
 			int64_t rawDamage = item.second;
 
-			const CGHeroInstance * actorOwner = gameHandler->gameState()->curB->getHero(actor->unitOwner());
+			const CGHeroInstance * actorOwner = battle.battleGetFightingHero(actor->unitOwner());
 
 			if(actorOwner)
 			{
@@ -1031,6 +1047,7 @@ void BattleActionProcessor::makeAttack(const CStack * attacker, const CStack * d
 		{
 			BattleStackAttacked bsa;
 
+			bsa.battleID = battle.getBattle()->getBattleID();
 			bsa.flags |= BattleStackAttacked::FIRE_SHIELD;
 			bsa.stackAttacked = attacker->unitId(); //invert
 			bsa.attackerID = defender->unitId();
@@ -1038,6 +1055,7 @@ void BattleActionProcessor::makeAttack(const CStack * attacker, const CStack * d
 			attacker->prepareAttacked(bsa, gameHandler->getRandomGenerator());
 
 			StacksInjured pack;
+			pack.battleID = battle.getBattle()->getBattleID();
 			pack.stacks.push_back(bsa);
 			gameHandler->sendAndApply(&pack);
 
@@ -1055,10 +1073,10 @@ void BattleActionProcessor::makeAttack(const CStack * attacker, const CStack * d
 
 	gameHandler->sendAndApply(&blm);
 
-	handleAfterAttackCasting(ranged, attacker, defender);
+	handleAfterAttackCasting(battle, ranged, attacker, defender);
 }
 
-void BattleActionProcessor::attackCasting(bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender)
+void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender)
 {
 	if(attacker->hasBonusOfType(attackMode))
 	{
@@ -1104,7 +1122,7 @@ void BattleActionProcessor::attackCasting(bool ranged, BonusType attackMode, con
 			spells::Target target;
 			target.emplace_back(defender);
 
-			spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell);
+			spells::BattleCast parameters(&battle, &caster, spells::Mode::PASSIVE, spell);
 
 			auto m = spell->battleMechanics(&parameters);
 
@@ -1126,17 +1144,17 @@ void BattleActionProcessor::attackCasting(bool ranged, BonusType attackMode, con
 	}
 }
 
-void BattleActionProcessor::handleAttackBeforeCasting(bool ranged, const CStack * attacker, const CStack * defender)
+void BattleActionProcessor::handleAttackBeforeCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender)
 {
-	attackCasting(ranged, BonusType::SPELL_BEFORE_ATTACK, attacker, defender); //no death stare / acid breath needed?
+	attackCasting(battle, ranged, BonusType::SPELL_BEFORE_ATTACK, attacker, defender); //no death stare / acid breath needed?
 }
 
-void BattleActionProcessor::handleAfterAttackCasting(bool ranged, const CStack * attacker, const CStack * defender)
+void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & battle, 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);
+	attackCasting(battle, ranged, BonusType::SPELL_AFTER_ATTACK, attacker, defender);
 
 	if(!defender->alive())
 	{
@@ -1169,7 +1187,7 @@ void BattleActionProcessor::handleAfterAttackCasting(bool ranged, const CStack *
 
 			spells::AbilityCaster caster(attacker, 0);
 
-			spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell);
+			spells::BattleCast parameters(&battle, &caster, spells::Mode::PASSIVE, spell);
 			spells::Target target;
 			target.emplace_back(defender);
 			parameters.setEffectValue(staredCreatures);
@@ -1194,7 +1212,7 @@ void BattleActionProcessor::handleAfterAttackCasting(bool ranged, const CStack *
 
 		spells::AbilityCaster caster(attacker, 0);
 
-		spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell);
+		spells::BattleCast parameters(&battle, &caster, spells::Mode::PASSIVE, spell);
 		spells::Target target;
 		target.emplace_back(defender);
 
@@ -1221,7 +1239,7 @@ void BattleActionProcessor::handleAfterAttackCasting(bool ranged, const CStack *
 			return;
 
 		battle::UnitInfo resurrectInfo;
-		resurrectInfo.id = gameHandler->gameState()->curB->battleNextUnitId();
+		resurrectInfo.id = battle.battleNextUnitId();
 		resurrectInfo.summoned = false;
 		resurrectInfo.position = defender->getPosition();
 		resurrectInfo.side = defender->unitSide();
@@ -1239,10 +1257,12 @@ void BattleActionProcessor::handleAfterAttackCasting(bool ranged, const CStack *
 			return; //wrong subtype
 
 		BattleUnitsChanged addUnits;
+		addUnits.battleID = battle.getBattle()->getBattleID();
 		addUnits.changedStacks.emplace_back(resurrectInfo.id, UnitChanges::EOperation::ADD);
 		resurrectInfo.save(addUnits.changedStacks.back().data);
 
 		BattleUnitsChanged removeUnits;
+		removeUnits.battleID = battle.getBattle()->getBattleID();
 		removeUnits.changedStacks.emplace_back(defender->unitId(), UnitChanges::EOperation::REMOVE);
 		gameHandler->sendAndApply(&removeUnits);
 		gameHandler->sendAndApply(&addUnits);
@@ -1279,14 +1299,15 @@ void BattleActionProcessor::handleAfterAttackCasting(bool ranged, const CStack *
 		defender->prepareAttacked(bsa, gameHandler->getRandomGenerator());
 
 		StacksInjured si;
+		si.battleID = battle.getBattle()->getBattleID();
 		si.stacks.push_back(bsa);
 
 		gameHandler->sendAndApply(&si);
-		sendGenericKilledLog(defender, bsa.killedAmount, false);
+		sendGenericKilledLog(battle, 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)
+int64_t BattleActionProcessor::applyBattleEffects(const CBattleInfoCallback & battle, BattleAttack & bat, std::shared_ptr<battle::CUnitState> attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary)
 {
 	BattleStackAttacked bsa;
 	if(secondary)
@@ -1302,8 +1323,8 @@ int64_t BattleActionProcessor::applyBattleEffects(BattleAttack & bat, std::share
 		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());
+		auto range = battle.calculateDmgRange(bai);
+		bsa.damageAmount = battle.getBattle()->getActualDamage(range.damage, attackerState->getCount(), gameHandler->getRandomGenerator());
 		CStack::prepareAttacked(bsa, gameHandler->getRandomGenerator(), bai.defender->acquireState()); //calculate casualties
 	}
 
@@ -1353,11 +1374,12 @@ int64_t BattleActionProcessor::applyBattleEffects(BattleAttack & bat, std::share
 	return drainedLife;
 }
 
-void BattleActionProcessor::sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple)
+void BattleActionProcessor::sendGenericKilledLog(const CBattleInfoCallback & battle, const CStack * defender, int32_t killed, bool multiple)
 {
 	if(killed > 0)
 	{
 		BattleLogMessage blm;
+		blm.battleID = battle.getBattle()->getBattleID();
 		addGenericKilledLog(blm, defender, killed, multiple);
 		gameHandler->sendAndApply(&blm);
 	}
@@ -1390,22 +1412,17 @@ void BattleActionProcessor::addGenericKilledLog(BattleLogMessage & blm, const CS
 	}
 }
 
-bool BattleActionProcessor::makeAutomaticBattleAction(const BattleAction & ba)
+bool BattleActionProcessor::makeAutomaticBattleAction(const CBattleInfoCallback & battle, const BattleAction & ba)
 {
-	return makeBattleActionImpl(ba);
+	return makeBattleActionImpl(battle, ba);
 }
 
-bool BattleActionProcessor::makePlayerBattleAction(PlayerColor player, const BattleAction &ba)
+bool BattleActionProcessor::makePlayerBattleAction(const CBattleInfoCallback & battle, 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(battle.battleGetTacticDist() != 0)
 	{
 		if(!ba.isTacticsAction())
 		{
@@ -1413,7 +1430,7 @@ bool BattleActionProcessor::makePlayerBattleAction(PlayerColor player, const Bat
 			return false;
 		}
 
-		if(player != battle->sides[ba.side].color)
+		if(player != battle.sideToPlayer(ba.side))
 		{
 			gameHandler->complain("Can not make actions in battles you are not part of!");
 			return false;
@@ -1421,21 +1438,21 @@ bool BattleActionProcessor::makePlayerBattleAction(PlayerColor player, const Bat
 	}
 	else
 	{
-		if (ba.isUnitAction() && ba.stackNumber != battle->getActiveStackID())
+		auto active = battle.battleActiveUnit();
+		if(!active && gameHandler->complain("No active unit in battle!"))
+			return false;
+
+		if (ba.isUnitAction() && ba.stackNumber != active->unitId())
 		{
 			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);
+		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);
+	return makeBattleActionImpl(battle, ba);
 }

+ 28 - 27
server/battles/BattleActionProcessor.h

@@ -14,6 +14,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 struct BattleLogMessage;
 struct BattleAttack;
 class BattleAction;
+class CBattleInfoCallback;
 struct BattleHex;
 class CStack;
 class PlayerColor;
@@ -38,42 +39,42 @@ class BattleActionProcessor : boost::noncopyable
 	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);
+	int moveStack(const CBattleInfoCallback & battle, int stack, BattleHex dest); //returned value - travelled distance
+	void makeAttack(const CBattleInfoCallback & battle, 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);
+	void handleAttackBeforeCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender);
+	void handleAfterAttackCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender);
+	void attackCasting(const CBattleInfoCallback & battle, 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);
+	int64_t applyBattleEffects(const CBattleInfoCallback & battle, 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 sendGenericKilledLog(const CBattleInfoCallback & battle, 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);
+	bool canStackAct(const CBattleInfoCallback & battle, const CStack * stack);
+
+	bool doEmptyAction(const CBattleInfoCallback & battle, const BattleAction & ba);
+	bool doEndTacticsAction(const CBattleInfoCallback & battle, const BattleAction & ba);
+	bool doRetreatAction(const CBattleInfoCallback & battle, const BattleAction & ba);
+	bool doSurrenderAction(const CBattleInfoCallback & battle, const BattleAction & ba);
+	bool doHeroSpellAction(const CBattleInfoCallback & battle, const BattleAction & ba);
+	bool doWalkAction(const CBattleInfoCallback & battle, const BattleAction & ba);
+	bool doWaitAction(const CBattleInfoCallback & battle, const BattleAction & ba);
+	bool doDefendAction(const CBattleInfoCallback & battle, const BattleAction & ba);
+	bool doAttackAction(const CBattleInfoCallback & battle, const BattleAction & ba);
+	bool doShootAction(const CBattleInfoCallback & battle, const BattleAction & ba);
+	bool doCatapultAction(const CBattleInfoCallback & battle, const BattleAction & ba);
+	bool doUnitSpellAction(const CBattleInfoCallback & battle, const BattleAction & ba);
+	bool doHealAction(const CBattleInfoCallback & battle, const BattleAction & ba);
+
+	bool dispatchBattleAction(const CBattleInfoCallback & battle, const BattleAction & ba);
+	bool makeBattleActionImpl(const CBattleInfoCallback & battle, const BattleAction & ba);
 
 public:
 	explicit BattleActionProcessor(BattleProcessor * owner);
 	void setGameHandler(CGameHandler * newGameHandler);
 
-	bool makeAutomaticBattleAction(const BattleAction & ba);
-	bool makePlayerBattleAction(PlayerColor player, const BattleAction & ba);
+	bool makeAutomaticBattleAction(const CBattleInfoCallback & battle, const BattleAction & ba);
+	bool makePlayerBattleAction(const CBattleInfoCallback & battle, PlayerColor player, const BattleAction & ba);
 };

+ 114 - 111
server/battles/BattleFlowProcessor.cpp

@@ -16,7 +16,8 @@
 
 #include "../../lib/CStack.h"
 #include "../../lib/GameSettings.h"
-#include "../../lib/battle/BattleInfo.h"
+#include "../../lib/battle/CBattleInfoCallback.h"
+#include "../../lib/battle/IBattleState.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/NetPacks.h"
@@ -35,7 +36,7 @@ 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
+void BattleFlowProcessor::summonGuardiansHelper(const CBattleInfoCallback & battle, 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();
@@ -110,40 +111,39 @@ void BattleFlowProcessor::summonGuardiansHelper(std::vector<BattleHex> & output,
 	}
 }
 
-void BattleFlowProcessor::tryPlaceMoats()
+void BattleFlowProcessor::tryPlaceMoats(const CBattleInfoCallback & battle)
 {
+	const auto * town = battle.battleGetDefendedTown();
+
 	//Moat should be initialized here, because only here we can use spellcasting
-	if (gameHandler->gameState()->curB->town && gameHandler->gameState()->curB->town->fortLevel() >= CGTownInstance::CITADEL)
+	if (town && town->fortLevel() >= CGTownInstance::CITADEL)
 	{
-		const auto * h = gameHandler->gameState()->curB->battleGetFightingHero(BattleSide::DEFENDER);
+		const auto * h = battle.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 moatCaster = spells::SilentCaster(battle.sideToPlayer(BattleSide::DEFENDER), actualCaster);
+		auto cast = spells::BattleCast(&battle, &moatCaster, spells::Mode::PASSIVE, town->town->moatAbility.toSpell());
 		auto target = spells::Target();
 		cast.cast(gameHandler->spellEnv, target);
 	}
 }
 
-void BattleFlowProcessor::onBattleStarted()
+void BattleFlowProcessor::onBattleStarted(const CBattleInfoCallback & battle)
 {
-	gameHandler->setBattle(gameHandler->gameState()->curB);
-	assert(gameHandler->gameState()->curB);
-
-	tryPlaceMoats();
+	tryPlaceMoats(battle);
 	
-	gameHandler->turnTimerHandler.onBattleStart();
+	gameHandler->turnTimerHandler.onBattleStart(battle.getBattle()->getBattleID());
 
-	if (gameHandler->gameState()->curB->tacticDistance == 0)
-		onTacticsEnded();
+	if (battle.battleGetTacticDist() == 0)
+		onTacticsEnded(battle);
 }
 
-void BattleFlowProcessor::trySummonGuardians(const CStack * stack)
+void BattleFlowProcessor::trySummonGuardians(const CBattleInfoCallback & battle, 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();
+	auto accessibility = battle.getAccesibility();
 	CreatureID creatureData = CreatureID(summonInfo->subtype);
 	std::vector<BattleHex> targetHexes;
 	const bool targetIsBig = stack->unitType()->isDoubleWide(); //target = creature to guard
@@ -156,14 +156,14 @@ void BattleFlowProcessor::trySummonGuardians(const CStack * stack)
 	if (!guardianIsBig)
 		targetHexes = stack->getSurroundingHexes();
 	else
-		summonGuardiansHelper(targetHexes, stack->getPosition(), stack->unitSide(), targetIsBig);
+		summonGuardiansHelper(battle, 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.id = battle.battleNextUnitId();
 			info.count =  std::max(1, (int)(stack->getCount() * 0.01 * summonInfo->val));
 			info.type = creatureData;
 			info.side = stack->unitSide();
@@ -171,6 +171,7 @@ void BattleFlowProcessor::trySummonGuardians(const CStack * stack)
 			info.summoned = true;
 
 			BattleUnitsChanged pack;
+			pack.battleID = battle.getBattle()->getBattleID();
 			pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD);
 			info.save(pack.changedStacks.back().data);
 			gameHandler->sendAndApply(&pack);
@@ -178,11 +179,11 @@ void BattleFlowProcessor::trySummonGuardians(const CStack * stack)
 	}
 }
 
-void BattleFlowProcessor::castOpeningSpells()
+void BattleFlowProcessor::castOpeningSpells(const CBattleInfoCallback & battle)
 {
 	for (int i = 0; i < 2; ++i)
 	{
-		auto h = gameHandler->gameState()->curB->battleGetFightingHero(i);
+		auto h = battle.battleGetFightingHero(i);
 		if (!h)
 			continue;
 
@@ -194,7 +195,7 @@ void BattleFlowProcessor::castOpeningSpells()
 
 			const CSpell * spell = SpellID(b->subtype).toSpell();
 
-			spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell);
+			spells::BattleCast parameters(&battle, &caster, spells::Mode::PASSIVE, spell);
 			parameters.setSpellLevel(3);
 			parameters.setEffectDuration(b->val);
 			parameters.massive = true;
@@ -203,55 +204,54 @@ void BattleFlowProcessor::castOpeningSpells()
 	}
 }
 
-void BattleFlowProcessor::onTacticsEnded()
+void BattleFlowProcessor::onTacticsEnded(const CBattleInfoCallback & battle)
 {
 	//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
+	auto initialStacks = battle.battleGetAllStacks(true);
 
-	for (CStack * stack : initialStacks)
+	for (const CStack * stack : initialStacks)
 	{
-		trySummonGuardians(stack);
-		stackEnchantedTrigger(stack);
+		trySummonGuardians(battle, stack);
+		stackEnchantedTrigger(battle, stack);
 	}
 
-	castOpeningSpells();
+	castOpeningSpells(battle);
 
 	// it is possible that due to opening spells one side was eliminated -> check for end of battle
-	if (owner->checkBattleStateChanges())
+	if (owner->checkBattleStateChanges(battle))
 		return;
 
-	startNextRound(true);
-	activateNextStack();
+	startNextRound(battle, true);
+	activateNextStack(battle);
 }
 
-void BattleFlowProcessor::startNextRound(bool isFirstRound)
+void BattleFlowProcessor::startNextRound(const CBattleInfoCallback & battle, bool isFirstRound)
 {
 	BattleNextRound bnr;
-	bnr.round = gameHandler->gameState()->curB->round + 1;
-	logGlobal->debug("Round %d", bnr.round);
+	bnr.battleID = battle.getBattle()->getBattleID();
+	logGlobal->debug("Next round starts");
 	gameHandler->sendAndApply(&bnr);
 
-	auto obstacles = gameHandler->gameState()->curB->obstacles; //we copy container, because we're going to modify it
+	// operate on copy - removing obstacles will invalidate iterator on 'battle' container
+	auto obstacles = battle.battleGetAllObstacles();
 	for (auto &obstPtr : obstacles)
 	{
 		if (const SpellCreatedObstacle *sco = dynamic_cast<const SpellCreatedObstacle *>(obstPtr.get()))
 			if (sco->turnsRemaining == 0)
-				removeObstacle(*obstPtr);
+				removeObstacle(battle, *obstPtr);
 	}
 
-	const BattleInfo & curB = *gameHandler->gameState()->curB;
-
-	for(auto stack : curB.stacks)
+	for(auto stack : battle.battleGetAllStacks(true))
 	{
 		if(stack->alive() && !isFirstRound)
-			stackEnchantedTrigger(stack);
+			stackEnchantedTrigger(battle, stack);
 	}
 }
 
-const CStack * BattleFlowProcessor::getNextStack()
+const CStack * BattleFlowProcessor::getNextStack(const CBattleInfoCallback & battle)
 {
 	std::vector<battle::Units> q;
-	gameHandler->gameState()->curB->battleGetTurnOrder(q, 1, 0, -1); //todo: get rid of "turn -1"
+	battle.battleGetTurnOrder(q, 1, 0, -1); //todo: get rid of "turn -1"
 
 	if(q.empty())
 		return nullptr;
@@ -267,6 +267,7 @@ const CStack * BattleFlowProcessor::getNextStack()
 	if(stack && stack->alive() && !stack->waiting)
 	{
 		BattleTriggerEffect bte;
+		bte.battleID = battle.getBattle()->getBattleID();
 		bte.stackID = stack->unitId();
 		bte.effect = vstd::to_underlying(BonusType::HP_REGENERATION);
 
@@ -284,53 +285,51 @@ const CStack * BattleFlowProcessor::getNextStack()
 	return stack;
 }
 
-void BattleFlowProcessor::activateNextStack()
+void BattleFlowProcessor::activateNextStack(const CBattleInfoCallback & battle)
 {
-	const auto & curB = *gameHandler->gameState()->curB;
-
 	// Find next stack that requires manual control
 	for (;;)
 	{
 		// battle has ended
-		if (owner->checkBattleStateChanges())
+		if (owner->checkBattleStateChanges(battle))
 			return;
 
-		const CStack * next = getNextStack();
+		const CStack * next = getNextStack(battle);
 
 		if (!next)
 		{
 			// No stacks to move - start next round
-			startNextRound(false);
-			next = getNextStack();
+			startNextRound(battle, false);
+			next = getNextStack(battle);
 			if (!next)
 				throw std::runtime_error("Failed to find valid stack to act!");
 		}
 
 		BattleUnitsChanged removeGhosts;
+		removeGhosts.battleID = battle.getBattle()->getBattleID();
 
-		for(auto stack : curB.stacks)
-		{
-			if(stack->ghostPending)
-				removeGhosts.changedStacks.emplace_back(stack->unitId(), UnitChanges::EOperation::REMOVE);
-		}
+		auto pendingGhosts = battle.battleGetStacksIf([](const CStack * stack){
+			return stack->ghostPending;
+		});
+
+		for(auto stack : pendingGhosts)
+			removeGhosts.changedStacks.emplace_back(stack->unitId(), UnitChanges::EOperation::REMOVE);
 
 		if(!removeGhosts.changedStacks.empty())
 			gameHandler->sendAndApply(&removeGhosts);
 		
-		gameHandler->turnTimerHandler.onBattleNextStack(*next);
+		gameHandler->turnTimerHandler.onBattleNextStack(battle.getBattle()->getBattleID(), *next);
 
-		if (!tryMakeAutomaticAction(next))
+		if (!tryMakeAutomaticAction(battle, next))
 		{
-			setActiveStack(next);
+			setActiveStack(battle, next);
 			break;
 		}
 	}
 }
 
-bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next)
+bool BattleFlowProcessor::tryMakeAutomaticAction(const CBattleInfoCallback & battle, 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)
@@ -346,7 +345,7 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next)
 			ba.side = next->unitSide();
 			ba.stackNumber = next->unitId();
 
-			makeAutomaticAction(next, ba);
+			makeAutomaticAction(battle, next, ba);
 			return true;
 		}
 	}
@@ -354,7 +353,7 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next)
 	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);
+		std::pair<const battle::Unit *, BattleHex> attackInfo = battle.getNearestStack(next);
 		if (attackInfo.first != nullptr)
 		{
 			BattleAction attack;
@@ -364,18 +363,18 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next)
 			attack.aimToHex(attackInfo.second);
 			attack.aimToUnit(attackInfo.first);
 
-			makeAutomaticAction(next, attack);
+			makeAutomaticAction(battle, next, attack);
 			logGlobal->trace("Attacked nearest target %s", attackInfo.first->getDescription());
 		}
 		else
 		{
-			makeStackDoNothing(next);
+			makeStackDoNothing(battle, next);
 			logGlobal->trace("No target found");
 		}
 		return true;
 	}
 
-	const CGHeroInstance * curOwner = gameHandler->battleGetOwnerHero(next);
+	const CGHeroInstance * curOwner = battle.battleGetOwnerHero(next);
 	const int stackCreatureId = next->unitType()->getId();
 
 	if ((stackCreatureId == CreatureID::ARROW_TOWERS || stackCreatureId == CreatureID::BALLISTA)
@@ -390,12 +389,12 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next)
 
 		const battle::Unit * target = nullptr;
 
-		for(auto & elem : gameHandler->gameState()->curB->stacks)
+		for(auto & elem : battle.battleGetAllStacks(true))
 		{
 			if(elem->unitType()->getId() != CreatureID::CATAPULT
 			   && elem->unitOwner() != next->unitOwner()
 			   && elem->isValidTarget()
-			   && gameHandler->gameState()->curB->battleCanShoot(next, elem->getPosition()))
+			   && battle.battleCanShoot(next, elem->getPosition()))
 			{
 				target = elem;
 				break;
@@ -404,23 +403,23 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next)
 
 		if(target == nullptr)
 		{
-			makeStackDoNothing(next);
+			makeStackDoNothing(battle, next);
 		}
 		else
 		{
 			attack.aimToUnit(target);
-			makeAutomaticAction(next, attack);
+			makeAutomaticAction(battle, next, attack);
 		}
 		return true;
 	}
 
 	if (next->unitType()->getId() == CreatureID::CATAPULT)
 	{
-		const auto & attackableBattleHexes = curB.getAttackableBattleHexes();
+		const auto & attackableBattleHexes = battle.getAttackableBattleHexes();
 
 		if (attackableBattleHexes.empty())
 		{
-			makeStackDoNothing(next);
+			makeStackDoNothing(battle, next);
 			return true;
 		}
 
@@ -431,21 +430,21 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next)
 			attack.side = next->unitSide();
 			attack.stackNumber = next->unitId();
 
-			makeAutomaticAction(next, attack);
+			makeAutomaticAction(battle, next, attack);
 			return true;
 		}
 	}
 
 	if (next->unitType()->getId() == CreatureID::FIRST_AID_TENT)
 	{
-		TStacks possibleStacks = gameHandler->battleGetStacksIf([=](const CStack * s)
+		TStacks possibleStacks = battle.battleGetStacksIf([=](const CStack * s)
 		{
 			return s->unitOwner() == next->unitOwner() && s->canBeHealed();
 		});
 
 		if (possibleStacks.empty())
 		{
-			makeStackDoNothing(next);
+			makeStackDoNothing(battle, next);
 			return true;
 		}
 
@@ -460,22 +459,22 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next)
 			heal.side = next->unitSide();
 			heal.stackNumber = next->unitId();
 
-			makeAutomaticAction(next, heal);
+			makeAutomaticAction(battle, next, heal);
 			return true;
 		}
 	}
 
-	stackTurnTrigger(next); //various effects
+	stackTurnTrigger(battle, next); //various effects
 
 	if(next->fear)
 	{
-		makeStackDoNothing(next); //end immediately if stack was affected by fear
+		makeStackDoNothing(battle, next); //end immediately if stack was affected by fear
 		return true;
 	}
 	return false;
 }
 
-bool BattleFlowProcessor::rollGoodMorale(const CStack * next)
+bool BattleFlowProcessor::rollGoodMorale(const CBattleInfoCallback & battle, const CStack * next)
 {
 	//check for good morale
 	auto nextStackMorale = next->moraleVal();
@@ -492,6 +491,7 @@ bool BattleFlowProcessor::rollGoodMorale(const CStack * next)
 		if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1)
 		{
 			BattleTriggerEffect bte;
+			bte.battleID = battle.getBattle()->getBattleID();
 			bte.stackID = next->unitId();
 			bte.effect = vstd::to_underlying(BonusType::MORALE);
 			bte.val = 1;
@@ -503,27 +503,25 @@ bool BattleFlowProcessor::rollGoodMorale(const CStack * next)
 	return false;
 }
 
-void BattleFlowProcessor::onActionMade(const BattleAction &ba)
+void BattleFlowProcessor::onActionMade(const CBattleInfoCallback & battle, const BattleAction &ba)
 {
-	const auto & battle = gameHandler->gameState()->curB;
-
+	const auto * actedStack = battle.battleGetStackByID(ba.stackNumber, false);
+	const auto * activeStack = battle.battleActiveUnit();
 	if (ba.actionType == EActionType::END_TACTIC_PHASE)
 	{
-		onTacticsEnded();
+		onTacticsEnded(battle);
 		return;
 	}
 
-	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())
+	if(owner->checkBattleStateChanges(battle))
 		return;
 
 	// tactics - next stack will be selected by player
-	if(battle->tacticDistance != 0)
+	if(battle.battleGetTacticDist() != 0)
 		return;
 
 	if (ba.isUnitAction())
@@ -531,10 +529,10 @@ void BattleFlowProcessor::onActionMade(const BattleAction &ba)
 		assert(activeStack != nullptr);
 		assert(actedStack != nullptr);
 
-		if (rollGoodMorale(actedStack))
+		if (rollGoodMorale(battle, actedStack))
 		{
 			// Good morale - same stack makes 2nd turn
-			setActiveStack(actedStack);
+			setActiveStack(battle, actedStack);
 			return;
 		}
 	}
@@ -544,36 +542,37 @@ void BattleFlowProcessor::onActionMade(const BattleAction &ba)
 		{
 			// 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);
+			setActiveStack(battle, activeStack);
 			return;
 		}
 	}
 
-	activateNextStack();
+	activateNextStack(battle);
 }
 
-void BattleFlowProcessor::makeStackDoNothing(const CStack * next)
+void BattleFlowProcessor::makeStackDoNothing(const CBattleInfoCallback & battle, const CStack * next)
 {
 	BattleAction doNothing;
 	doNothing.actionType = EActionType::NO_ACTION;
 	doNothing.side = next->unitSide();
 	doNothing.stackNumber = next->unitId();
 
-	makeAutomaticAction(next, doNothing);
+	makeAutomaticAction(battle, next, doNothing);
 }
 
-bool BattleFlowProcessor::makeAutomaticAction(const CStack *stack, BattleAction &ba)
+bool BattleFlowProcessor::makeAutomaticAction(const CBattleInfoCallback & battle, const CStack *stack, BattleAction &ba)
 {
 	BattleSetActiveStack bsa;
+	bsa.battleID = battle.getBattle()->getBattleID();
 	bsa.stack = stack->unitId();
 	bsa.askPlayerInterface = false;
 	gameHandler->sendAndApply(&bsa);
 
-	bool ret = owner->makeAutomaticBattleAction(ba);
+	bool ret = owner->makeAutomaticBattleAction(battle, ba);
 	return ret;
 }
 
-void BattleFlowProcessor::stackEnchantedTrigger(const CStack * st)
+void BattleFlowProcessor::stackEnchantedTrigger(const CBattleInfoCallback & battle, const CStack * st)
 {
 	auto bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTED)));
 	for(auto b : bl)
@@ -585,7 +584,7 @@ void BattleFlowProcessor::stackEnchantedTrigger(const CStack * st)
 		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);
+		spells::BattleCast battleCast(&battle, 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);
@@ -593,8 +592,8 @@ void BattleFlowProcessor::stackEnchantedTrigger(const CStack * st)
 
 		if(val > 3)
 		{
-			for(auto s : gameHandler->gameState()->curB->battleGetAllStacks())
-				if(gameHandler->battleMatchOwner(st, s, true) && s->isValidTarget()) //all allied
+			for(auto s : battle.battleGetAllStacks())
+				if(battle.battleMatchOwner(st, s, true) && s->isValidTarget()) //all allied
 					target.emplace_back(s);
 		}
 		else
@@ -605,16 +604,18 @@ void BattleFlowProcessor::stackEnchantedTrigger(const CStack * st)
 	}
 }
 
-void BattleFlowProcessor::removeObstacle(const CObstacleInstance & obstacle)
+void BattleFlowProcessor::removeObstacle(const CBattleInfoCallback & battle, const CObstacleInstance & obstacle)
 {
 	BattleObstaclesChanged obsRem;
+	obsRem.battleID = battle.getBattle()->getBattleID();
 	obsRem.changes.emplace_back(obstacle.uniqueID, ObstacleChanges::EOperation::REMOVE);
 	gameHandler->sendAndApply(&obsRem);
 }
 
-void BattleFlowProcessor::stackTurnTrigger(const CStack *st)
+void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, const CStack *st)
 {
 	BattleTriggerEffect bte;
+	bte.battleID = battle.getBattle()->getBattleID();
 	bte.stackID = st->unitId();
 	bte.effect = -1;
 	bte.val = 0;
@@ -626,13 +627,13 @@ void BattleFlowProcessor::stackTurnTrigger(const CStack *st)
 		{
 			bool unbind = true;
 			BonusList bl = *(st->getBonuses(Selector::type()(BonusType::BIND_EFFECT)));
-			auto adjacent = gameHandler->gameState()->curB->battleAdjacentUnits(st);
+			auto adjacent = battle.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
+					const CStack * stack = battle.battleGetStackByID(b->additionalInfo[0]); //binding stack must be alive and adjacent
 					if(stack)
 					{
 						if(vstd::contains(adjacent, stack)) //binding stack is still present
@@ -647,6 +648,7 @@ void BattleFlowProcessor::stackTurnTrigger(const CStack *st)
 			if (unbind)
 			{
 				BattleSetStackProperty ssp;
+				ssp.battleID = battle.getBattle()->getBattleID();
 				ssp.which = BattleSetStackProperty::UNBIND;
 				ssp.stackID = st->unitId();
 				gameHandler->sendAndApply(&ssp);
@@ -668,8 +670,8 @@ void BattleFlowProcessor::stackTurnTrigger(const CStack *st)
 		}
 		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);
+			const PlayerColor opponent = battle.otherPlayer(battle.battleGetOwner(st));
+			const CGHeroInstance * opponentHero = battle.battleGetFightingHero(opponent);
 			if(opponentHero)
 			{
 				ui32 manaDrained = st->valOfBonuses(BonusType::MANA_DRAIN);
@@ -686,9 +688,9 @@ void BattleFlowProcessor::stackTurnTrigger(const CStack *st)
 		if (st->isLiving() && !st->hasBonusOfType(BonusType::FEARLESS))
 		{
 			bool fearsomeCreature = false;
-			for (CStack * stack : gameHandler->gameState()->curB->stacks)
+			for (const CStack * stack : battle.battleGetAllStacks(true))
 			{
-				if (gameHandler->battleMatchOwner(st, stack) && stack->alive() && stack->hasBonusOfType(BonusType::FEAR))
+				if (battle.battleMatchOwner(st, stack) && stack->alive() && stack->hasBonusOfType(BonusType::FEAR))
 				{
 					fearsomeCreature = true;
 					break;
@@ -704,8 +706,8 @@ void BattleFlowProcessor::stackTurnTrigger(const CStack *st)
 			}
 		}
 		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)
+		int side = *battle.playerToSide(st->unitOwner());
+		if(st->canCast() && battle.battleGetEnchanterCounter(side) == 0)
 		{
 			bool cast = false;
 			while(!bl.empty() && !cast)
@@ -717,7 +719,7 @@ void BattleFlowProcessor::stackTurnTrigger(const CStack *st)
 				{
 					return b == bonus.get();
 				});
-				spells::BattleCast parameters(gameHandler->gameState()->curB, st, spells::Mode::ENCHANTER, spell);
+				spells::BattleCast parameters(&battle, st, spells::Mode::ENCHANTER, spell);
 				parameters.setSpellLevel(bonus->val);
 				parameters.massive = true;
 				parameters.smart = true;
@@ -728,6 +730,7 @@ void BattleFlowProcessor::stackTurnTrigger(const CStack *st)
 
 					int cooldown = bonus->additionalInfo[0];
 					BattleSetStackProperty ssp;
+					ssp.battleID = battle.getBattle()->getBattleID();
 					ssp.which = BattleSetStackProperty::ENCHANTER_COUNTER;
 					ssp.absolute = false;
 					ssp.val = cooldown;
@@ -739,12 +742,12 @@ void BattleFlowProcessor::stackTurnTrigger(const CStack *st)
 	}
 }
 
-void BattleFlowProcessor::setActiveStack(const CStack * stack)
+void BattleFlowProcessor::setActiveStack(const CBattleInfoCallback & battle, const battle::Unit * stack)
 {
 	assert(stack);
 
-	logGlobal->trace("Activating %s", stack->nodeName());
 	BattleSetActiveStack sas;
+	sas.battleID = battle.getBattle()->getBattleID();
 	sas.stack = stack->unitId();
 	gameHandler->sendAndApply(&sas);
 }

+ 23 - 18
server/battles/BattleFlowProcessor.h

@@ -13,7 +13,12 @@ VCMI_LIB_NAMESPACE_BEGIN
 class CStack;
 struct BattleHex;
 class BattleAction;
+class CBattleInfoCallback;
 struct CObstacleInstance;
+namespace battle
+{
+class Unit;
+}
 VCMI_LIB_NAMESPACE_END
 
 class CGameHandler;
@@ -25,31 +30,31 @@ class BattleFlowProcessor : boost::noncopyable
 	BattleProcessor * owner;
 	CGameHandler * gameHandler;
 
-	const CStack * getNextStack();
+	const CStack * getNextStack(const CBattleInfoCallback & battle);
 
-	bool rollGoodMorale(const CStack * stack);
-	bool tryMakeAutomaticAction(const CStack * stack);
+	bool rollGoodMorale(const CBattleInfoCallback & battle, const CStack * stack);
+	bool tryMakeAutomaticAction(const CBattleInfoCallback & battle, 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 summonGuardiansHelper(const CBattleInfoCallback & battle, std::vector<BattleHex> & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex);
+	void trySummonGuardians(const CBattleInfoCallback & battle, const CStack * stack);
+	void tryPlaceMoats(const CBattleInfoCallback & battle);
+	void castOpeningSpells(const CBattleInfoCallback & battle);
+	void activateNextStack(const CBattleInfoCallback & battle);
+	void startNextRound(const CBattleInfoCallback & battle, bool isFirstRound);
 
-	void stackEnchantedTrigger(const CStack * stack);
-	void removeObstacle(const CObstacleInstance & obstacle);
-	void stackTurnTrigger(const CStack * stack);
-	void setActiveStack(const CStack * stack);
+	void stackEnchantedTrigger(const CBattleInfoCallback & battle, const CStack * stack);
+	void removeObstacle(const CBattleInfoCallback & battle, const CObstacleInstance & obstacle);
+	void stackTurnTrigger(const CBattleInfoCallback & battle, const CStack * stack);
+	void setActiveStack(const CBattleInfoCallback & battle, const battle::Unit * 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)
+	void makeStackDoNothing(const CBattleInfoCallback & battle, const CStack * next);
+	bool makeAutomaticAction(const CBattleInfoCallback & battle, 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);
+	void onBattleStarted(const CBattleInfoCallback & battle);
+	void onTacticsEnded(const CBattleInfoCallback & battle);
+	void onActionMade(const CBattleInfoCallback & battle, const BattleAction & ba);
 };

+ 103 - 57
server/battles/BattleProcessor.cpp

@@ -19,6 +19,7 @@
 #include "../queries/BattleQueries.h"
 
 #include "../../lib/TerrainHandler.h"
+#include "../../lib/battle/CBattleInfoCallback.h"
 #include "../../lib/battle/BattleInfo.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/mapping/CMap.h"
@@ -50,31 +51,23 @@ void BattleProcessor::engageIntoBattle(PlayerColor player)
 	gameHandler->sendAndApply(&pb);
 }
 
-void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile,
+void BattleProcessor::restartBattlePrimary(const BattleID & battleID, 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
+								const CGTownInstance *town)
 {
-	if(gameHandler->gameState()->curB)
-		gameHandler->gameState()->curB.dellNull();
+	auto battle = gameHandler->gameState()->getBattle(battleID);
 
-	engageIntoBattle(army1->tempOwner);
-	engageIntoBattle(army2->tempOwner);
+	auto lastBattleQuery = std::dynamic_pointer_cast<CBattleQuery>(gameHandler->queries->topQuery(battle->sides[0].color));
 
-	static const CArmedInstance *armies[2];
-	armies[0] = army1;
-	armies[1] = army2;
-	static const CGHeroInstance*heroes[2];
-	heroes[0] = hero1;
-	heroes[1] = hero2;
-
-	resultProcessor->setupBattle();
-	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));
+	assert(lastBattleQuery);
 
 	//existing battle query for retying auto-combat
 	if(lastBattleQuery)
 	{
+		const CGHeroInstance*heroes[2];
+		heroes[0] = hero1;
+		heroes[1] = hero2;
+
 		for(int i : {0, 1})
 		{
 			if(heroes[i])
@@ -86,23 +79,60 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm
 			}
 		}
 
-		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;
+
+		assert(lastBattleQuery->belligerents[0] == battle->sides[0].armyObject);
+		assert(lastBattleQuery->belligerents[1] == battle->sides[1].armyObject);
 	}
 
-	auto nextBattleQuery = std::make_shared<CBattleQuery>(gameHandler, gameHandler->gameState()->curB);
-	for(int i : {0, 1})
+	BattleCancelled bc;
+	bc.battleID = battleID;
+	gameHandler->sendAndApply(&bc);
+
+	startBattlePrimary(army1, army2, tile, hero1, hero2, creatureBank, town);
+}
+
+void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile,
+								const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank,
+								const CGTownInstance *town)
+{
+	assert(gameHandler->gameState()->getBattle(army1->getOwner()) == nullptr);
+	assert(gameHandler->gameState()->getBattle(army2->getOwner()) == nullptr);
+
+	engageIntoBattle(army1->tempOwner);
+	engageIntoBattle(army2->tempOwner);
+
+	const CArmedInstance *armies[2];
+	armies[0] = army1;
+	armies[1] = army2;
+	const CGHeroInstance*heroes[2];
+	heroes[0] = hero1;
+	heroes[1] = hero2;
+
+	auto battleID = setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces
+
+	const auto * battle = gameHandler->gameState()->getBattle(battleID);
+	assert(battle);
+
+	auto lastBattleQuery = std::dynamic_pointer_cast<CBattleQuery>(gameHandler->queries->topQuery(battle->sides[0].color));
+
+	if (lastBattleQuery)
 	{
-		if(heroes[i])
-		{
-			nextBattleQuery->initialHeroMana[i] = heroes[i]->mana;
-		}
+		lastBattleQuery->battleID = battleID;
 	}
-	gameHandler->queries->addQuery(nextBattleQuery);
+	else
+	{
+		auto newBattleQuery = std::make_shared<CBattleQuery>(gameHandler, battle);
 
-	flowProcessor->onBattleStarted();
+		// store initial mana to reset if battle has been restarted
+		for(int i : {0, 1})
+			if(heroes[i])
+				newBattleQuery->initialHeroMana[i] = heroes[i]->mana;
+
+		gameHandler->queries->addQuery(newBattleQuery);
+	}
+
+	flowProcessor->onBattleStarted(*battle);
 }
 
 void BattleProcessor::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank)
@@ -118,7 +148,7 @@ void BattleProcessor::startBattleI(const CArmedInstance *army1, const CArmedInst
 	startBattleI(army1, army2, army2->visitablePos(), creatureBank);
 }
 
-void BattleProcessor::setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town)
+BattleID 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();
@@ -132,6 +162,7 @@ void BattleProcessor::setupBattle(int3 tile, const CArmedInstance *armies[2], co
 	//send info about battles
 	BattleStart bs;
 	bs.info = BattleInfo::setupBattle(tile, terrain, terType, armies, heroes, creatureBank, town);
+	bs.battleID = gameHandler->gameState()->nextBattleID;
 
 	engageIntoBattle(bs.info->sides[0].color);
 	engageIntoBattle(bs.info->sides[1].color);
@@ -140,28 +171,30 @@ void BattleProcessor::setupBattle(int3 tile, const CArmedInstance *armies[2], co
 	bs.info->replayAllowed = lastBattleQuery == nullptr && !bs.info->sides[1].color.isValidPlayer();
 
 	gameHandler->sendAndApply(&bs);
+
+	return bs.battleID;
 }
 
-bool BattleProcessor::checkBattleStateChanges()
+bool BattleProcessor::checkBattleStateChanges(const CBattleInfoCallback & battle)
 {
 	//check if drawbridge state need to be changes
-	if (gameHandler->battleGetSiegeLevel() > 0)
-		updateGateState();
+	if (battle.battleGetSiegeLevel() > 0)
+		updateGateState(battle);
 
-	if (resultProcessor->battleIsEnding())
+	if (resultProcessor->battleIsEnding(battle))
 		return true;
 
 	//check if battle ended
-	if (auto result = gameHandler->battleIsFinished())
+	if (auto result = battle.battleIsFinished())
 	{
-		setBattleResult(EBattleResult::NORMAL, *result);
+		setBattleResult(battle, EBattleResult::NORMAL, *result);
 		return true;
 	}
 
 	return false;
 }
 
-void BattleProcessor::updateGateState()
+void BattleProcessor::updateGateState(const CBattleInfoCallback & battle)
 {
 	// GATE_BRIDGE - leftmost tile, located over moat
 	// GATE_OUTER - central tile, mostly covered by gate image
@@ -177,18 +210,20 @@ void BattleProcessor::updateGateState()
 	// - 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(BattleHex::GATE_BRIDGE), true).empty();
-	bool hasStackAtGateInner   = gameHandler->gameState()->curB->battleGetUnitByPos(BattleHex(BattleHex::GATE_INNER), false) != nullptr;
-	bool hasStackAtGateOuter   = gameHandler->gameState()->curB->battleGetUnitByPos(BattleHex(BattleHex::GATE_OUTER), false) != nullptr;
-	bool hasStackAtGateBridge  = gameHandler->gameState()->curB->battleGetUnitByPos(BattleHex(BattleHex::GATE_BRIDGE), false) != nullptr;
-	bool hasWideMoat           = vstd::contains_if(gameHandler->battleGetAllObstaclesOnPos(BattleHex(BattleHex::GATE_BRIDGE), false), [](const std::shared_ptr<const CObstacleInstance> & obst)
+	bool hasForceFieldOnBridge = !battle.battleGetAllObstaclesOnPos(BattleHex(BattleHex::GATE_BRIDGE), true).empty();
+	bool hasStackAtGateInner   = battle.battleGetUnitByPos(BattleHex(BattleHex::GATE_INNER), false) != nullptr;
+	bool hasStackAtGateOuter   = battle.battleGetUnitByPos(BattleHex(BattleHex::GATE_OUTER), false) != nullptr;
+	bool hasStackAtGateBridge  = battle.battleGetUnitByPos(BattleHex(BattleHex::GATE_BRIDGE), false) != nullptr;
+	bool hasWideMoat           = vstd::contains_if(battle.battleGetAllObstaclesOnPos(BattleHex(BattleHex::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 = battle.battleGetGateState();
+	db.battleID = battle.getBattle()->getBattleID();
+
+	if (battle.battleGetWallState(EWallPart::GATE) == EWallState::DESTROYED)
 	{
 		db.state = EGateState::DESTROYED;
 	}
@@ -212,37 +247,48 @@ void BattleProcessor::updateGateState()
 			db.state = EGateState::CLOSED;
 	}
 
-	if (db.state != gameHandler->gameState()->curB->si.gateState)
+	if (db.state != battle.battleGetGateState())
 		gameHandler->sendAndApply(&db);
 }
 
-bool BattleProcessor::makePlayerBattleAction(PlayerColor player, const BattleAction &ba)
+bool BattleProcessor::makePlayerBattleAction(const BattleID & battleID, PlayerColor player, const BattleAction &ba)
 {
-	bool result = actionsProcessor->makePlayerBattleAction(player, ba);
-	if (!resultProcessor->battleIsEnding() && gameHandler->gameState()->curB != nullptr)
-		flowProcessor->onActionMade(ba);
+	const auto * battle = gameHandler->gameState()->getBattle(battleID);
+
+	if (!battle)
+		return false;
+
+	bool result = actionsProcessor->makePlayerBattleAction(*battle, player, ba);
+	if (gameHandler->gameState()->getBattle(battleID) != nullptr && !resultProcessor->battleIsEnding(*battle))
+		flowProcessor->onActionMade(*battle, ba);
 	return result;
 }
 
-void BattleProcessor::setBattleResult(EBattleResult resultType, int victoriusSide)
+void BattleProcessor::setBattleResult(const CBattleInfoCallback & battle, EBattleResult resultType, int victoriusSide)
 {
-	resultProcessor->setBattleResult(resultType, victoriusSide);
-	resultProcessor->endBattle(gameHandler->gameState()->curB->tile, gameHandler->gameState()->curB->battleGetFightingHero(0), gameHandler->gameState()->curB->battleGetFightingHero(1));
+	resultProcessor->setBattleResult(battle, resultType, victoriusSide);
+	resultProcessor->endBattle(battle);
 }
 
-bool BattleProcessor::makeAutomaticBattleAction(const BattleAction &ba)
+bool BattleProcessor::makeAutomaticBattleAction(const CBattleInfoCallback & battle, const BattleAction &ba)
 {
-	return actionsProcessor->makeAutomaticBattleAction(ba);
+	return actionsProcessor->makeAutomaticBattleAction(battle, ba);
 }
 
-void BattleProcessor::endBattleConfirm(const BattleInfo * battleInfo)
+void BattleProcessor::endBattleConfirm(const BattleID & battleID)
 {
-	resultProcessor->endBattleConfirm(battleInfo);
+	auto battle = gameHandler->gameState()->getBattle(battleID);
+	assert(battle);
+
+	if (!battle)
+		return;
+
+	resultProcessor->endBattleConfirm(*battle);
 }
 
-void BattleProcessor::battleAfterLevelUp(const BattleResult &result)
+void BattleProcessor::battleAfterLevelUp(const BattleID & battleID, const BattleResult &result)
 {
-	resultProcessor->battleAfterLevelUp(result);
+	resultProcessor->battleAfterLevelUp(battleID, result);
 }
 
 void BattleProcessor::setGameHandler(CGameHandler * newGameHandler)

+ 12 - 10
server/battles/BattleProcessor.h

@@ -17,8 +17,9 @@ class CGTownInstance;
 class CArmedInstance;
 class BattleAction;
 class int3;
-class BattleInfo;
+class CBattleInfoCallback;
 struct BattleResult;
+class BattleID;
 VCMI_LIB_NAMESPACE_END
 
 class CGameHandler;
@@ -39,16 +40,15 @@ class BattleProcessor : boost::noncopyable
 	std::unique_ptr<BattleFlowProcessor> flowProcessor;
 	std::unique_ptr<BattleResultProcessor> resultProcessor;
 
-	void onTacticsEnded();
-	void updateGateState();
+	void updateGateState(const CBattleInfoCallback & battle);
 	void engageIntoBattle(PlayerColor player);
 
-	bool checkBattleStateChanges();
-	void setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town);
+	bool checkBattleStateChanges(const CBattleInfoCallback & battle);
+	BattleID setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town);
 
-	bool makeAutomaticBattleAction(const BattleAction & ba);
+	bool makeAutomaticBattleAction(const CBattleInfoCallback & battle, const BattleAction & ba);
 
-	void setBattleResult(EBattleResult resultType, int victoriusSide);
+	void setBattleResult(const CBattleInfoCallback & battle, EBattleResult resultType, int victoriusSide);
 
 public:
 	explicit BattleProcessor(CGameHandler * gameHandler);
@@ -63,14 +63,16 @@ public:
 	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);
+	/// Restart ongoing battle and end previous battle
+	void restartBattlePrimary(const BattleID & battleID, const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr);
 
 	/// Processing of incoming battle action netpack
-	bool makePlayerBattleAction(PlayerColor player, const BattleAction & ba);
+	bool makePlayerBattleAction(const BattleID & battleID, PlayerColor player, const BattleAction & ba);
 
 	/// Applies results of a battle once player agrees to them
-	void endBattleConfirm(const BattleInfo * battleInfo);
+	void endBattleConfirm(const BattleID & battleID);
 	/// Applies results of a battle after potential levelup
-	void battleAfterLevelUp(const BattleResult & result);
+	void battleAfterLevelUp(const BattleID & battleID, const BattleResult & result);
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{

+ 94 - 58
server/battles/BattleResultProcessor.cpp

@@ -18,7 +18,9 @@
 #include "../../lib/ArtifactUtils.h"
 #include "../../lib/CStack.h"
 #include "../../lib/GameSettings.h"
-#include "../../lib/battle/BattleInfo.h"
+#include "../../lib/battle/CBattleInfoCallback.h"
+#include "../../lib/battle/IBattleState.h"
+#include "../../lib/battle/SideInBattle.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/serializer/Cast.h"
@@ -35,15 +37,19 @@ void BattleResultProcessor::setGameHandler(CGameHandler * newGameHandler)
 	gameHandler = newGameHandler;
 }
 
-CasualtiesAfterBattle::CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat):
-	army(battleSide.armyObject)
+CasualtiesAfterBattle::CasualtiesAfterBattle(const CBattleInfoCallback & battle, uint8_t sideInBattle):
+	army(battle.battleGetArmyObject(sideInBattle))
 {
 	heroWithDeadCommander = ObjectInstanceID();
 
-	PlayerColor color = battleSide.color;
+	PlayerColor color = battle.sideToPlayer(sideInBattle);
 
-	for(CStack * st : bat->stacks)
+	for(const CStack * stConst : battle.battleGetAllStacks(true))
 	{
+		// Use const cast - in order to call non-const "takeResurrected" for proper calculation of casualties
+		// TODO: better solution
+		CStack * st = const_cast<CStack*>(stConst);
+
 		if(st->summoned) //don't take into account temporary summoned stacks
 			continue;
 		if(st->unitOwner() != color) //remove only our stacks
@@ -174,29 +180,36 @@ void CasualtiesAfterBattle::updateArmy(CGameHandler *gh)
 	}
 }
 
-FinishingBattleHelper::FinishingBattleHelper(std::shared_ptr<const CBattleQuery> Query, int remainingBattleQueriesCount)
+FinishingBattleHelper::FinishingBattleHelper(const CBattleInfoCallback & info, const BattleResult & result, 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;
+	if (result.winner == BattleSide::ATTACKER)
+	{
+		winnerHero = info.getBattle()->getSideHero(BattleSide::ATTACKER);
+		loserHero = info.getBattle()->getSideHero(BattleSide::DEFENDER);
+		victor = info.getBattle()->getSidePlayer(BattleSide::ATTACKER);
+		loser = info.getBattle()->getSidePlayer(BattleSide::DEFENDER);
+	}
+	else
+	{
+		winnerHero = info.getBattle()->getSideHero(BattleSide::DEFENDER);
+		loserHero = info.getBattle()->getSideHero(BattleSide::ATTACKER);
+		victor = info.getBattle()->getSidePlayer(BattleSide::DEFENDER);
+		loser = info.getBattle()->getSidePlayer(BattleSide::ATTACKER);
+	}
+
 	winnerSide = result.winner;
+
 	this->remainingBattleQueriesCount = remainingBattleQueriesCount;
 }
 
-FinishingBattleHelper::FinishingBattleHelper()
-{
-	winnerHero = loserHero = nullptr;
-	winnerSide = 0;
-	remainingBattleQueriesCount = 0;
-}
+//FinishingBattleHelper::FinishingBattleHelper()
+//{
+//	winnerHero = loserHero = nullptr;
+//	winnerSide = 0;
+//	remainingBattleQueriesCount = 0;
+//}
 
-void BattleResultProcessor::endBattle(int3 tile, const CGHeroInstance * heroAttacker, const CGHeroInstance * heroDefender)
+void BattleResultProcessor::endBattle(const CBattleInfoCallback & battle)
 {
 	auto const & giveExp = [](BattleResult &r)
 	{
@@ -215,6 +228,10 @@ void BattleResultProcessor::endBattle(int3 tile, const CGHeroInstance * heroAtta
 
 	LOG_TRACE(logGlobal);
 
+	auto * battleResult = battleResults.at(battle.getBattle()->getBattleID()).get();
+	const auto * heroAttacker = battle.battleGetFightingHero(BattleSide::ATTACKER);
+	const auto * heroDefender = battle.battleGetFightingHero(BattleSide::DEFENDER);
+
 	//Fill BattleResult structure with exp info
 	giveExp(*battleResult);
 
@@ -231,11 +248,11 @@ void BattleResultProcessor::endBattle(int3 tile, const CGHeroInstance * heroAtta
 	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));
+	auto battleQuery = std::dynamic_pointer_cast<CBattleQuery>(gameHandler->queries->topQuery(battle.sideToPlayer(0)));
 	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!");
+		gameHandler->complain("Player " + boost::lexical_cast<std::string>(battle.sideToPlayer(0)) + " has no battle query at the top!");
 		return;
 	}
 
@@ -243,12 +260,14 @@ void BattleResultProcessor::endBattle(int3 tile, const CGHeroInstance * heroAtta
 
 	//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);
+
+	assert(finishingBattles.count(battle.getBattle()->getBattleID()) == 0);
+	finishingBattles[battle.getBattle()->getBattleID()] = std::make_unique<FinishingBattleHelper>(battle, *battleResult, queriedPlayers);
 
 	// in battles against neutrals, 1st player can ask to replay battle manually
-	if (!gameHandler->gameState()->curB->sides[1].color.isValidPlayer())
+	if (!battle.sideToPlayer(1).isValidPlayer())
 	{
-		auto battleDialogQuery = std::make_shared<CBattleDialogQuery>(gameHandler, gameHandler->gameState()->curB);
+		auto battleDialogQuery = std::make_shared<CBattleDialogQuery>(gameHandler, battle.getBattle());
 		battleResult->queryID = battleDialogQuery->queryID;
 		gameHandler->queries->addQuery(battleDialogQuery);
 	}
@@ -259,29 +278,35 @@ void BattleResultProcessor::endBattle(int3 tile, const CGHeroInstance * heroAtta
 	for(auto q : gameHandler->queries->allQueries())
 	{
 		auto otherBattleQuery = std::dynamic_pointer_cast<CBattleQuery>(q);
-		if(otherBattleQuery)
+		if(otherBattleQuery && otherBattleQuery->battleID == battle.getBattle()->getBattleID())
 			otherBattleQuery->result = battleQuery->result;
 	}
 
-	gameHandler->turnTimerHandler.onBattleEnd();
-	gameHandler->sendAndApply(battleResult.get()); //after this point casualties objects are destroyed
+	gameHandler->turnTimerHandler.onBattleEnd(battle.getBattle()->getBattleID());
+	gameHandler->sendAndApply(battleResult);
 
 	if (battleResult->queryID == QueryID::NONE)
-		endBattleConfirm(gameHandler->gameState()->curB);
+		endBattleConfirm(battle);
 }
 
-void BattleResultProcessor::endBattleConfirm(const BattleInfo * battleInfo)
+void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle)
 {
-	auto battleQuery = std::dynamic_pointer_cast<CBattleQuery>(gameHandler->queries->topQuery(battleInfo->sides.at(0).color));
+	auto battleQuery = std::dynamic_pointer_cast<CBattleQuery>(gameHandler->queries->topQuery(battle.sideToPlayer(0)));
 	if(!battleQuery)
 	{
 		logGlobal->trace("No battle query, battle end was confirmed by another player");
 		return;
 	}
 
-	const EBattleResult result = battleResult.get()->result;
+	auto * battleResult = battleResults.at(battle.getBattle()->getBattleID()).get();
+	auto * finishingBattle = finishingBattles.at(battle.getBattle()->getBattleID()).get();
+
+	const EBattleResult result = battleResult->result;
+
+	//calculate casualties before deleting battle
+	CasualtiesAfterBattle cab1(battle, BattleSide::ATTACKER);
+	CasualtiesAfterBattle cab2(battle, BattleSide::DEFENDER);
 
-	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)
@@ -289,7 +314,7 @@ void BattleResultProcessor::endBattleConfirm(const BattleInfo * battleInfo)
 		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)
+			for(auto & spellId : battle.getBattle()->getUsedSpells(battle.otherSide(battleResult->winner)))
 			{
 				auto spell = spellId.toSpell(VLC->spells());
 				if(spell && spell->getLevel() <= eagleEyeLevel && !finishingBattle->winnerHero->spellbookContainsSpell(spell->getId()) && gameHandler->getRandomGenerator().nextInt(99) < eagleEyeChance)
@@ -357,7 +382,10 @@ void BattleResultProcessor::endBattleConfirm(const BattleInfo * battleInfo)
 				}
 			}
 		}
-		for (auto armySlot : battleInfo->sides.at(!battleResult->winner).armyObject->stacks)
+
+		auto loser = battle.otherSide(battleResult->winner);
+
+		for (auto armySlot : battle.battleGetArmyObject(loser)->stacks)
 		{
 			auto artifactsWorn = armySlot.second->artifactsWorn;
 			for (auto artSlot : artifactsWorn)
@@ -458,10 +486,11 @@ void BattleResultProcessor::endBattleConfirm(const BattleInfo * battleInfo)
 		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.battleID = battle.getBattle()->getBattleID();
+	raccepted.heroResult[0].army = const_cast<CArmedInstance*>(battle.battleGetArmyObject(0));
+	raccepted.heroResult[1].army = const_cast<CArmedInstance*>(battle.battleGetArmyObject(1));
+	raccepted.heroResult[0].hero = const_cast<CGHeroInstance*>(battle.battleGetFightingHero(0));
+	raccepted.heroResult[1].hero = const_cast<CGHeroInstance*>(battle.battleGetFightingHero(1));
 	raccepted.heroResult[0].exp = battleResult->exp[0];
 	raccepted.heroResult[1].exp = battleResult->exp[1];
 	raccepted.winnerSide = finishingBattle->winnerSide; 
@@ -471,13 +500,16 @@ void BattleResultProcessor::endBattleConfirm(const BattleInfo * battleInfo)
 	//--> continuation (battleAfterLevelUp) occurs after level-up gameHandler->queries are handled or on removing query
 }
 
-void BattleResultProcessor::battleAfterLevelUp(const BattleResult &result)
+void BattleResultProcessor::battleAfterLevelUp(const BattleID & battleID, const BattleResult & result)
 {
 	LOG_TRACE(logGlobal);
 
-	if(!finishingBattle)
+	assert(finishingBattles.count(battleID) != 0);
+	if(finishingBattles.count(battleID) == 0)
 		return;
 
+	auto & finishingBattle = finishingBattles[battleID];
+
 	finishingBattle->remainingBattleQueriesCount--;
 	logGlobal->trace("Decremented gameHandler->queries count to %d", finishingBattle->remainingBattleQueriesCount);
 
@@ -490,7 +522,7 @@ void BattleResultProcessor::battleAfterLevelUp(const BattleResult &result)
 	// Still, it looks like a hole.
 
 	// Necromancy if applicable.
-	const CStackBasicDescriptor raisedStack = finishingBattle->winnerHero ? finishingBattle->winnerHero->calculateNecromancy(*battleResult) : CStackBasicDescriptor();
+	const CStackBasicDescriptor raisedStack = finishingBattle->winnerHero ? finishingBattle->winnerHero->calculateNecromancy(result) : 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();
@@ -502,12 +534,11 @@ void BattleResultProcessor::battleAfterLevelUp(const BattleResult &result)
 	}
 
 	BattleResultsApplied resultsApplied;
+	resultsApplied.battleID = battleID;
 	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);
@@ -528,25 +559,30 @@ void BattleResultProcessor::battleAfterLevelUp(const BattleResult &result)
 			gameHandler->heroPool->onHeroEscaped(finishingBattle->victor, finishingBattle->winnerHero);
 	}
 
-	finishingBattle.reset();
-	battleResult.reset();
+	finishingBattles.erase(battleID);
+	battleResults.erase(battleID);
 }
 
-void BattleResultProcessor::setBattleResult(EBattleResult resultType, int victoriusSide)
+void BattleResultProcessor::setBattleResult(const CBattleInfoCallback & battle, EBattleResult resultType, int victoriusSide)
 {
-	battleResult = std::make_unique<BattleResult>();
+	assert(battleResults.count(battle.getBattle()->getBattleID()) == 0);
+
+	battleResults[battle.getBattle()->getBattleID()] = std::make_unique<BattleResult>();
+
+	auto & battleResult = battleResults[battle.getBattle()->getBattleID()];
+	battleResult->battleID = battle.getBattle()->getBattleID();
 	battleResult->result = resultType;
 	battleResult->winner = victoriusSide; //surrendering side loses
-	gameHandler->gameState()->curB->calculateCasualties(battleResult->casualties);
-}
 
-void BattleResultProcessor::setupBattle()
-{
-	finishingBattle.reset();
-	battleResult.reset();
+	for(const auto & st : battle.battleGetAllStacks(true)) //setting casualties
+	{
+		si32 killed = st->getKilled();
+		if(killed > 0)
+			battleResult->casualties[st->unitSide()][st->creatureId()] += killed;
+	}
 }
 
-bool BattleResultProcessor::battleIsEnding() const
+bool BattleResultProcessor::battleIsEnding(const CBattleInfoCallback & battle) const
 {
-	return battleResult != nullptr;
+	return battleResults.count(battle.getBattle()->getBattleID()) != 0;
 }

+ 10 - 11
server/battles/BattleResultProcessor.h

@@ -31,14 +31,14 @@ struct CasualtiesAfterBattle
 	TSummoned summoned;
 	ObjectInstanceID heroWithDeadCommander; //TODO: unify stack locations
 
-	CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat);
+	CasualtiesAfterBattle(const CBattleInfoCallback & battle, uint8_t sideInBattle);
 	void updateArmy(CGameHandler * gh);
 };
 
 struct FinishingBattleHelper
 {
-	FinishingBattleHelper();
-	FinishingBattleHelper(std::shared_ptr<const CBattleQuery> Query, int RemainingBattleQueriesCount);
+//	FinishingBattleHelper();
+	FinishingBattleHelper(const CBattleInfoCallback & battle, const BattleResult & result, int RemainingBattleQueriesCount);
 
 	inline bool isDraw() const {return winnerSide == 2;}
 
@@ -64,18 +64,17 @@ class BattleResultProcessor : boost::noncopyable
 	//	BattleProcessor * owner;
 	CGameHandler * gameHandler;
 
-	std::unique_ptr<BattleResult> battleResult;
-	std::unique_ptr<FinishingBattleHelper> finishingBattle;
+	std::map<BattleID, std::unique_ptr<BattleResult>> battleResults;
+	std::map<BattleID, std::unique_ptr<FinishingBattleHelper>> finishingBattles;
 
 public:
 	explicit BattleResultProcessor(BattleProcessor * owner);
 	void setGameHandler(CGameHandler * newGameHandler);
 
-	bool battleIsEnding() const;
+	bool battleIsEnding(const CBattleInfoCallback & battle) const;
 
-	void setupBattle();
-	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);
+	void setBattleResult(const CBattleInfoCallback & battle, EBattleResult resultType, int victoriusSide);
+	void endBattle(const CBattleInfoCallback & battle); //ends battle
+	void endBattleConfirm(const CBattleInfoCallback & battle);
+	void battleAfterLevelUp(const BattleID & battleID, const BattleResult & result);
 };

+ 0 - 0
server/battles/ServerBattleCallback.cpp


+ 0 - 0
server/battles/ServerBattleCallback.h


+ 39 - 18
server/queries/BattleQueries.cpp

@@ -10,32 +10,34 @@
 #include "StdInc.h"
 #include "BattleQueries.h"
 #include "MapQueries.h"
+#include "QueriesProcessor.h"
 
 #include "../CGameHandler.h"
 #include "../battles/BattleProcessor.h"
 
-#include "../../lib/battle/BattleInfo.h"
+#include "../../lib/battle/IBattleState.h"
 
 void CBattleQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const
 {
+	assert(result);
+
 	if(result)
 		objectVisit.visitedObject->battleFinished(objectVisit.visitingHero, *result);
 }
 
-CBattleQuery::CBattleQuery(CGameHandler * owner, const BattleInfo * Bi):
-	CGhQuery(owner)
+CBattleQuery::CBattleQuery(CGameHandler * owner, const IBattleInfo * bi):
+	CGhQuery(owner),
+	battleID(bi->getBattleID())
 {
-	belligerents[0] = Bi->sides[0].armyObject;
-	belligerents[1] = Bi->sides[1].armyObject;
+	belligerents[0] = bi->getSideArmy(0);
+	belligerents[1] = bi->getSideArmy(1);
 
-	bi = Bi;
-
-	for(auto & side : bi->sides)
-		addPlayer(side.color);
+	addPlayer(bi->getSidePlayer(0));
+	addPlayer(bi->getSidePlayer(1));
 }
 
 CBattleQuery::CBattleQuery(CGameHandler * owner):
-	CGhQuery(owner), bi(nullptr)
+	CGhQuery(owner)
 {
 	belligerents[0] = belligerents[1] = nullptr;
 }
@@ -48,17 +50,27 @@ bool CBattleQuery::blocksPack(const CPack * pack) const
 
 void CBattleQuery::onRemoval(PlayerColor color)
 {
+	assert(result);
+
 	if(result)
-		gh->battles->battleAfterLevelUp(*result);
+		gh->battles->battleAfterLevelUp(battleID, *result);
 }
 
-CBattleDialogQuery::CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi):
-	CDialogQuery(owner)
+void CBattleQuery::onExposure(QueryPtr topQuery)
 {
-	bi = Bi;
+	// this method may be called in two cases:
+	// 1) when requesting battle replay (but before replay starts -> no valid result)
+	// 2) when aswering on levelup queries after accepting battle result -> valid result
+	if(result)
+		owner->popQuery(*this);
+}
 
-	for(auto & side : bi->sides)
-		addPlayer(side.color);
+CBattleDialogQuery::CBattleDialogQuery(CGameHandler * owner, const IBattleInfo * bi):
+	CDialogQuery(owner),
+	bi(bi)
+{
+	addPlayer(bi->getSidePlayer(0));
+	addPlayer(bi->getSidePlayer(1));
 }
 
 void CBattleDialogQuery::onRemoval(PlayerColor color)
@@ -66,10 +78,19 @@ 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);
+		gh->battles->restartBattlePrimary(
+			bi->getBattleID(),
+			bi->getSideArmy(0),
+			bi->getSideArmy(1),
+			bi->getLocation(),
+			bi->getSideHero(0),
+			bi->getSideHero(1),
+			bi->isCreatureBank(),
+			bi->getDefendedTown()
+		);
 	}
 	else
 	{
-		gh->battles->endBattleConfirm(bi);
+		gh->battles->endBattleConfirm(bi->getBattleID());
 	}
 }

+ 9 - 4
server/queries/BattleQueries.h

@@ -13,28 +13,33 @@
 
 #include "../../lib/NetPacks.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+class IBattleInfo;
+VCMI_LIB_NAMESPACE_END
+
 class CBattleQuery : public CGhQuery
 {
 public:
 	std::array<const CArmedInstance *,2> belligerents;
 	std::array<int, 2> initialHeroMana;
 
-	const BattleInfo *bi;
+	BattleID battleID;
 	std::optional<BattleResult> result;
 
 	CBattleQuery(CGameHandler * owner);
-	CBattleQuery(CGameHandler * owner, const BattleInfo * Bi); //TODO
+	CBattleQuery(CGameHandler * owner, const IBattleInfo * Bi); //TODO
 	virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override;
 	virtual bool blocksPack(const CPack *pack) const override;
 	virtual void onRemoval(PlayerColor color) override;
+	virtual void onExposure(QueryPtr topQuery) override;
 };
 
 class CBattleDialogQuery : public CDialogQuery
 {
 public:
-	CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi);
+	CBattleDialogQuery(CGameHandler * owner, const IBattleInfo * Bi);
 
-	const BattleInfo * bi;
+	const IBattleInfo * bi;
 
 	virtual void onRemoval(PlayerColor color) override;
 };

+ 9 - 3
test/battle/CBattleInfoCallbackTest.cpp

@@ -124,6 +124,7 @@ public:
 	{
 	public:
 
+		const IBattleInfo * battle;
 #if SCRIPTING_ENABLED
 		scripting::Pool * pool;
 
@@ -137,9 +138,14 @@ public:
 		{
 		}
 
-		void setBattle(const IBattleInfo * battleInfo)
+		const IBattleInfo * getBattle() const override
 		{
-			CBattleInfoCallback::setBattle(battleInfo);
+			return battle;
+		}
+
+		std::optional<PlayerColor> getPlayerID() const override
+		{
+			return std::nullopt;
 		}
 
 #if SCRIPTING_ENABLED
@@ -170,7 +176,7 @@ public:
 
 	void startBattle()
 	{
-		subject.setBattle(&battleMock);
+		subject.battle = &battleMock;
 	}
 
 	void redirectUnitsToFake()

+ 12 - 12
test/game/CGameStateTest.cpp

@@ -204,9 +204,9 @@ public:
 
 		BattleStart bs;
 		bs.info = battle;
-		ASSERT_EQ(gameState->curB, nullptr);
+		ASSERT_EQ(gameState->currentBattles.size(), 0);
 		gameCallback->sendAndApply(&bs);
-		ASSERT_EQ(gameState->curB, battle);
+		ASSERT_EQ(gameState->currentBattles.size(), 1);
 	}
 
 	std::shared_ptr<CGameState> gameState;
@@ -247,11 +247,11 @@ TEST_F(CGameStateTest, issue2765)
 
 	{
 		battle::UnitInfo info;
-		info.id = gameState->curB->battleNextUnitId();
+		info.id = gameState->currentBattles.front()->battleNextUnitId();
 		info.count = 1;
 		info.type = CreatureID(69);
 		info.side = BattleSide::ATTACKER;
-		info.position = gameState->curB->getAvaliableHex(info.type, info.side);
+		info.position = gameState->currentBattles.front()->getAvaliableHex(info.type, info.side);
 		info.summoned = false;
 
 		BattleUnitsChanged pack;
@@ -263,7 +263,7 @@ TEST_F(CGameStateTest, issue2765)
 	const CStack * att = nullptr;
 	const CStack * def = nullptr;
 
-	for(const CStack * s : gameState->curB->stacks)
+	for(const CStack * s : gameState->currentBattles.front()->stacks)
 	{
 		if(s->unitType()->getId() == CreatureID::BALLISTA && s->unitSide() == BattleSide::DEFENDER)
 			def = s;
@@ -292,7 +292,7 @@ TEST_F(CGameStateTest, issue2765)
 		spells::AbilityCaster caster(att, 3);
 
 		//here tested ballista, but this applied to all war machines
-		spells::BattleCast cast(gameState->curB, &caster, spells::Mode::PASSIVE, age);
+		spells::BattleCast cast(gameState->currentBattles.front().get(), &caster, spells::Mode::PASSIVE, age);
 
 		spells::Target target;
 		target.emplace_back(def);
@@ -339,7 +339,7 @@ TEST_F(CGameStateTest, battleResurrection)
 
 	startTestBattle(attacker, defender);
 
-	uint32_t unitId = gameState->curB->battleNextUnitId();
+	uint32_t unitId = gameState->currentBattles.front()->battleNextUnitId();
 
 	{
 		battle::UnitInfo info;
@@ -347,7 +347,7 @@ TEST_F(CGameStateTest, battleResurrection)
 		info.count = 10;
 		info.type = CreatureID(13);
 		info.side = BattleSide::ATTACKER;
-		info.position = gameState->curB->getAvaliableHex(info.type, info.side);
+		info.position = gameState->currentBattles.front()->getAvaliableHex(info.type, info.side);
 		info.summoned = false;
 
 		BattleUnitsChanged pack;
@@ -358,11 +358,11 @@ TEST_F(CGameStateTest, battleResurrection)
 
 	{
 		battle::UnitInfo info;
-		info.id = gameState->curB->battleNextUnitId();
+		info.id = gameState->currentBattles.front()->battleNextUnitId();
 		info.count = 10;
 		info.type = CreatureID(13);
 		info.side = BattleSide::DEFENDER;
-		info.position = gameState->curB->getAvaliableHex(info.type, info.side);
+		info.position = gameState->currentBattles.front()->getAvaliableHex(info.type, info.side);
 		info.summoned = false;
 
 		BattleUnitsChanged pack;
@@ -371,7 +371,7 @@ TEST_F(CGameStateTest, battleResurrection)
 		gameCallback->sendAndApply(&pack);
 	}
 
-	CStack * unit = gameState->curB->getStack(unitId);
+	CStack * unit = gameState->currentBattles.front()->getStack(unitId);
 
 	ASSERT_NE(unit, nullptr);
 
@@ -390,7 +390,7 @@ TEST_F(CGameStateTest, battleResurrection)
 		const CSpell * spell = SpellID(SpellID::RESURRECTION).toSpell();
 		ASSERT_NE(spell, nullptr);
 
-			spells::BattleCast cast(gameState->curB, attacker, spells::Mode::HERO, spell);
+			spells::BattleCast cast(gameState->currentBattles.front().get(), attacker, spells::Mode::HERO, spell);
 
 		spells::Target target;
 		target.emplace_back(unit);

+ 0 - 1
test/mock/BattleFake.cpp

@@ -87,7 +87,6 @@ BattleFake::BattleFake() = default;
 
 void BattleFake::setUp()
 {
-	CBattleInfoCallback::setBattle(this);
 }
 
 void BattleFake::setupEmptyBattlefield()

+ 11 - 0
test/mock/BattleFake.h

@@ -88,6 +88,17 @@ public:
 		pack->applyBattle(this);
 	}
 
+	const IBattleInfo * getBattle() const override
+	{
+		return nullptr;
+	}
+
+	std::optional<PlayerColor> getPlayerID() const override
+	{
+		return std::nullopt;
+	}
+
+
 protected:
 
 private:

+ 1 - 1
test/mock/mock_Environment.h

@@ -16,7 +16,7 @@ class EnvironmentMock : public Environment
 {
 public:
 	MOCK_CONST_METHOD0(services, const Services *());
-	MOCK_CONST_METHOD0(battle, const BattleCb *());
+	MOCK_CONST_METHOD1(battle, const BattleCb *(const BattleID & battleID));
 	MOCK_CONST_METHOD0(game, const GameCb *());
 	MOCK_CONST_METHOD0(logger, ::vstd::CLoggerBase *());
 	MOCK_CONST_METHOD0(eventBus, ::events::EventBus *());

+ 2 - 0
test/mock/mock_IBattleInfoCallback.h

@@ -36,6 +36,8 @@ public:
 	MOCK_CONST_METHOD0(battleActiveUnit, const battle::Unit *());
 
 	MOCK_CONST_METHOD0(getBonusBearer, IBonusBearer*());
+	MOCK_CONST_METHOD0(getBattle, IBattleInfo*());
+	MOCK_CONST_METHOD0(getPlayerID, std::optional<PlayerColor>());
 
 	MOCK_CONST_METHOD2(battleGetAllObstaclesOnPos, std::vector<std::shared_ptr<const CObstacleInstance>>(BattleHex, bool));
 	MOCK_CONST_METHOD2(getAllAffectedObstaclesByStack, std::vector<std::shared_ptr<const CObstacleInstance>>(const battle::Unit *, const std::set<BattleHex> &));

+ 1 - 0
test/mock/mock_IGameInfoCallback.h

@@ -22,6 +22,7 @@ public:
 	//player
 	MOCK_CONST_METHOD1(getPlayer, const Player *(PlayerColor));
 	MOCK_CONST_METHOD0(getLocalPlayer, PlayerColor());
+	MOCK_CONST_METHOD0(getPlayerID, std::optional<PlayerColor>());
 
 	//hero
 	MOCK_CONST_METHOD1(getHero, const CGHeroInstance *(ObjectInstanceID));

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác