Browse Source

Feature: Army Management Shortcuts should work as in HD+ Mod

Dmitry Orlov 3 years ago
parent
commit
8cae3398ba

+ 29 - 0
CCallback.cpp

@@ -109,6 +109,7 @@ int CCallback::mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, S
 	sendRequest(&pack);
 	return 0;
 }
+
 int CCallback::splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val)
 {
 	ArrangeStacks pack(3,p1,p2,s1->id,s2->id,val);
@@ -116,6 +117,34 @@ int CCallback::splitStack(const CArmedInstance *s1, const CArmedInstance *s2, Sl
 	return 0;
 }
 
+int CCallback::bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot)
+{
+	BulkMoveArmy pack(srcArmy, destArmy, srcSlot);
+	sendRequest(&pack);
+	return 0;
+}
+
+int CCallback::bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany)
+{
+	BulkSplitStack pack(armyId, srcSlot, howMany);
+	sendRequest(&pack);
+	return 0;
+}
+
+int CCallback::bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot)
+{
+	BulkSmartSplitStack pack(armyId, srcSlot);
+	sendRequest(&pack);
+	return 0;
+}
+
+int CCallback::bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot)
+{
+	BulkMergeStacks pack(armyId, srcSlot);
+	sendRequest(&pack);
+	return 0;
+}
+
 bool CCallback::dismissHero(const CGHeroInstance *hero)
 {
 	if(player!=hero->tempOwner) return false;

+ 13 - 1
CCallback.h

@@ -78,6 +78,12 @@ public:
 	virtual void save(const std::string &fname) = 0;
 	virtual void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) = 0;
 	virtual void buildBoat(const IShipyard *obj) = 0;
+
+	// To implement high-level army management bulk actions
+	virtual int bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) = 0;
+	virtual int bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany = 1) = 0;
+	virtual int bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot) = 0;
+	virtual int bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) = 0;
 };
 
 struct CPackForServer;
@@ -99,7 +105,9 @@ public:
 	friend class CClient;
 };
 
-class CCallback : public CPlayerSpecificInfoCallback, public IGameActionCallback, public CBattleCallback
+class CCallback : public CPlayerSpecificInfoCallback,
+	public IGameActionCallback,
+	public CBattleCallback
 {
 public:
 	CCallback(CGameState * GS, boost::optional<PlayerColor> Player, CClient *C);
@@ -125,6 +133,10 @@ public:
 	int mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; //first goes to the second
 	int mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; //first goes to the second
 	int splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val) override;
+	int bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) override;
+	int bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany = 1) override;
+	int bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot) override;
+	int bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) override;
 	bool dismissHero(const CGHeroInstance * hero) override;
 	bool swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2) override;
 	bool assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) override;

+ 1 - 1
README.md

@@ -35,4 +35,4 @@ Platform support is constantly tested by continuous integration and CMake config
 VCMI Project source code is licensed under GPL version 2 or later.
 VCMI Project assets are licensed under CC-BY-SA 4.0. Assets sources and information about contributors are available under following link: [https://github.com/vcmi/vcmi-assets]
 
-Copyright (C) 2007-2020  VCMI Team (check AUTHORS file for the contributors list)
+Copyright (C) 2007-2022  VCMI Team (check AUTHORS file for the contributors list)

+ 24 - 0
client/NetPacksClient.cpp

@@ -238,6 +238,30 @@ void RebalanceStacks::applyCl(CClient * cl)
 	dispatchGarrisonChange(cl, srcArmy, dstArmy);
 }
 
+void BulkRebalanceStacks::applyCl(CClient * cl)
+{
+	if(!moves.empty())
+	{
+		auto destArmy = moves[0].srcArmy == moves[0].dstArmy
+			? ObjectInstanceID()
+			: moves[0].dstArmy;
+		dispatchGarrisonChange(cl, moves[0].srcArmy, destArmy);
+	}
+}
+
+void BulkSmartRebalanceStacks::applyCl(CClient * cl)
+{
+	if(!moves.empty())
+	{
+		assert(moves[0].srcArmy == moves[0].dstArmy);
+		dispatchGarrisonChange(cl, moves[0].srcArmy, ObjectInstanceID());
+	}
+	else if(!changes.empty())
+	{
+		dispatchGarrisonChange(cl, changes[0].army, ObjectInstanceID());
+	}
+}
+
 void PutArtifact::applyCl(CClient *cl)
 {
 	callInterfaceIfPresent(cl, al.owningPlayer(), &IGameEventsReceiver::artifactPut, al);

+ 200 - 39
client/widgets/CGarrisonInt.cpp

@@ -74,17 +74,22 @@ void CGarrisonSlot::hover (bool on)
 			}
 			else
 			{
-				if(upg == EGarrisonType::UP)
+				const bool isHeroOnMap = owner->armedObjs[0] // Hero is not a visitor and not a garrison defender
+					&& owner->armedObjs[0]->ID == Obj::HERO
+					&& (!owner->armedObjs[1] || owner->armedObjs[1]->ID == Obj::HERO) // one hero or we are in the Heroes exchange window
+					&& !(static_cast<const CGHeroInstance*>(owner->armedObjs[0]))->inTownGarrison;
+
+				if(isHeroOnMap)
 				{
-					temp = CGI->generaltexth->tcommands[12]; //Select %s (in garrison)
+					temp = CGI->generaltexth->allTexts[481]; //Select %s
 				}
-				else if(owner->armedObjs[0] && (owner->armedObjs[0]->ID == Obj::TOWN || owner->armedObjs[0]->ID == Obj::HERO))
+				else if(upg == EGarrisonType::UP)
 				{
-					temp = CGI->generaltexth->tcommands[32]; //Select %s (visiting)
+					temp = CGI->generaltexth->tcommands[12]; //Select %s (in garrison)
 				}
-				else
+				else // Hero is visiting some object (town, mine, etc)
 				{
-					temp = CGI->generaltexth->allTexts[481]; //Select %s
+					temp = CGI->generaltexth->tcommands[32]; //Select %s (visiting)
 				}
 				boost::algorithm::replace_first(temp,"%s",creature->nameSing);
 			}
@@ -140,6 +145,18 @@ bool CGarrisonSlot::ally() const
 	return PlayerRelations::ALLIES == LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, getObj()->tempOwner);
 }
 
+std::function<void()> CGarrisonSlot::getDismiss() const
+{
+	const bool canDismiss = getObj()->tempOwner == LOCPLINT->playerID
+		&& (getObj()->stacksCount() > 1 ||
+			!getObj()->needsLastStack());
+
+	return canDismiss ? [=]()
+	{
+		LOCPLINT->cb->dismissCreature(getObj(), ID);
+	} : (std::function<void()>)nullptr;
+}
+
 /// The creature slot has been clicked twice, therefore the creature info should be shown
 /// @return Whether the view should be refreshed
 bool CGarrisonSlot::viewInfo()
@@ -148,11 +165,9 @@ bool CGarrisonSlot::viewInfo()
 	LOCPLINT->cb->getUpgradeInfo(getObj(), ID, pom);
 
 	bool canUpgrade = getObj()->tempOwner == LOCPLINT->playerID && pom.oldID>=0; //upgrade is possible
-	bool canDismiss = getObj()->tempOwner == LOCPLINT->playerID && (getObj()->stacksCount()>1  || !getObj()->needsLastStack());
 	std::function<void(CreatureID)> upgr = nullptr;
-	std::function<void()> dism = nullptr;
+	auto dism = getDismiss();
 	if(canUpgrade) upgr = [=] (CreatureID newID) { LOCPLINT->cb->upgradeCreature(getObj(), ID, newID); };
-	if(canDismiss) dism = [=](){ LOCPLINT->cb->dismissCreature(getObj(), ID); };
 
 	owner->selectSlot(nullptr);
 	owner->setSplittingMode(false);
@@ -288,13 +303,17 @@ void CGarrisonSlot::clickLeft(tribool down, bool previousState)
 	{
 		bool refr = false;
 		const CGarrisonSlot * selection = owner->getSelection();
+
 		if(!selection)
 		{
-			refr = highlightOrDropArtifact();
+			refr = highlightOrDropArtifact(); // Affects selection
 			handleSplittingShortcuts();
 		}
 		else if(selection == this)
-			refr = viewInfo();
+		{
+			if(!handleSplittingShortcuts())
+				refr = viewInfo(); // Affects selection
+		}
 		// Re-highlight if troops aren't removable or not ours.
 		else if (mustForceReselection())
 		{
@@ -403,34 +422,67 @@ CGarrisonSlot::CGarrisonSlot(CGarrisonInt * Owner, int x, int y, SlotID IID, CGa
 	update();
 }
 
-void CGarrisonSlot::splitIntoParts(CGarrisonSlot::EGarrisonType type, int amount, int maxOfSplittedSlots)
+void CGarrisonSlot::splitIntoParts(CGarrisonSlot::EGarrisonType type, int amount)
 {
+	auto empty = owner->getEmptySlot(type);
+
+	if(empty == SlotID())
+		return;
+
 	owner->pb = type;
-	for(CGarrisonSlot * slot : owner->getEmptySlots(type))
-	{
-		owner->p2 = slot->ID;
-		owner->splitStacks(1, amount);
-		maxOfSplittedSlots--;
-		if(!maxOfSplittedSlots || owner->getSelection()->myStack->count <= 1)
-			break;
-	}
+	owner->p2 = empty;
+	owner->splitStacks(1, amount);
 }
 
-void CGarrisonSlot::handleSplittingShortcuts()
+bool CGarrisonSlot::handleSplittingShortcuts()
 {
 	const Uint8 * state = SDL_GetKeyboardState(NULL);
-	if(owner->getSelection() && owner->getEmptySlots(owner->getSelection()->upg).size() && owner->getSelection()->myStack->count > 1)
+	const bool isAlt = !!state[SDL_SCANCODE_LALT];
+	const bool isLShift = !!state[SDL_SCANCODE_LSHIFT];
+	const bool isLCtrl = !!state[SDL_SCANCODE_LCTRL];
+
+	if(!isAlt && !isLShift && !isLCtrl)
+		return false; // This is only case when return false
+
+	auto selected = owner->getSelection();
+	if(!selected)
+		return true; // Some Shortcusts are pressed but there are no appropriate actions
+
+	auto units = selected->myStack->count;
+	if(units < 1)
+		return true;
+
+	if (isLShift && isLCtrl && isAlt)
 	{
-		if(state[SDL_SCANCODE_LCTRL] && state[SDL_SCANCODE_LSHIFT])
-			splitIntoParts(owner->getSelection()->upg, 1, 7);
-		else if(state[SDL_SCANCODE_LCTRL])
-			splitIntoParts(owner->getSelection()->upg, 1, 1);
-		else if(state[SDL_SCANCODE_LSHIFT])
-			splitIntoParts(owner->getSelection()->upg, owner->getSelection()->myStack->count/2 , 1);
+		owner->bulkMoveArmy(selected);
+	}
+	else if(isLCtrl && isAlt)
+	{
+		owner->moveStackToAnotherArmy(selected);
+	}
+	else if(isLShift && isAlt)
+	{
+		auto dismiss = getDismiss();
+		if(dismiss)
+			LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[12], dismiss, nullptr);
+	}
+	else if(isAlt)
+	{
+		owner->bulkMergeStacks(selected);
+	}
+	else
+	{
+		if(units <= 1)
+			return true;
+
+		if(isLCtrl && isLShift)
+			owner->bulkSplitStack(selected);
+		else if(isLShift)
+			owner->bulkSmartSplitStack(selected);
 		else
-			return;
-		owner->selectSlot(nullptr);
+			splitIntoParts(selected->upg, 1); // LCtrl
 	}
+	return true;
 }
 
 void CGarrisonInt::addSplitBtn(std::shared_ptr<CButton> button)
@@ -492,6 +544,115 @@ void CGarrisonInt::splitStacks(int, int amountRight)
 	LOCPLINT->cb->splitStack(armedObjs[getSelection()->upg], armedObjs[pb], getSelection()->ID, p2, amountRight);
 }
 
+bool CGarrisonInt::checkSelected(const CGarrisonSlot * selected, TQuantity min) const
+{
+	return selected && selected->myStack && selected->myStack->count > min && selected->creature;
+}
+
+void CGarrisonInt::moveStackToAnotherArmy(const CGarrisonSlot * selected)
+{
+	if(!checkSelected(selected))
+		return;
+
+	const auto srcArmyType = selected->upg;
+	const auto destArmyType = srcArmyType == CGarrisonSlot::UP
+		? CGarrisonSlot::DOWN
+		: CGarrisonSlot::UP;
+
+	auto srcArmy = armedObjs[srcArmyType];
+	auto destArmy = armedObjs[destArmyType];
+
+	if(!destArmy)
+		return;
+
+	auto destSlot = destArmy->getSlotFor(selected->creature);
+
+	if(destSlot == SlotID())
+		return;
+
+	const auto srcSlot = selected->ID;
+	const bool isDestSlotEmpty = !destArmy->getStackCount(destSlot);
+
+	if(isDestSlotEmpty && !destArmy->getStackCount(srcSlot))
+		destSlot = srcSlot; // Same place is more preferable
+
+	const bool isLastStack = srcArmy->stacksCount() == 1 && srcArmy->needsLastStack();
+	auto srcAmount = selected->myStack->count - (isLastStack ? 1 : 0);
+
+	if(!srcAmount)
+		return;
+
+	if(!isDestSlotEmpty || isLastStack)
+	{
+		srcAmount += destArmy->getStackCount(destSlot); // Due to 'split' implementation in the 'CGameHandler::arrangeStacks'
+		LOCPLINT->cb->splitStack(srcArmy, destArmy, srcSlot, destSlot, srcAmount);
+	}
+	else
+	{
+		LOCPLINT->cb->swapCreatures(srcArmy, destArmy, srcSlot, destSlot);
+	}
+}
+
+void CGarrisonInt::bulkMoveArmy(const CGarrisonSlot * selected)
+{
+	if(!checkSelected(selected))
+		return;
+
+	const auto srcArmyType = selected->upg;
+	const auto destArmyType = (srcArmyType == CGarrisonSlot::UP)
+		? CGarrisonSlot::DOWN
+		: CGarrisonSlot::UP;
+
+	auto srcArmy = armedObjs[srcArmyType];
+	auto destArmy = armedObjs[destArmyType];
+
+	if(!destArmy)
+		return;
+
+	const auto srcSlot = selected->ID;
+	LOCPLINT->cb->bulkMoveArmy(srcArmy->id, destArmy->id, srcSlot);
+}
+
+void CGarrisonInt::bulkMergeStacks(const CGarrisonSlot * selected)
+{
+	if(!checkSelected(selected))
+		return;
+
+	const auto type = selected->upg;
+
+	if(!armedObjs[type]->hasCreatureSlots(selected->creature, selected->ID))
+		return;
+
+	LOCPLINT->cb->bulkMergeStacks(armedObjs[type]->id, selected->ID);
+}
+
+void CGarrisonInt::bulkSplitStack(const CGarrisonSlot * selected)
+{
+	if(!checkSelected(selected, 1)) // check if > 1
+		return;
+
+	const auto type = selected->upg;
+
+	if(!hasEmptySlot(type))
+		return;
+
+	LOCPLINT->cb->bulkSplitStack(armedObjs[type]->id, selected->ID);
+}
+
+void CGarrisonInt::bulkSmartSplitStack(const CGarrisonSlot * selected)
+{
+	if(!checkSelected(selected, 1))
+		return;
+
+	const auto type = selected->upg;
+
+	// Do not disturb the server if the creature is already balanced
+	if(!hasEmptySlot(type) && armedObjs[type]->isCreatureBalanced(selected->creature))
+		return;
+
+	LOCPLINT->cb->bulkSmartSplitStack(armedObjs[type]->id, selected->ID);
+}
+
 CGarrisonInt::CGarrisonInt(int x, int y, int inx, const Point & garsOffset,
 		const CArmedInstance * s1, const CArmedInstance * s2,
 		bool _removableUnits, bool smallImgs, bool _twoRows)
@@ -513,7 +674,7 @@ CGarrisonInt::CGarrisonInt(int x, int y, int inx, const Point & garsOffset,
 	createSlots();
 }
 
-const CGarrisonSlot * CGarrisonInt::getSelection()
+const CGarrisonSlot * CGarrisonInt::getSelection() const
 {
 	return highlighted;
 }
@@ -554,15 +715,15 @@ bool CGarrisonInt::getSplittingMode()
 	return inSplittingMode;
 }
 
-std::vector<CGarrisonSlot *> CGarrisonInt::getEmptySlots(CGarrisonSlot::EGarrisonType type)
+SlotID CGarrisonInt::getEmptySlot(CGarrisonSlot::EGarrisonType type) const
 {
-	std::vector<CGarrisonSlot *> emptySlots;
-	for(auto slot : availableSlots)
-	{
-		if(type == slot->upg && ((slot->our() || slot->ally()) && slot->creature == nullptr))
-			emptySlots.push_back(slot.get());
-	}
-	return emptySlots;
+	assert(armedObjs[type]);
+	return armedObjs[type] ? armedObjs[type]->getFreeSlot() : SlotID();
+}
+
+bool CGarrisonInt::hasEmptySlot(CGarrisonSlot::EGarrisonType type) const
+{
+	return getEmptySlot(type) != SlotID();
 }
 
 void CGarrisonInt::setArmy(const CArmedInstance * army, bool bottomGarrison)

+ 14 - 4
client/widgets/CGarrisonInt.h

@@ -45,6 +45,8 @@ class CGarrisonSlot : public CIntObject
 	bool mustForceReselection() const;
 
 	void setHighlight(bool on);
+	std::function<void()> getDismiss() const;
+
 public:
 	virtual void hover (bool on) override; //call-in
 	const CArmedInstance * getObj() const;
@@ -55,8 +57,8 @@ public:
 	void update();
 	CGarrisonSlot(CGarrisonInt *Owner, int x, int y, SlotID IID, EGarrisonType Upg=EGarrisonType::UP, const CStackInstance * creature_ = nullptr);
 
-	void splitIntoParts(EGarrisonType type, int amount, int maxOfSplittedSlots);
-	void handleSplittingShortcuts();
+	void splitIntoParts(EGarrisonType type, int amount);
+	bool handleSplittingShortcuts(); /// Returns true when some shortcut is pressed, false otherwise
 
 	friend class CGarrisonInt;
 };
@@ -70,6 +72,8 @@ class CGarrisonInt :public CIntObject
 	std::vector<std::shared_ptr<CGarrisonSlot>> availableSlots;  ///< Slots of upper and lower garrison
 
 	void createSlots();
+	bool checkSelected(const CGarrisonSlot * selected, TQuantity min = 0) const;
+
 public:
 	int interx;  ///< Space between slots
 	Point garOffset;  ///< Offset between garrisons (not used if only one hero)
@@ -83,12 +87,13 @@ public:
 		 owned[2];        ///< player Owns up or down army ([0] upper, [1] lower)
 
 	void selectSlot(CGarrisonSlot * slot); ///< @param slot null = deselect
-	const CGarrisonSlot * getSelection();
+	const CGarrisonSlot * getSelection() const;
 
 	void setSplittingMode(bool on);
 	bool getSplittingMode();
 
-	std::vector<CGarrisonSlot *> getEmptySlots(CGarrisonSlot::EGarrisonType type);
+	bool hasEmptySlot(CGarrisonSlot::EGarrisonType type) const;
+	SlotID getEmptySlot(CGarrisonSlot::EGarrisonType type) const;
 
 	const CArmedInstance * armedObjs[2];  ///< [0] is upper, [1] is down
 
@@ -99,6 +104,11 @@ public:
 
 	void splitClick();  ///< handles click on split button
 	void splitStacks(int amountLeft, int amountRight);  ///< TODO: comment me
+	void moveStackToAnotherArmy(const CGarrisonSlot * selected);
+	void bulkMoveArmy(const CGarrisonSlot * selected);
+	void bulkMergeStacks(const CGarrisonSlot * selected); // Gather all creatures of selected type to the selected slot from other hero/garrison slots
+	void bulkSplitStack(const CGarrisonSlot * selected); // Used to separate one-creature troops from main stack
+	void bulkSmartSplitStack(const CGarrisonSlot * selected);
 
 	/// Constructor
 	/// @param x, y Position

+ 138 - 1
lib/CCreatureSet.cpp

@@ -23,6 +23,12 @@
 #include "serializer/JsonSerializeFormat.h"
 #include "NetPacksBase.h"
 
+
+bool CreatureSlotComparer::operator()(const TPairCreatureSlot & lhs, const TPairCreatureSlot & rhs)
+{
+	return lhs.first->getAIValue() < rhs.first->getAIValue(); // Descendant order sorting
+}
+
 const CStackInstance &CCreatureSet::operator[](SlotID slot) const
 {
 	auto i = stacks.find(slot);
@@ -72,7 +78,7 @@ SlotID CCreatureSet::getSlotFor(CreatureID creature, ui32 slotsAmount) const /*r
 
 SlotID CCreatureSet::getSlotFor(const CCreature *c, ui32 slotsAmount) const
 {
-	assert(c->valid());
+	assert(c && c->valid());
 	for(auto & elem : stacks)
 	{
 		assert(elem.second->type->valid());
@@ -84,6 +90,75 @@ SlotID CCreatureSet::getSlotFor(const CCreature *c, ui32 slotsAmount) const
 	return getFreeSlot(slotsAmount);
 }
 
+bool CCreatureSet::hasCreatureSlots(const CCreature * c, SlotID exclude) const
+{
+	assert(c && c->valid());
+	for(auto & elem : stacks) // elem is const
+	{
+		if(elem.first == exclude) // Check slot
+			continue;
+
+		if(!elem.second || !elem.second->type) // Check creature
+			continue;
+
+		assert(elem.second->type->valid());
+
+		if(elem.second->type == c)
+			return true;
+	}
+	return false;
+}
+
+std::vector<SlotID> CCreatureSet::getCreatureSlots(const CCreature * c, SlotID exclude, TQuantity ignoreAmount) const
+{
+	assert(c && c->valid());
+	std::vector<SlotID> result;
+
+	for(auto & elem : stacks)
+	{
+		if(elem.first == exclude)
+			continue;
+
+		if(!elem.second || !elem.second->type || elem.second->type != c)
+			continue;
+
+		if(elem.second->count == ignoreAmount || elem.second->count < 1)
+			continue;
+
+		assert(elem.second->type->valid());
+		result.push_back(elem.first);
+	}
+	return result;
+}
+
+bool CCreatureSet::isCreatureBalanced(const CCreature * c, TQuantity ignoreAmount) const
+{
+	assert(c && c->valid());
+	TQuantity max = 0;
+	TQuantity min = std::numeric_limits<TQuantity>::max();
+
+	for(auto & elem : stacks)
+	{
+		if(!elem.second || !elem.second->type || elem.second->type != c)
+			continue;
+
+		const auto count = elem.second->count;
+
+		if(count == ignoreAmount || count < 1)
+			continue;
+
+		assert(elem.second->type->valid());
+
+		if(count > max)
+			max = count;
+		if(count < min)
+			min = count;
+		if(max - min > 1)
+			return false;
+	}
+	return true;
+}
+
 SlotID CCreatureSet::getFreeSlot(ui32 slotsAmount) const
 {
 	for(ui32 i=0; i<slotsAmount; i++)
@@ -96,6 +171,68 @@ SlotID CCreatureSet::getFreeSlot(ui32 slotsAmount) const
 	return SlotID(); //no slot available
 }
 
+std::vector<SlotID> CCreatureSet::getFreeSlots(ui32 slotsAmount) const
+{
+	std::vector<SlotID> freeSlots;
+
+	for(ui32 i = 0; i < slotsAmount; i++)
+	{
+		auto slot = SlotID(i);
+
+		if(!vstd::contains(stacks, slot))
+			freeSlots.push_back(slot);
+	}
+	return freeSlots;
+}
+
+std::queue<SlotID> CCreatureSet::getFreeSlotsQueue(ui32 slotsAmount) const
+{
+	std::queue<SlotID> freeSlots;
+
+	for (ui32 i = 0; i < slotsAmount; i++)
+	{
+		auto slot = SlotID(i);
+
+		if(!vstd::contains(stacks, slot))
+			freeSlots.push(slot);
+	}
+	return freeSlots;
+}
+
+TMapCreatureSlot CCreatureSet::getCreatureMap() const
+{
+	TMapCreatureSlot creatureMap;
+	TMapCreatureSlot::key_compare keyComp = creatureMap.key_comp();
+
+	// https://stackoverflow.com/questions/97050/stdmap-insert-or-stdmap-find
+	// https://www.cplusplus.com/reference/map/map/key_comp/
+	for(auto pair : stacks)
+	{
+		auto creature = pair.second->type;
+		auto slot = pair.first;
+		TMapCreatureSlot::iterator lb = creatureMap.lower_bound(creature);
+
+		if(lb != creatureMap.end() && !(keyComp(creature, lb->first)))
+			continue;
+
+		creatureMap.insert(lb, TMapCreatureSlot::value_type(creature, slot));
+	}
+	return creatureMap;
+}
+
+TCreatureQueue CCreatureSet::getCreatureQueue(SlotID exclude) const
+{
+	TCreatureQueue creatureQueue;
+
+	for(auto pair : stacks)
+	{
+		if(pair.first == exclude)
+			continue;
+		creatureQueue.push(std::make_pair(pair.second->type, pair.first));
+	}
+	return creatureQueue;
+}
+
 TQuantity CCreatureSet::getStackCount(SlotID slot) const
 {
 	auto i = stacks.find(slot);

+ 25 - 1
lib/CCreatureSet.h

@@ -142,6 +142,20 @@ public:
 typedef std::map<SlotID, CStackInstance*> TSlots;
 typedef std::map<SlotID, std::pair<CreatureID, TQuantity>> TSimpleSlots;
 
+typedef std::pair<const CCreature*, SlotID> TPairCreatureSlot;
+typedef std::map<const CCreature*, SlotID> TMapCreatureSlot;
+
+struct DLL_LINKAGE CreatureSlotComparer
+{
+	bool operator()(const TPairCreatureSlot & lhs, const TPairCreatureSlot & rhs);
+};
+
+typedef std::priority_queue<
+	TPairCreatureSlot,
+	std::vector<TPairCreatureSlot>,
+	CreatureSlotComparer
+> TCreatureQueue;
+
 class IArmyDescriptor
 {
 public:
@@ -209,7 +223,17 @@ public:
 	SlotID findStack(const CStackInstance *stack) const; //-1 if none
 	SlotID getSlotFor(CreatureID creature, ui32 slotsAmount = GameConstants::ARMY_SIZE) const; //returns -1 if no slot available
 	SlotID getSlotFor(const CCreature *c, ui32 slotsAmount = GameConstants::ARMY_SIZE) const; //returns -1 if no slot available
-	SlotID getFreeSlot(ui32 slotsAmount = GameConstants::ARMY_SIZE) const;
+	bool hasCreatureSlots(const CCreature * c, SlotID exclude) const;
+	std::vector<SlotID> getCreatureSlots(const CCreature * c, SlotID exclude, TQuantity ignoreAmount = -1) const;
+	bool isCreatureBalanced(const CCreature* c, TQuantity ignoreAmount = 1) const; // Check if the creature is evenly distributed across slots
+
+	SlotID getFreeSlot(ui32 slotsAmount = GameConstants::ARMY_SIZE) const; //returns first free slot
+	std::vector<SlotID> getFreeSlots(ui32 slotsAmount = GameConstants::ARMY_SIZE) const;
+	std::queue<SlotID> getFreeSlotsQueue(ui32 slotsAmount = GameConstants::ARMY_SIZE) const;
+
+	TMapCreatureSlot getCreatureMap() const;
+	TCreatureQueue getCreatureQueue(SlotID exclude) const;
+
 	bool mergableStacks(std::pair<SlotID, SlotID> &out, SlotID preferable = SlotID()) const; //looks for two same stacks, returns slot positions;
 	bool validTypes(bool allowUnrandomized = false) const; //checks if all types of creatures are set properly
 	bool slotEmpty(SlotID slot) const;

+ 127 - 1
lib/NetPacks.h

@@ -886,7 +886,7 @@ struct RebalanceStacks : CGarrisonOperationPack
 	TQuantity count;
 
 	void applyCl(CClient *cl);
-	DLL_LINKAGE void applyGs(CGameState *gs);
+	DLL_LINKAGE void applyGs(CGameState * gs);
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
@@ -898,6 +898,36 @@ struct RebalanceStacks : CGarrisonOperationPack
 	}
 };
 
+struct BulkRebalanceStacks : CGarrisonOperationPack
+{
+	std::vector<RebalanceStacks> moves;
+
+	void applyCl(CClient * cl);
+	DLL_LINKAGE void applyGs(CGameState * gs);
+
+	template <typename Handler> 
+	void serialize(Handler & h, const int version)
+	{
+		h & moves;
+	}
+};
+
+struct BulkSmartRebalanceStacks : CGarrisonOperationPack
+{
+	std::vector<RebalanceStacks> moves;
+	std::vector<ChangeStackCount> changes;
+
+	void applyCl(CClient * cl);
+	DLL_LINKAGE void applyGs(CGameState * gs);
+
+	template <typename Handler> 
+	void serialize(Handler & h, const int version)
+	{
+		h & moves;
+		h & changes;
+	}
+};
+
 struct GetEngagedHeroIds : boost::static_visitor<boost::optional<ObjectInstanceID>>
 {
 	boost::optional<ObjectInstanceID> operator()(const ConstTransitivePtr<CGHeroInstance> &h) const
@@ -1946,6 +1976,102 @@ struct ArrangeStacks : public CPackForServer
 	}
 };
 
+struct BulkMoveArmy : public CPackForServer
+{
+	SlotID srcSlot;
+	ObjectInstanceID srcArmy;
+	ObjectInstanceID destArmy;
+
+	BulkMoveArmy()
+	{};
+
+	BulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot)
+		: srcArmy(srcArmy), destArmy(destArmy), srcSlot(srcSlot)
+	{};
+
+	bool applyGh(CGameHandler * gh);
+
+	template <typename Handler>
+	void serialize(Handler & h, const int version)
+	{
+		h & static_cast<CPackForServer&>(*this);
+		h & srcSlot;
+		h & srcArmy;
+		h & destArmy;
+	}
+};
+
+struct BulkSplitStack : public CPackForServer
+{
+	SlotID src;
+	ObjectInstanceID srcOwner;
+	si32 amount;
+
+	BulkSplitStack() : amount(0)
+	{};
+
+	BulkSplitStack(ObjectInstanceID srcOwner, SlotID src, si32 howMany)
+		: src(src), srcOwner(srcOwner), amount(howMany) 
+	{};
+
+	bool applyGh(CGameHandler * gh);
+
+	template <typename Handler> 
+	void serialize(Handler & h, const int version)
+	{
+		h & static_cast<CPackForServer&>(*this);
+		h & src;
+		h & srcOwner;
+		h & amount;
+	}
+};
+
+struct BulkMergeStacks : public CPackForServer
+{
+	SlotID src;
+	ObjectInstanceID srcOwner;
+
+	BulkMergeStacks()
+	{};
+
+	BulkMergeStacks(ObjectInstanceID srcOwner, SlotID src)
+		: src(src), srcOwner(srcOwner)
+	{};
+
+	bool applyGh(CGameHandler * gh);
+
+	template <typename Handler>
+	void serialize(Handler & h, const int version)
+	{
+		h & static_cast<CPackForServer&>(*this);
+		h & src;
+		h & srcOwner;
+	}
+};
+
+struct BulkSmartSplitStack : public CPackForServer
+{
+	SlotID src;
+	ObjectInstanceID srcOwner;
+
+	BulkSmartSplitStack()
+	{};
+
+	BulkSmartSplitStack(ObjectInstanceID srcOwner, SlotID src)
+		: src(src), srcOwner(srcOwner)
+	{};
+
+	bool applyGh(CGameHandler * gh);
+
+	template <typename Handler>
+	void serialize(Handler & h, const int version)
+	{
+		h & static_cast<CPackForServer&>(*this);
+		h & src;
+		h & srcOwner;
+	}
+};
+
 struct DisbandCreature : public CPackForServer
 {
 	DisbandCreature(){};

+ 15 - 0
lib/NetPacksLib.cpp

@@ -1020,6 +1020,21 @@ DLL_LINKAGE void RebalanceStacks::applyGs(CGameState * gs)
 	CBonusSystemNode::treeHasChanged();
 }
 
+DLL_LINKAGE void BulkRebalanceStacks::applyGs(CGameState * gs)
+{
+	for(auto & move : moves)
+		move.applyGs(gs);
+}
+
+DLL_LINKAGE void BulkSmartRebalanceStacks::applyGs(CGameState * gs)
+{
+	for(auto & move : moves)
+		move.applyGs(gs);
+
+	for(auto & change : changes)
+		change.applyGs(gs);
+}
+
 DLL_LINKAGE void PutArtifact::applyGs(CGameState *gs)
 {
 	assert(art->canBePutAt(al));

+ 6 - 0
lib/registerTypes/RegisterTypes.h

@@ -317,6 +317,8 @@ void registerTypesClientPacks2(Serializer &s)
 
 	s.template registerType<CPackForClient, SaveGameClient>();
 	s.template registerType<CPackForClient, PlayerMessageClient>();
+	s.template registerType<CGarrisonOperationPack, BulkRebalanceStacks>();
+	s.template registerType<CGarrisonOperationPack, BulkSmartRebalanceStacks>();
 }
 
 template<typename Serializer>
@@ -347,6 +349,10 @@ void registerTypesServerPacks(Serializer &s)
 	s.template registerType<CPackForServer, CastleTeleportHero>();
 	s.template registerType<CPackForServer, SaveGame>();
 	s.template registerType<CPackForServer, PlayerMessage>();
+	s.template registerType<CPackForServer, BulkSplitStack>();
+	s.template registerType<CPackForServer, BulkMergeStacks>();
+	s.template registerType<CPackForServer, BulkSmartSplitStack>();
+	s.template registerType<CPackForServer, BulkMoveArmy>();
 }
 
 template<typename Serializer>

+ 265 - 5
server/CGameHandler.cpp

@@ -1629,6 +1629,9 @@ int CGameHandler::moveStack(int stack, BattleHex dest)
 
 CGameHandler::CGameHandler(CVCMIServer * lobby)
 	: lobby(lobby)
+	, complainNoCreatures("No creatures to split")
+	, complainNotEnoughCreatures("Cannot split that stack, not enough creatures!")
+	, complainInvalidSlot("Invalid slot accessed!")
 {
 	QID = 1;
 	IObjectInterface::cb = this;
@@ -2962,6 +2965,259 @@ void CGameHandler::load(const std::string & filename)
 	gs->updateOnLoad(lobby->si.get());
 }
 
+bool CGameHandler::bulkSplitStack(SlotID slotSrc, ObjectInstanceID srcOwner, si32 howMany)
+{
+	if(!slotSrc.validSlot() && complain(complainInvalidSlot))
+		return false;
+
+	const CArmedInstance * army = static_cast<const CArmedInstance*>(getObjInstance(srcOwner));
+	const CCreatureSet & creatureSet = *army;
+
+	if((!vstd::contains(creatureSet.stacks, slotSrc) && complain(complainNoCreatures))
+		|| (howMany < 1 && complain("Invalid split parameter!")))
+	{
+		return false;
+	}
+	auto actualAmount = army->getStackCount(slotSrc);
+
+	if(actualAmount <= howMany && complain(complainNotEnoughCreatures)) // '<=' because it's not intended just for moving a stack
+		return false;
+
+	auto freeSlots = creatureSet.getFreeSlots();
+
+	if(freeSlots.empty() && complain("No empty stacks"))
+		return false;
+
+	BulkRebalanceStacks bulkRS;
+
+	for(auto slot : freeSlots)
+	{
+		RebalanceStacks rs;
+		rs.srcArmy = army->id;
+		rs.dstArmy = army->id;
+		rs.srcSlot = slotSrc;
+		rs.dstSlot = slot;
+		rs.count = howMany;
+
+		bulkRS.moves.push_back(rs);
+		actualAmount -= howMany;
+
+		if(actualAmount <= howMany)
+			break;
+	}
+	sendAndApply(&bulkRS);
+	return true;
+}
+
+bool CGameHandler::bulkMergeStacks(SlotID slotSrc, ObjectInstanceID srcOwner)
+{
+	if(!slotSrc.validSlot() && complain(complainInvalidSlot))
+		return false;
+
+	const CArmedInstance * army = static_cast<const CArmedInstance*>(getObjInstance(srcOwner));
+	const CCreatureSet & creatureSet = *army;
+
+	if(!vstd::contains(creatureSet.stacks, slotSrc) && complain(complainNoCreatures))
+		return false;
+
+	auto actualAmount = creatureSet.getStackCount(slotSrc);
+
+	if(actualAmount < 1 && complain(complainNoCreatures))
+		return false;
+
+	auto currentCreature = creatureSet.getCreature(slotSrc);
+
+	if(!currentCreature && complain(complainNoCreatures))
+		return false;
+
+	auto creatureSlots = creatureSet.getCreatureSlots(currentCreature, slotSrc);
+
+	if(!creatureSlots.size())
+		return false;
+
+	BulkRebalanceStacks bulkRS;
+
+	for(auto slot : creatureSlots)
+	{
+		RebalanceStacks rs;
+		rs.srcArmy = army->id;
+		rs.dstArmy = army->id;
+		rs.srcSlot = slot;
+		rs.dstSlot = slotSrc;
+		rs.count = creatureSet.getStackCount(slot);
+		bulkRS.moves.push_back(rs);
+	}
+	sendAndApply(&bulkRS);
+	return true;
+}
+
+bool CGameHandler::bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot)
+{
+	if(!srcSlot.validSlot() && complain(complainInvalidSlot))
+		return false;
+
+	const CArmedInstance * armySrc = static_cast<const CArmedInstance*>(getObjInstance(srcArmy));
+	const CCreatureSet & setSrc = *armySrc;
+
+	if(!vstd::contains(setSrc.stacks, srcSlot) && complain(complainNoCreatures))
+		return false;
+
+	const CArmedInstance * armyDest = static_cast<const CArmedInstance*>(getObjInstance(destArmy));
+	const CCreatureSet & setDest = *armyDest;
+	auto freeSlots = setDest.getFreeSlotsQueue();
+
+	typedef std::map<SlotID, std::pair<SlotID, TQuantity>> TRebalanceMap;
+	TRebalanceMap moves;
+
+	auto srcQueue = setSrc.getCreatureQueue(srcSlot); // Exclude srcSlot, it should be moved last
+	auto slotsLeft = setSrc.stacksCount();
+	auto destMap = setDest.getCreatureMap();
+	TMapCreatureSlot::key_compare keyComp = destMap.key_comp();
+
+	while(!srcQueue.empty())
+	{
+		auto pair = srcQueue.top();
+		srcQueue.pop();
+
+		auto currCreature = pair.first;
+		auto currSlot = pair.second;
+		const auto quantity = setSrc.getStackCount(currSlot);
+
+		TMapCreatureSlot::iterator lb = destMap.lower_bound(currCreature);
+		const bool alreadyExists = (lb != destMap.end() && !(keyComp(currCreature, lb->first)));
+
+		if(!alreadyExists)
+		{
+			if(freeSlots.empty())
+				continue;
+
+			auto currFreeSlot = freeSlots.front();
+			freeSlots.pop();
+			destMap.insert(lb, TMapCreatureSlot::value_type(currCreature, currFreeSlot));
+		}
+		moves.insert(std::make_pair(currSlot, std::make_pair(destMap[currCreature], quantity)));
+		slotsLeft--;
+	}
+	if(slotsLeft == 1)
+	{
+		auto lastCreature = setSrc.getCreature(srcSlot);
+		auto slotToMove = SlotID();
+		// Try to find a slot for last creature
+		if(destMap.find(lastCreature) == destMap.end())
+		{
+			if(!freeSlots.empty())
+				slotToMove = freeSlots.front();
+		}
+		else
+		{
+			slotToMove = destMap[lastCreature];
+		}
+
+		if(slotToMove != SlotID())
+		{
+			const bool needsLastStack = armySrc->needsLastStack();
+			const auto quantity = setSrc.getStackCount(srcSlot) - (needsLastStack ? 1 : 0);
+			moves.insert(std::make_pair(srcSlot, std::make_pair(slotToMove, quantity)));
+		}
+	}
+	BulkRebalanceStacks bulkRS;
+
+	for(auto & move : moves)
+	{
+		RebalanceStacks rs;
+		rs.srcArmy = armySrc->id;
+		rs.dstArmy = armyDest->id;
+		rs.srcSlot = move.first;
+		rs.dstSlot = move.second.first;
+		rs.count = move.second.second;
+		bulkRS.moves.push_back(rs);
+	}
+	sendAndApply(&bulkRS);
+	return true;
+}
+
+bool CGameHandler::bulkSmartSplitStack(SlotID slotSrc, ObjectInstanceID srcOwner)
+{
+	if(!slotSrc.validSlot() && complain(complainInvalidSlot))
+		return false;
+
+	const CArmedInstance * army = static_cast<const CArmedInstance*>(getObjInstance(srcOwner));
+	const CCreatureSet & creatureSet = *army;
+
+	if(!vstd::contains(creatureSet.stacks, slotSrc) && complain(complainNoCreatures))
+		return false;
+
+	auto actualAmount = creatureSet.getStackCount(slotSrc);
+
+	if(actualAmount <= 1 && complain(complainNoCreatures))
+		return false;
+
+	auto freeSlot = creatureSet.getFreeSlot();
+	auto currentCreature = creatureSet.getCreature(slotSrc);
+
+	if(freeSlot == SlotID() && creatureSet.isCreatureBalanced(currentCreature))
+		return true;
+
+	auto creatureSlots = creatureSet.getCreatureSlots(currentCreature, SlotID(-1), 1); // Ignore slots where's only 1 creature, don't ignore slotSrc
+	TQuantity totalCreatures = 0;
+
+	for(auto slot : creatureSlots)
+		totalCreatures += creatureSet.getStackCount(slot);
+
+	if(totalCreatures <= 1 && complain("Total creatures number is invalid"))
+		return false;
+
+	if(freeSlot != SlotID())
+		creatureSlots.push_back(freeSlot);
+
+	if(creatureSlots.empty() && complain("No available slots for smart rebalancing"))
+		return false;
+
+	const auto totalCreatureSlots = creatureSlots.size();
+	const auto rem = totalCreatures % totalCreatureSlots;
+	const auto quotient = totalCreatures / totalCreatureSlots;
+
+	// totalCreatures == rem * (quotient + 1) + (totalCreatureSlots - rem) * quotient;
+	// Proof: r(q+1)+(s-r)q = rq+r+qs-rq = r+qs = total, where total/s = q+r/s
+
+	BulkSmartRebalanceStacks bulkSRS;
+
+	if(freeSlot != SlotID())
+	{
+		RebalanceStacks rs;
+		rs.srcArmy = rs.dstArmy = army->id;
+		rs.srcSlot = slotSrc;
+		rs.dstSlot = freeSlot;
+		rs.count = 1;
+		bulkSRS.moves.push_back(rs);
+	}
+	auto currSlot = 0;
+	auto check = 0;
+
+	for(auto slot : creatureSlots)
+	{
+		ChangeStackCount csc;
+
+		csc.army = army->id;
+		csc.slot = slot;
+		csc.count = (currSlot < rem)
+			? quotient + 1
+			: quotient;
+		csc.absoluteValue = true;
+		bulkSRS.changes.push_back(csc);
+		currSlot++;
+		check += csc.count;
+	}
+
+	if(check != totalCreatures)
+	{
+		complain((boost::format("Failure: totalCreatures=%d but check=%d") % totalCreatures % check).str());
+		return false;
+	}
+	sendAndApply(&bulkSRS);
+	return true;
+}
+
 bool CGameHandler::arrangeStacks(ObjectInstanceID id1, ObjectInstanceID id2, ui8 what, SlotID p1, SlotID p2, si32 val, PlayerColor player)
 {
 	const CArmedInstance * s1 = static_cast<const CArmedInstance *>(getObjInstance(id1)),
@@ -2970,7 +3226,7 @@ bool CGameHandler::arrangeStacks(ObjectInstanceID id1, ObjectInstanceID id2, ui8
 	StackLocation sl1(s1, p1), sl2(s2, p2);
 	if (!sl1.slot.validSlot()  ||  !sl2.slot.validSlot())
 	{
-		complain("Invalid slot accessed!");
+		complain(complainInvalidSlot);
 		return false;
 	}
 
@@ -3051,8 +3307,8 @@ bool CGameHandler::arrangeStacks(ObjectInstanceID id1, ObjectInstanceID id2, ui8
 		}
 
 		//general conditions checking
-		if ((!vstd::contains(S1.stacks,p1) && complain("no creatures to split"))
-			|| (val<1  && complain("no creatures to split")) )
+		if ((!vstd::contains(S1.stacks,p1) && complain(complainNoCreatures))
+			|| (val<1  && complain(complainNoCreatures)) )
 		{
 			return false;
 		}
@@ -3087,7 +3343,7 @@ bool CGameHandler::arrangeStacks(ObjectInstanceID id1, ObjectInstanceID id2, ui8
 		{
 			if (s1->getStackCount(p1) < val)//not enough creatures
 			{
-				complain("Cannot split that stack, not enough creatures!");
+				complain(complainNotEnoughCreatures);
 				return false;
 			}
 
@@ -6793,7 +7049,11 @@ bool CGameHandler::isBlockedByQueries(const CPack *pack, PlayerColor player)
 	auto query = queries.topQuery(player);
 	if (query && query->blocksPack(pack))
 	{
-		complain(boost::str(boost::format("Player %s has to answer queries  before attempting any further actions. Top query is %s!") % player % query->toString()));
+		complain(boost::str(boost::format(
+			"\r\n| Player \"%s\" has to answer queries before attempting any further actions.\r\n| Top Query: \"%s\"\r\n")
+			% boost::to_upper_copy<std::string>(player.getStr())
+			% query->toString()
+		));
 		return true;
 	}
 

+ 7 - 1
server/CGameHandler.h

@@ -249,6 +249,10 @@ public:
 	bool razeStructure(ObjectInstanceID tid, BuildingID bid);
 	bool disbandCreature( ObjectInstanceID id, SlotID pos );
 	bool arrangeStacks( ObjectInstanceID id1, ObjectInstanceID id2, ui8 what, SlotID p1, SlotID p2, si32 val, PlayerColor player);
+	bool bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot);
+	bool bulkSplitStack(SlotID src, ObjectInstanceID srcOwner, si32 howMany);
+	bool bulkMergeStacks(SlotID slotSrc, ObjectInstanceID srcOwner);
+	bool bulkSmartSplitStack(SlotID slotSrc, ObjectInstanceID srcOwner);
 	void save(const std::string &fname);
 	void load(const std::string &fname);
 
@@ -353,7 +357,9 @@ private:
 	void checkVictoryLossConditions(const std::set<PlayerColor> & playerColors);
 	void checkVictoryLossConditionsForAll();
 
-
+	const std::string complainNoCreatures;
+	const std::string complainNotEnoughCreatures;
+	const std::string complainInvalidSlot;
 };
 
 class ExceptionNotAllowedAction : public std::exception

+ 31 - 1
server/CQuery.cpp

@@ -69,7 +69,25 @@ void CQuery::addPlayer(PlayerColor color)
 
 std::string CQuery::toString() const
 {
-	std::string ret = boost::str(boost::format("A query of type %s and qid=%d affecting players %s") % typeid(*this).name() % queryID % formatContainer(players));
+	const auto size = players.size();
+	const std::string plural = size > 1 ? "s" : "";
+	std::string names;
+
+	for(size_t i = 0; i < size; i++)
+	{
+		names += boost::to_upper_copy<std::string>(players[i].getStr());
+
+		if(i < size - 2)
+			names += ", ";
+		else if(size > 1 && i == size - 2)
+			names += " and ";
+	}
+	std::string ret = boost::str(boost::format("A query of type '%s' and qid = %d affecting player%s %s")
+		% typeid(*this).name()
+		% queryID 
+		% plural
+		% names
+	);
 	return ret;
 }
 
@@ -327,6 +345,18 @@ bool CGarrisonDialogQuery::blocksPack(const CPack * pack) const
 	if(auto stacks = dynamic_ptr_cast<ArrangeStacks>(pack))
 		return !vstd::contains(ourIds, stacks->id1) || !vstd::contains(ourIds, stacks->id2);
 
+	if(auto stacks = dynamic_ptr_cast<BulkSplitStack>(pack))
+		return !vstd::contains(ourIds, stacks->srcOwner);
+
+	if(auto stacks = dynamic_ptr_cast<BulkMergeStacks>(pack))
+		return !vstd::contains(ourIds, stacks->srcOwner);
+
+	if(auto stacks = dynamic_ptr_cast<BulkSmartSplitStack>(pack))
+		return !vstd::contains(ourIds, stacks->srcOwner);
+
+	if(auto stacks = dynamic_ptr_cast<BulkMoveArmy>(pack))
+		return !vstd::contains(ourIds, stacks->srcArmy) || !vstd::contains(ourIds, stacks->destArmy);
+
 	if(auto arts = dynamic_ptr_cast<ExchangeArtifacts>(pack))
 	{
 		if(auto id1 = boost::apply_visitor(GetEngagedHeroIds(), arts->src.artHolder))

+ 20 - 0
server/NetPacksServer.cpp

@@ -123,6 +123,26 @@ bool ArrangeStacks::applyGh(CGameHandler * gh)
 	return gh->arrangeStacks(id1, id2, what, p1, p2, val, gh->getPlayerAt(c));
 }
 
+bool BulkMoveArmy::applyGh(CGameHandler * gh)
+{
+	return gh->bulkMoveArmy(srcArmy, destArmy, srcSlot);
+}
+
+bool BulkSplitStack::applyGh(CGameHandler * gh)
+{
+	return gh->bulkSplitStack(src, srcOwner, amount);
+}
+
+bool BulkMergeStacks::applyGh(CGameHandler* gh)
+{
+	return gh->bulkMergeStacks(src, srcOwner);
+}
+
+bool BulkSmartSplitStack::applyGh(CGameHandler * gh)
+{
+	return gh->bulkSmartSplitStack(src, srcOwner);
+}
+
 bool DisbandCreature::applyGh(CGameHandler * gh)
 {
 	throwOnWrongOwner(gh, id);