Selaa lähdekoodia

Unit stack rebalancing rework

- CStackInstance::count is now private with accessor methods
- CStackInstance::experience renamed to totalExperience and now stores
total stack experience (multiplied by stack size) to reduce rounding
errors
- CStackInstance::totalExperience is now private with accessors methods
- stack experience is now automatically reallocated on stack management
- Removed buggy BulkSmartRebalanceStacks pack, that mostly duplicates
BulkRebalanceStacks
- Renamed BulkSmartSplitStack to BulkSplitAndRebalanceStack to drop
unclear "smart" in name
- Reworked split-and-rebalance logic to correctly reallocate stack
experience
Ivan Savenko 5 kuukautta sitten
vanhempi
sitoutus
283adc37d7
52 muutettua tiedostoa jossa 338 lisäystä ja 327 poistoa
  1. 2 2
      AI/Nullkiller/AIGateway.cpp
  2. 5 5
      AI/Nullkiller/Analyzers/ArmyManager.cpp
  3. 1 1
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  4. 2 2
      AI/Nullkiller/Pathfinding/Actors.cpp
  5. 1 1
      AI/VCAI/ArmyManager.cpp
  6. 1 1
      AI/VCAI/Goals/CompleteQuest.cpp
  7. 1 1
      AI/VCAI/Goals/GatherTroops.cpp
  8. 2 2
      AI/VCAI/VCAI.cpp
  9. 2 2
      CCallback.cpp
  10. 2 2
      CCallback.h
  11. 0 1
      client/ClientNetPackVisitors.h
  12. 1 14
      client/NetPacksClient.cpp
  13. 2 2
      client/UIHelper.cpp
  14. 11 11
      client/widgets/CGarrisonInt.cpp
  15. 1 1
      client/widgets/CGarrisonInt.h
  16. 4 4
      client/widgets/MiscWidgets.cpp
  17. 1 1
      client/windows/CCastleInterface.cpp
  18. 9 9
      client/windows/CCreatureWindow.cpp
  19. 115 65
      lib/CCreatureSet.cpp
  20. 40 11
      lib/CCreatureSet.h
  21. 2 2
      lib/CGameInfoCallback.cpp
  22. 2 2
      lib/CStack.cpp
  23. 1 1
      lib/gameState/CGameState.cpp
  24. 2 2
      lib/gameState/InfoAboutArmy.cpp
  25. 1 1
      lib/json/JsonRandom.cpp
  26. 1 1
      lib/mapObjectConstructors/DwellingInstanceConstructor.cpp
  27. 19 20
      lib/mapObjects/CGCreature.cpp
  28. 1 1
      lib/mapObjects/CGHeroInstance.cpp
  29. 1 1
      lib/mapObjects/CGPandoraBox.cpp
  30. 3 3
      lib/mapObjects/CQuest.cpp
  31. 3 3
      lib/mapObjects/MiscObjects.cpp
  32. 3 3
      lib/mapping/MapFormatH3M.cpp
  33. 1 2
      lib/networkPacks/NetPackVisitor.h
  34. 22 65
      lib/networkPacks/NetPacksLib.cpp
  35. 0 16
      lib/networkPacks/PacksForClient.h
  36. 3 3
      lib/networkPacks/PacksForServer.h
  37. 1 1
      lib/rewardable/Interface.cpp
  38. 3 3
      lib/rewardable/Limiter.cpp
  39. 1 1
      lib/rewardable/Reward.cpp
  40. 2 1
      lib/serializer/ESerializationVersion.h
  41. 1 2
      lib/serializer/RegisterTypes.h
  42. 1 1
      lib/texts/MetaString.cpp
  43. 2 2
      mapeditor/inspector/armywidget.cpp
  44. 2 2
      mapeditor/inspector/inspector.cpp
  45. 2 2
      mapeditor/inspector/questwidget.cpp
  46. 3 3
      mapeditor/inspector/rewardswidget.cpp
  47. 46 39
      server/CGameHandler.cpp
  48. 1 1
      server/CGameHandler.h
  49. 2 2
      server/NetPacksServer.cpp
  50. 1 1
      server/ServerNetPackVisitors.h
  51. 1 1
      server/battles/BattleResultProcessor.cpp
  52. 1 1
      server/queries/MapQueries.cpp

+ 2 - 2
AI/Nullkiller/AIGateway.cpp

@@ -818,11 +818,11 @@ bool AIGateway::makePossibleUpgrades(const CArmedInstance * obj)
 					int oldValue = s->getCreature()->getAIValue();
 					int newValue = upgID.toCreature()->getAIValue();
 
-					if(newValue > oldValue && nullkiller->getFreeResources().canAfford(upgradeInfo.getUpgradeCostsFor(upgID) * s->count))
+					if(newValue > oldValue && nullkiller->getFreeResources().canAfford(upgradeInfo.getUpgradeCostsFor(upgID) * s->getCount()))
 					{
 						myCb->upgradeCreature(obj, SlotID(i), upgID);
 						upgraded = true;
-						logAi->debug("Upgraded %d %s to %s", s->count, upgradeInfo.oldID.toCreature()->getNamePluralTranslated(), 
+						logAi->debug("Upgraded %d %s to %s", s->getCount(), upgradeInfo.oldID.toCreature()->getNamePluralTranslated(),
 							upgradeInfo.getUpgrade().toCreature()->getNamePluralTranslated());
 					}
 					else

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

@@ -98,7 +98,7 @@ std::vector<SlotInfo> ArmyManager::getSortedSlots(const CCreatureSet * target, c
 
 			slotInfp.creature = cre;
 			slotInfp.power += i.second->getPower();
-			slotInfp.count += i.second->count;
+			slotInfp.count += i.second->getCount();
 		}
 	}
 
@@ -491,7 +491,7 @@ void ArmyManager::update()
 	{
 		for(const auto & slot : army->Slots())
 		{
-			totalArmy[slot.second->getCreatureID()].count += slot.second->count;
+			totalArmy[slot.second->getCreatureID()].count += slot.second->getCount();
 		}
 	}
 
@@ -511,7 +511,7 @@ std::vector<SlotInfo> ArmyManager::convertToSlots(const CCreatureSet * army) con
 		SlotInfo slotInfo;
 
 		slotInfo.creature = slot.second->getCreatureID().toCreature();
-		slotInfo.count = slot.second->count;
+		slotInfo.count = slot.second->getCount();
 		slotInfo.power = evaluateStackPower(slotInfo.creature, slotInfo.count);
 
 		result.push_back(slotInfo);
@@ -537,7 +537,7 @@ std::vector<StackUpgradeInfo> ArmyManager::getHillFortUpgrades(const CCreatureSe
 			return cre.toCreature()->getAIValue();
 		});
 
-		StackUpgradeInfo upgrade = StackUpgradeInfo(initial, strongestUpgrade, creature.second->count);
+		StackUpgradeInfo upgrade = StackUpgradeInfo(initial, strongestUpgrade, creature.second->getCount());
 
 		if(initial.toCreature()->getLevel() == 1)
 			upgrade.cost = TResources();
@@ -576,7 +576,7 @@ std::vector<StackUpgradeInfo> ArmyManager::getDwellingUpgrades(const CCreatureSe
 			return cre.toCreature()->getAIValue();
 		});
 
-		StackUpgradeInfo upgrade = StackUpgradeInfo(initial, strongestUpgrade, creature.second->count);
+		StackUpgradeInfo upgrade = StackUpgradeInfo(initial, strongestUpgrade, creature.second->getCount());
 
 		upgrades.push_back(upgrade);
 	}

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

@@ -699,7 +699,7 @@ int32_t getArmyCost(const CArmedInstance * army)
 
 	for(const auto & stack : army->Slots())
 	{
-		value += stack.second->getCreatureID().toCreature()->getFullRecruitCost().marketValue() * stack.second->count;
+		value += stack.second->getCreatureID().toCreature()->getFullRecruitCost().marketValue() * stack.second->getCount();
 	}
 
 	return value;

+ 2 - 2
AI/Nullkiller/Pathfinding/Actors.cpp

@@ -362,7 +362,7 @@ HeroExchangeArmy * HeroExchangeMap::tryUpgrade(
 		{
 			const auto & targetSlot = target->getSlotFor(slot.second->getCreatureID());
 
-			target->addToSlot(targetSlot, slot.second->getCreatureID(), slot.second->count);
+			target->addToSlot(targetSlot, slot.second->getCreatureID(), slot.second->getCount());
 		}
 	}
 
@@ -422,7 +422,7 @@ DwellingActor::DwellingActor(const CGDwelling * dwelling, uint64_t chainMask, bo
 {
 	for(auto & slot : creatureSet->Slots())
 	{
-		armyCost += slot.second->getCreatureID().toCreature()->getFullRecruitCost() * slot.second->count;
+		armyCost += slot.second->getCreatureID().toCreature()->getFullRecruitCost() * slot.second->getCount();
 	}
 }
 

+ 1 - 1
AI/VCAI/ArmyManager.cpp

@@ -41,7 +41,7 @@ std::vector<SlotInfo> ArmyManager::getSortedSlots(const CCreatureSet * target, c
 
 			slotInfp.creature = cre;
 			slotInfp.power += i.second->getPower();
-			slotInfp.count += i.second->count;
+			slotInfp.count += i.second->getCount();
 		}
 	}
 

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

@@ -174,7 +174,7 @@ TGoalVec CompleteQuest::missionArmy() const
 
 	for(auto creature : q.getQuest(cb)->mission.creatures)
 	{
-		solutions.push_back(sptr(GatherTroops(creature.getId().getNum(), creature.count)));
+		solutions.push_back(sptr(GatherTroops(creature.getId().getNum(), creature.getCount())));
 	}
 
 	return solutions;

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

@@ -33,7 +33,7 @@ int GatherTroops::getCreaturesCount(const CArmedInstance * army)
 	{
 		if(objid == stack.second->getCreatureID().num)
 		{
-			count += stack.second->count;
+			count += stack.second->getCount();
 		}
 	}
 

+ 2 - 2
AI/VCAI/VCAI.cpp

@@ -791,10 +791,10 @@ void makePossibleUpgrades(const CArmedInstance * obj)
 						{
 							return id.toCreature()->getAIValue();
 						});
-					if(cb->getResourceAmount().canAfford(upgradeInfo.getUpgradeCostsFor(upgID) * s->count))
+					if(cb->getResourceAmount().canAfford(upgradeInfo.getUpgradeCostsFor(upgID) * s->getCount()))
 					{
 						cb->upgradeCreature(obj, SlotID(i), upgID);
-						logAi->debug("Upgraded %d %s to %s", s->count, upgradeInfo.oldID.toCreature()->getNamePluralTranslated(), 
+						logAi->debug("Upgraded %d %s to %s", s->getCount(), upgradeInfo.oldID.toCreature()->getNamePluralTranslated(),
 							upgradeInfo.getUpgrade().toCreature()->getNamePluralTranslated());
 					}
 					else

+ 2 - 2
CCallback.cpp

@@ -131,9 +131,9 @@ int CCallback::bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMa
 	return 0;
 }
 
-int CCallback::bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot)
+int CCallback::bulkSplitAndRebalanceStack(ObjectInstanceID armyId, SlotID srcSlot)
 {
-	BulkSmartSplitStack pack(armyId, srcSlot);
+	BulkSplitAndRebalanceStack pack(armyId, srcSlot);
 	sendRequest(pack);
 	return 0;
 }

+ 2 - 2
CCallback.h

@@ -113,7 +113,7 @@ public:
 	// 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 bulkSplitAndRebalanceStack(ObjectInstanceID armyId, SlotID srcSlot) = 0;
 	virtual int bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) = 0;
 	
 	
@@ -181,7 +181,7 @@ public:
 	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 bulkSplitAndRebalanceStack(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;

+ 0 - 1
client/ClientNetPackVisitors.h

@@ -46,7 +46,6 @@ public:
 	void visitInsertNewStack(InsertNewStack & pack) override;
 	void visitRebalanceStacks(RebalanceStacks & pack) override;
 	void visitBulkRebalanceStacks(BulkRebalanceStacks & pack) override;
-	void visitBulkSmartRebalanceStacks(BulkSmartRebalanceStacks & pack) override;
 	void visitPutArtifact(PutArtifact & pack) override;
 	void visitEraseArtifact(BulkEraseArtifacts & pack) override;
 	void visitBulkMoveArtifacts(BulkMoveArtifacts & pack) override;

+ 1 - 14
client/NetPacksClient.cpp

@@ -263,19 +263,6 @@ void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & p
 	}
 }
 
-void ApplyClientNetPackVisitor::visitBulkSmartRebalanceStacks(BulkSmartRebalanceStacks & pack)
-{
-	if(!pack.moves.empty())
-	{
-		assert(pack.moves[0].srcArmy == pack.moves[0].dstArmy);
-		dispatchGarrisonChange(cl, pack.moves[0].srcArmy, ObjectInstanceID());
-	}
-	else if(!pack.changes.empty())
-	{
-		dispatchGarrisonChange(cl, pack.changes[0].army, ObjectInstanceID());
-	}
-}
-
 void ApplyClientNetPackVisitor::visitPutArtifact(PutArtifact & pack)
 {
 	callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactPut, pack.al);
@@ -880,7 +867,7 @@ void ApplyClientNetPackVisitor::visitBattleResultsApplied(BattleResultsApplied &
 	if(pack.raisedStack.getCreature())
 		callInterfaceIfPresent(cl, pack.victor, &CGameInterface::showInfoDialog, EInfoWindowMode::AUTO,
 			UIHelper::getNecromancyInfoWindowText(pack.raisedStack), std::vector<Component>{Component(ComponentType::CREATURE, pack.raisedStack.getId(),
-			pack.raisedStack.count)}, UIHelper::getNecromancyInfoWindowSound());
+			pack.raisedStack.getCount())}, UIHelper::getNecromancyInfoWindowSound());
 
 	callInterfaceIfPresent(cl, pack.victor, &IGameEventsReceiver::battleResultsApplied);
 	callInterfaceIfPresent(cl, pack.loser, &IGameEventsReceiver::battleResultsApplied);

+ 2 - 2
client/UIHelper.cpp

@@ -50,10 +50,10 @@ soundBase::soundID UIHelper::getNecromancyInfoWindowSound()
 std::string UIHelper::getNecromancyInfoWindowText(const CStackBasicDescriptor & stack)
 {
     MetaString text;
-    if(stack.count > 1) // Practicing the dark arts of necromancy, ... (plural)
+    if(stack.getCount() > 1) // Practicing the dark arts of necromancy, ... (plural)
     {
         text.appendLocalString(EMetaText::GENERAL_TXT, 145);
-        text.replaceNumber(stack.count);
+        text.replaceNumber(stack.getCount());
     }
     else // Practicing the dark arts of necromancy, ... (singular)
     {

+ 11 - 11
client/widgets/CGarrisonInt.cpp

@@ -255,8 +255,8 @@ bool CGarrisonSlot::split()
 		}
 	}
 
-	int countLeft = selection->myStack ? selection->myStack->count : 0;
-	int countRight = myStack ? myStack->count : 0;
+	int countLeft = selection->myStack ? selection->myStack->getCount() : 0;
+	int countRight = myStack ? myStack->getCount() : 0;
 
 	auto splitFunctor = [this, selection](int amountLeft, int amountRight)
 	{
@@ -345,7 +345,7 @@ void CGarrisonSlot::clickPressed(const Point & cursorPosition)
 				refr = split();
 			}
 			else if(!creature && lastHeroStackSelected) // split all except last creature
-				GAME->interface()->cb->splitStack(selectedObj, owner->army(upg), selection->ID, ID, selection->myStack->count - 1);
+				GAME->interface()->cb->splitStack(selectedObj, owner->army(upg), selection->ID, ID, selection->myStack->getCount() - 1);
 			else if(creature != selection->creature) // swap
 				GAME->interface()->cb->swapCreatures(owner->army(upg), selectedObj, ID, selection->ID);
 			else if(lastHeroStackSelected) // merge last stack to other hero stack
@@ -388,7 +388,7 @@ void CGarrisonSlot::gesture(bool on, const Point & initialPosition, const Point
 		{ RadialMenuConfig::ITEM_NW, hasSameUnit, "stackMerge", "vcmi.radialWheel.mergeSameUnit", [this](){owner->bulkMergeStacks(this);} },
 		{ RadialMenuConfig::ITEM_NE, hasOwnEmptySlots, "stackFillOne", "vcmi.radialWheel.fillSingleUnit", [this](){owner->bulkSplitStack(this);} },
 		{ RadialMenuConfig::ITEM_WW, hasOwnEmptySlots, "stackSplitOne", "vcmi.radialWheel.splitSingleUnit", [this](){splitIntoParts(this->getGarrison(), 1); } },
-		{ RadialMenuConfig::ITEM_EE, hasOwnEmptySlots, "stackSplitEqual", "vcmi.radialWheel.splitUnitEqually", [this](){owner->bulkSmartSplitStack(this);} },
+		{ RadialMenuConfig::ITEM_EE, hasOwnEmptySlots, "stackSplitEqual", "vcmi.radialWheel.splitUnitEqually", [this](){owner->bulkSplitAndRebalanceStack(this);} },
 		{ RadialMenuConfig::ITEM_SW, hasOtherEmptySlots, "heroMove", "vcmi.radialWheel.moveUnit", [this](){owner->moveStackToAnotherArmy(this);} },
 		{ RadialMenuConfig::ITEM_SE, hasAnyEmptySlots, "heroSwap", "vcmi.radialWheel.splitUnit", [this](){ owner->selectSlot(this); owner->splitClick();} },
 	};
@@ -418,7 +418,7 @@ void CGarrisonSlot::update()
 		creatureImage->setFrame(creature->getIconIndex());
 
 		stackCount->enable();
-		stackCount->setText(TextOperations::formatMetric(myStack->count, 4));
+		stackCount->setText(TextOperations::formatMetric(myStack->getCount(), 4));
 	}
 	else
 	{
@@ -499,7 +499,7 @@ bool CGarrisonSlot::handleSplittingShortcuts()
 	if(!selected)
 		return true; // Some Shortcusts are pressed but there are no appropriate actions
 
-	auto units = selected->myStack->count;
+	auto units = selected->myStack->getCount();
 	if(units < 1)
 		return true;
 
@@ -529,7 +529,7 @@ bool CGarrisonSlot::handleSplittingShortcuts()
 		if(isLCtrl && isLShift)
 			owner->bulkSplitStack(selected);
 		else if(isLShift)
-			owner->bulkSmartSplitStack(selected);
+			owner->bulkSplitAndRebalanceStack(selected);
 		else
 			splitIntoParts(selected->upg, 1); // LCtrl
 	}
@@ -603,7 +603,7 @@ void CGarrisonInt::splitStacks(const CGarrisonSlot * from, const CArmedInstance
 
 bool CGarrisonInt::checkSelected(const CGarrisonSlot * selected, TQuantity min) const
 {
-	return selected && selected->myStack && selected->myStack->count > min && selected->creature;
+	return selected && selected->myStack && selected->myStack->getCount() > min && selected->creature;
 }
 
 void CGarrisonInt::moveStackToAnotherArmy(const CGarrisonSlot * selected)
@@ -634,7 +634,7 @@ void CGarrisonInt::moveStackToAnotherArmy(const CGarrisonSlot * selected)
 		destSlot = srcSlot; // Same place is more preferable
 
 	const bool isLastStack = srcArmy->stacksCount() == 1 && srcArmy->needsLastStack();
-	auto srcAmount = selected->myStack->count - (isLastStack ? 1 : 0);
+	auto srcAmount = selected->myStack->getCount() - (isLastStack ? 1 : 0);
 
 	if(!srcAmount)
 		return;
@@ -696,7 +696,7 @@ void CGarrisonInt::bulkSplitStack(const CGarrisonSlot * selected)
 	GAME->interface()->cb->bulkSplitStack(armedObjs[type]->id, selected->ID);
 }
 
-void CGarrisonInt::bulkSmartSplitStack(const CGarrisonSlot * selected)
+void CGarrisonInt::bulkSplitAndRebalanceStack(const CGarrisonSlot * selected)
 {
 	if(!checkSelected(selected, 1))
 		return;
@@ -707,7 +707,7 @@ void CGarrisonInt::bulkSmartSplitStack(const CGarrisonSlot * selected)
 	if(!hasEmptySlot(type) && armedObjs[type]->isCreatureBalanced(selected->creature))
 		return;
 
-	GAME->interface()->cb->bulkSmartSplitStack(armedObjs[type]->id, selected->ID);
+	GAME->interface()->cb->bulkSplitAndRebalanceStack(armedObjs[type]->id, selected->ID);
 }
 
 CGarrisonInt::CGarrisonInt(const Point & position, int inx, const Point & garsOffset, const CArmedInstance * s1, const CArmedInstance * s2, bool _removableUnits, bool smallImgs, ESlotsLayout _layout)

+ 1 - 1
client/widgets/CGarrisonInt.h

@@ -134,7 +134,7 @@ public:
 	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);
+	void bulkSplitAndRebalanceStack(const CGarrisonSlot * selected);
 
 	/// Constructor
 	/// @param position Relative position to parent element

+ 4 - 4
client/widgets/MiscWidgets.cpp

@@ -286,20 +286,20 @@ void CArmyTooltip::init(const InfoAboutArmy &army)
 		std::string subtitle;
 		if(army.army.isDetailed)
 		{
-			subtitle = TextOperations::formatMetric(slot.second.count, 4);
+			subtitle = TextOperations::formatMetric(slot.second.getCount(), 4);
 		}
 		else
 		{
 			//if =0 - we have no information about stack size at all
-			if(slot.second.count)
+			if(slot.second.getCount())
 			{
 				if(settings["gameTweaks"]["numericCreaturesQuantities"].Bool())
 				{
-					subtitle = CCreature::getQuantityRangeStringForId((CCreature::CreatureQuantityId)slot.second.count);
+					subtitle = CCreature::getQuantityRangeStringForId((CCreature::CreatureQuantityId)slot.second.getCount());
 				}
 				else
 				{
-					subtitle = LIBRARY->generaltexth->arraytxt[171 + 3*(slot.second.count)];
+					subtitle = LIBRARY->generaltexth->arraytxt[171 + 3*(slot.second.getCount())];
 				}
 			}
 		}

+ 1 - 1
client/windows/CCastleInterface.cpp

@@ -403,7 +403,7 @@ void CHeroGSlot::gesture(bool on, const Point & initialPosition, const Point & f
 			resComps.push_back(std::make_shared<CComponent>(ComponentType::RESOURCE, static_cast<GameResID>(GameResID::GOLD), 0)); // add at least gold, when there are no costs
 		resComps.back()->newLine = true;
 		for(auto & upgradeInfo : upgradableSlots.upgradeInfos)
-			resComps.push_back(std::make_shared<CComponent>(ComponentType::CREATURE, upgradeInfo.second.getUpgrade(), obj->Slots().at(upgradeInfo.first)->count));
+			resComps.push_back(std::make_shared<CComponent>(ComponentType::CREATURE, upgradeInfo.second.getUpgrade(), obj->Slots().at(upgradeInfo.first)->getCount()));
 			
 		std::string textID = upgradableSlots.canAffordAll ? "core.genrltxt.207" : "vcmi.townWindow.upgradeAll.notAllUpgradable";
 

+ 9 - 9
client/windows/CCreatureWindow.cpp

@@ -664,7 +664,7 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s
 			area->component.value = commander->getExpRank();
 			boost::replace_first(area->text, "%d", std::to_string(commander->getExpRank()));
 			boost::replace_first(area->text, "%d", std::to_string(LIBRARY->heroh->reqExp(commander->getExpRank() + 1)));
-			boost::replace_first(area->text, "%d", std::to_string(commander->experience));
+			boost::replace_first(area->text, "%d", std::to_string(commander->getAverageExperience()));
 		}
 		else
 		{
@@ -674,7 +674,7 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s
 		}
 		expLabel = std::make_shared<CLabel>(
 				pos.x + 21, pos.y + 55, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE,
-				TextOperations::formatMetric(stack->experience, 6));
+				TextOperations::formatMetric(stack->getAverageExperience(), 6));
 	}
 
 	if(showArt)
@@ -764,7 +764,7 @@ CStackWindow::CStackWindow(const CStackInstance * stack, bool popup)
 {
 	info->stackNode = stack;
 	info->creature = stack->getCreature();
-	info->creatureCount = stack->count;
+	info->creatureCount = stack->getCount();
 	info->popupWindow = popup;
 	info->owner = dynamic_cast<const CGHeroInstance *> (stack->getArmy());
 	init();
@@ -776,7 +776,7 @@ CStackWindow::CStackWindow(const CStackInstance * stack, std::function<void()> d
 {
 	info->stackNode = stack;
 	info->creature = stack->getCreature();
-	info->creatureCount = stack->count;
+	info->creatureCount = stack->getCount();
 
 	if(upgradeInfo.canUpgrade())
 	{
@@ -968,8 +968,8 @@ std::string CStackWindow::generateStackExpDescription()
 	boost::replace_first(expText, "%s", creature->getNamePluralTranslated());
 	boost::replace_first(expText, "%s", LIBRARY->generaltexth->translate("vcmi.stackExperience.rank", rank));
 	boost::replace_first(expText, "%i", std::to_string(rank));
-	boost::replace_first(expText, "%i", std::to_string(stack->experience));
-	number = static_cast<int>(LIBRARY->creh->expRanks[tier][rank] - stack->experience);
+	boost::replace_first(expText, "%i", std::to_string(stack->getAverageExperience()));
+	number = static_cast<int>(LIBRARY->creh->expRanks[tier][rank] - stack->getAverageExperience());
 	boost::replace_first(expText, "%i", std::to_string(number));
 
 	number = LIBRARY->creh->maxExpPerBattle[tier]; //percent
@@ -977,10 +977,10 @@ std::string CStackWindow::generateStackExpDescription()
 	number *= LIBRARY->creh->expRanks[tier].back() / 100; //actual amount
 	boost::replace_first(expText, "%i", std::to_string(number));
 
-	boost::replace_first(expText, "%i", std::to_string(stack->count)); //Number of Creatures in stack
+	boost::replace_first(expText, "%i", std::to_string(stack->getCount())); //Number of Creatures in stack
 
 	int expmin = std::max(LIBRARY->creh->expRanks[tier][std::max(rank-1, 0)], (ui32)1);
-	number = static_cast<int>((stack->count * (stack->experience - expmin)) / expmin); //Maximum New Recruits without losing current Rank
+	number = stack->getTotalExperience() / expmin - stack->getCount(); //Maximum New Recruits without losing current Rank
 	boost::replace_first(expText, "%i", std::to_string(number)); //TODO
 
 	boost::replace_first(expText, "%.2f", std::to_string(1)); //TODO Experience Multiplier
@@ -991,7 +991,7 @@ std::string CStackWindow::generateStackExpDescription()
 	int expmax = LIBRARY->creh->expRanks[tier][10];
 	number = expmax - expmin;
 	boost::replace_first(expText, "%i", std::to_string(number)); //Experience after Rank 10
-	number = (stack->count * (expmax - expmin)) / expmin;
+	number = (stack->getCount() * (expmax - expmin)) / expmin;
 	boost::replace_first(expText, "%i", std::to_string(number)); //Maximum New Recruits to remain at Rank 10 if at Maximum Experience
 
 	return expText;

+ 115 - 65
lib/CCreatureSet.cpp

@@ -125,7 +125,7 @@ std::vector<SlotID> CCreatureSet::getCreatureSlots(const CCreature * c, const Sl
 		if(!elem.second || !elem.second->getType() || elem.second->getType() != c)
 			continue;
 
-		if(elem.second->count == ignoreAmount || elem.second->count < 1)
+		if(elem.second->getCount() == ignoreAmount || elem.second->getCount() < 1)
 			continue;
 
 		result.push_back(elem.first);
@@ -144,7 +144,7 @@ bool CCreatureSet::isCreatureBalanced(const CCreature * c, TQuantity ignoreAmoun
 		if(!elem.second || !elem.second->getType() || elem.second->getType() != c)
 			continue;
 
-		const auto count = elem.second->count;
+		const auto count = elem.second->getCount();
 
 		if(count == ignoreAmount || count < 1)
 			continue;
@@ -236,20 +236,19 @@ TCreatureQueue CCreatureSet::getCreatureQueue(const SlotID & exclude) const
 
 TQuantity CCreatureSet::getStackCount(const SlotID & slot) const
 {
-	auto i = stacks.find(slot);
-	if (i != stacks.end())
-		return i->second->count;
-	else
-		return 0; //TODO? consider issuing a warning
+	if (!hasStackAtSlot(slot))
+		return 0;
+	return stacks.at(slot)->getCount();
 }
 
-TExpType CCreatureSet::getStackExperience(const SlotID & slot) const
+TExpType CCreatureSet::getStackTotalExperience(const SlotID & slot) const
 {
-	auto i = stacks.find(slot);
-	if (i != stacks.end())
-		return i->second->experience;
-	else
-		return 0; //TODO? consider issuing a warning
+	return stacks.at(slot)->getTotalExperience();
+}
+
+TExpType CCreatureSet::getStackAverageExperience(const SlotID & slot) const
+{
+	return stacks.at(slot)->getAverageExperience();
 }
 
 bool CCreatureSet::mergeableStacks(std::pair<SlotID, SlotID> & out, const SlotID & preferable) const /*looks for two same stacks, returns slot positions */
@@ -284,19 +283,6 @@ bool CCreatureSet::mergeableStacks(std::pair<SlotID, SlotID> & out, const SlotID
 	return false;
 }
 
-void CCreatureSet::sweep()
-{
-	for(auto i=stacks.begin(); i!=stacks.end(); ++i)
-	{
-		if(!i->second->count)
-		{
-			stacks.erase(i);
-			sweep();
-			break;
-		}
-	}
-}
-
 void CCreatureSet::addToSlot(const SlotID & slot, const CreatureID & cre, TQuantity count, bool allowMerging)
 {
 	const CCreature *c = cre.toCreature();
@@ -437,23 +423,24 @@ void CCreatureSet::setFormation(EArmyFormation mode)
 
 void CCreatureSet::setStackCount(const SlotID & slot, TQuantity count)
 {
-	assert(hasStackAtSlot(slot));
-	assert(stacks[slot]->count + count > 0);
-	if (count > stacks[slot]->count)
-		stacks[slot]->experience = static_cast<TExpType>(stacks[slot]->experience * (count / static_cast<double>(stacks[slot]->count)));
-	stacks[slot]->count = count;
+	stacks.at(slot)->setCount(count);
 	armyChanged();
 }
 
-void CCreatureSet::giveStackExp(TExpType exp)
+void CCreatureSet::giveAverageStackExperience(TExpType exp)
 {
-	for(TSlots::const_iterator i = stacks.begin(); i != stacks.end(); i++)
-		i->second->giveStackExp(exp);
+	for(const auto & stack : stacks)
+	{
+		stack.second->giveAverageStackExperience(exp);
+		stack.second->nodeHasChanged();
+	}
 }
-void CCreatureSet::setStackExp(const SlotID & slot, TExpType exp)
+
+void CCreatureSet::giveTotalStackExperience(const SlotID & slot, TExpType exp)
 {
 	assert(hasStackAtSlot(slot));
-	stacks[slot]->experience = exp;
+	stacks[slot]->giveTotalStackExperience(exp);
+	stacks[slot]->nodeHasChanged();
 }
 
 void CCreatureSet::clearSlots()
@@ -528,7 +515,23 @@ void CCreatureSet::joinStack(const SlotID & slot, std::unique_ptr<CStackInstance
 	assert(c);
 
 	//TODO move stuff
-	changeStackCount(slot, stack->count);
+	changeStackCount(slot, stack->getCount());
+	giveTotalStackExperience(slot, stack->getTotalExperience());
+}
+
+std::unique_ptr<CStackInstance> CCreatureSet::splitStack(const SlotID & slot, TQuantity toSplit)
+{
+	auto & currentStack = stacks.at(slot);
+	assert(currentStack->getCount() > toSplit);
+
+	TExpType experienceBefore = currentStack->getTotalExperience();
+	currentStack->setCount(currentStack->getCount() - toSplit);
+	TExpType experienceAfter = currentStack->getTotalExperience();
+
+	auto newStack = std::make_unique<CStackInstance>(currentStack->cb, currentStack->getCreatureID(), toSplit);
+	newStack->giveTotalStackExperience(experienceBefore - experienceAfter);
+
+	return newStack;
 }
 
 void CCreatureSet::changeStackCount(const SlotID & slot, TQuantity toAdd)
@@ -674,14 +677,13 @@ void CCreatureSet::serializeJson(JsonSerializeFormat & handler, const std::strin
 
 CStackInstance::CStackInstance(IGameCallback *cb, bool isHypothetic)
 	: CBonusSystemNode(isHypothetic)
+	, CStackBasicDescriptor(nullptr, 0)
 	, CArtifactSet(cb)
 	, GameCallbackHolder(cb)
 	, nativeTerrain(this, Selector::type()(BonusType::TERRAIN_NATIVE))
 	, initiative(this, Selector::type()(BonusType::STACKS_SPEED))
+	, totalExperience(0)
 {
-	experience = 0;
-	count = 0;
-	setType(nullptr);
 	setNodeType(STACK_INSTANCE);
 }
 
@@ -689,12 +691,12 @@ CStackInstance::CStackInstance(IGameCallback *cb, const CreatureID & id, TQuanti
 	: CStackInstance(cb, false)
 {
 	setType(id);
-	count = Count;
+	setCount(Count);
 }
 
 CCreature::CreatureQuantityId CStackInstance::getQuantityID() const
 {
-	return CCreature::getQuantityID(count);
+	return CCreature::getQuantityID(getCount());
 }
 
 int CStackInstance::getExpRank() const
@@ -706,7 +708,7 @@ int CStackInstance::getExpRank() const
 	{
 		for(int i = static_cast<int>(LIBRARY->creh->expRanks[tier].size()) - 2; i > -1; --i) //sic!
 		{ //exp values vary from 1st level to max exp at 11th level
-			if (experience >= LIBRARY->creh->expRanks[tier][i])
+			if (getAverageExperience() >= LIBRARY->creh->expRanks[tier][i])
 				return ++i; //faster, but confusing - 0 index mean 1st level of experience
 		}
 		return 0;
@@ -715,7 +717,7 @@ int CStackInstance::getExpRank() const
 	{
 		for(int i = static_cast<int>(LIBRARY->creh->expRanks[0].size()) - 2; i > -1; --i)
 		{
-			if (experience >= LIBRARY->creh->expRanks[0][i])
+			if (getAverageExperience() >= LIBRARY->creh->expRanks[0][i])
 				return ++i;
 		}
 		return 0;
@@ -727,17 +729,47 @@ int CStackInstance::getLevel() const
 	return std::max(1, getType()->getLevel());
 }
 
-void CStackInstance::giveStackExp(TExpType exp)
+void CStackInstance::giveAverageStackExperience(TExpType desiredAmountPerUnit)
+{
+	if (!canGainExperience())
+		return;
+
+	int level = std::clamp(getLevel(), 1, 7);
+	TExpType maxAmountPerUnit = LIBRARY->creh->expRanks[level].back();
+	TExpType actualAmountPerUnit = std::min(desiredAmountPerUnit, maxAmountPerUnit * LIBRARY->creh->maxExpPerBattle[level]/100);
+	TExpType maxExperience = maxAmountPerUnit * getCount();
+	TExpType maxExperienceToGain = maxExperience - totalExperience;
+	TExpType actualGainedExperience = std::min(maxExperienceToGain, actualAmountPerUnit * getCount());
+
+	totalExperience	+= actualGainedExperience;
+}
+
+void CStackInstance::giveTotalStackExperience(TExpType experienceToGive)
 {
-	int level = getType()->getLevel();
-	if (!vstd::iswithin(level, 1, 7))
-		level = 0;
+	if (!canGainExperience())
+		return;
 
-	ui32 maxExp = LIBRARY->creh->expRanks[level].back();
+	int level = std::clamp(getLevel(), 1, 7);
+	TExpType maxAmountPerUnit = LIBRARY->creh->expRanks[level].back();
+	TExpType maxExperience = maxAmountPerUnit * getCount();
+	TExpType maxExperienceToGain = maxExperience - totalExperience;
+	TExpType actualGainedExperience = std::min(maxExperienceToGain, experienceToGive);
+	totalExperience	+= actualGainedExperience;
+}
 
-	vstd::amin(exp, static_cast<TExpType>(maxExp)); //prevent exp overflow due to different types
-	vstd::amin(exp, (maxExp * LIBRARY->creh->maxExpPerBattle[level])/100);
-	vstd::amin(experience += exp, maxExp); //can't get more exp than this limit
+TExpType CStackInstance::getTotalExperience() const
+{
+	return totalExperience;
+}
+
+TExpType CStackInstance::getAverageExperience() const
+{
+	return totalExperience / getCount();
+}
+
+bool CStackInstance::canGainExperience() const
+{
+	return cb->getSettings().getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE);
 }
 
 void CStackInstance::setType(const CreatureID & creID)
@@ -753,8 +785,8 @@ void CStackInstance::setType(const CCreature *c)
 	if(getCreature())
 	{
 		detachFromSource(*getCreature());
-		if (getCreature()->isMyUpgrade(c) && LIBRARY->engineSettings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE))
-			experience = static_cast<TExpType>(experience * LIBRARY->creh->expAfterUpgrade / 100.0);
+		if (LIBRARY->engineSettings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE))
+			totalExperience = totalExperience * LIBRARY->creh->expAfterUpgrade / 100;
 	}
 
 	CStackBasicDescriptor::setType(c);
@@ -762,6 +794,20 @@ void CStackInstance::setType(const CCreature *c)
 	if(getCreature())
 		attachToSource(*getCreature());
 }
+
+void CStackInstance::setCount(TQuantity newCount)
+{
+	assert(newCount >= 0);
+
+	if (newCount < getCount())
+	{
+		TExpType averageExperience = totalExperience / getCount();
+		totalExperience = averageExperience * newCount;
+	}
+
+	CStackBasicDescriptor::setCount(newCount);
+}
+
 std::string CStackInstance::bonusToString(const std::shared_ptr<Bonus>& bonus, bool description) const
 {
 	return LIBRARY->getBth()->bonusToString(bonus, this, description);
@@ -831,7 +877,7 @@ bool CStackInstance::valid(bool allowUnrandomized) const
 std::string CStackInstance::nodeName() const
 {
 	std::ostringstream oss;
-	oss << "Stack of " << count << " of ";
+	oss << "Stack of " << getCount() << " of ";
 	if(getType())
 		oss << getType()->getNamePluralTextID();
 	else
@@ -877,19 +923,19 @@ CreatureID CStackInstance::getCreatureID() const
 
 std::string CStackInstance::getName() const
 {
-	return (count > 1) ? getType()->getNamePluralTranslated() : getType()->getNameSingularTranslated();
+	return (getCount() > 1) ? getType()->getNamePluralTranslated() : getType()->getNameSingularTranslated();
 }
 
 ui64 CStackInstance::getPower() const
 {
 	assert(getType());
-	return static_cast<ui64>(getType()->getAIValue()) * count;
+	return static_cast<ui64>(getType()->getAIValue()) * getCount();
 }
 
 ui64 CStackInstance::getMarketValue() const
 {
 	assert(getType());
-	return getType()->getFullRecruitCost().marketValue() * count;
+	return getType()->getFullRecruitCost().marketValue() * getCount();
 }
 
 ArtBearer::ArtBearer CStackInstance::bearerType() const
@@ -968,9 +1014,8 @@ CCommanderInstance::CCommanderInstance(IGameCallback *cb, const CreatureID & id)
 	, name("Commando")
 {
 	alive = true;
-	experience = 0;
 	level = 1;
-	count = 1;
+	setCount(1);
 	setType(nullptr);
 	setNodeType (CBonusSystemNode::COMMANDER);
 	secondarySkills.resize (ECommander::SPELL_POWER + 1);
@@ -988,15 +1033,14 @@ void CCommanderInstance::setAlive (bool Alive)
 	}
 }
 
-void CCommanderInstance::giveStackExp (TExpType exp)
+bool CCommanderInstance::canGainExperience() const
 {
-	if (alive)
-		experience += exp;
+	return alive && CStackInstance::canGainExperience();
 }
 
 int CCommanderInstance::getExpRank() const
 {
-	return LIBRARY->heroh->level (experience);
+	return LIBRARY->heroh->level (getTotalExperience());
 }
 
 int CCommanderInstance::getLevel() const
@@ -1020,7 +1064,7 @@ ArtBearer::ArtBearer CCommanderInstance::bearerType() const
 
 bool CCommanderInstance::gainsLevel() const
 {
-	return experience >= LIBRARY->heroh->reqExp(level + 1);
+	return getTotalExperience() >= LIBRARY->heroh->reqExp(level + 1);
 }
 
 //This constructor should be placed here to avoid side effects
@@ -1062,6 +1106,12 @@ void CStackBasicDescriptor::setType(const CCreature * c)
 	typeID = c ? c->getId() : CreatureID();
 }
 
+void CStackBasicDescriptor::setCount(TQuantity newCount)
+{
+	assert(newCount >= 0);
+	count = newCount;
+}
+
 bool operator== (const CStackBasicDescriptor & l, const CStackBasicDescriptor & r)
 {
 	return l.typeID == r.typeID && l.count == r.count;

+ 40 - 11
lib/CCreatureSet.h

@@ -33,9 +33,9 @@ class JsonSerializeFormat;
 class DLL_LINKAGE CStackBasicDescriptor
 {
 	CreatureID typeID;
-public:
 	TQuantity count = -1; //exact quantity or quantity ID from CCreature::getQuantityID when getting info about enemy army
 
+public:
 	CStackBasicDescriptor();
 	CStackBasicDescriptor(const CreatureID & id, TQuantity Count);
 	CStackBasicDescriptor(const CCreature *c, TQuantity Count);
@@ -47,6 +47,7 @@ public:
 	TQuantity getCount() const;
 
 	virtual void setType(const CCreature * c);
+	virtual void setCount(TQuantity amount);
 
 	friend bool operator== (const CStackBasicDescriptor & l, const CStackBasicDescriptor & r);
 
@@ -78,6 +79,8 @@ class DLL_LINKAGE CStackInstance : public CBonusSystemNode, public CStackBasicDe
 	ObjectInstanceID armyInstanceID; //stack must be part of some army, army must be part of some object
 
 	IGameCallback * getCallback() const final { return cb; }
+
+	TExpType totalExperience;//commander needs same amount of exp as hero
 public:
 	struct RandomStackInfo
 	{
@@ -91,7 +94,9 @@ public:
 	const CArmedInstance * getArmy() const; //stack must be part of some army, army must be part of some object
 	void setArmy(const CArmedInstance *ArmyObj);
 
-	TExpType experience;//commander needs same amount of exp as hero
+	TExpType getTotalExperience() const;
+	TExpType getAverageExperience() const;
+	virtual bool canGainExperience() const;
 
 	template <typename Handler> void serialize(Handler &h)
 	{
@@ -109,7 +114,12 @@ public:
 			h & army;
 			armyInstanceID = army->id;
 		}
-		h & experience;
+
+		h & totalExperience;
+		if (!h.hasFeature(Handler::Version::STACK_INSTANCE_EXPERIENCE_FIX))
+		{
+			totalExperience *= getCount();
+		}
 	}
 
 	void serializeJson(JsonSerializeFormat & handler);
@@ -138,7 +148,12 @@ public:
 
 	void setType(const CreatureID & creID);
 	void setType(const CCreature * c) final;
-	virtual void giveStackExp(TExpType exp);
+	void setCount(TQuantity amount) final;
+
+	/// Gives specified amount of stack experience that will not be scaled by unit size
+	void giveAverageStackExperience(TExpType exp);
+	void giveTotalStackExperience(TExpType exp);
+
 	bool valid(bool allowUnrandomized) const;
 	ArtPlacementMap putArtifact(const ArtifactPosition & pos, const CArtifactInstance * art) override;//from CArtifactSet
 	void removeArtifact(const ArtifactPosition & pos) override;
@@ -166,9 +181,9 @@ public:
 	CCommanderInstance(IGameCallback *cb);
 	CCommanderInstance(IGameCallback *cb, const CreatureID & id);
 	void setAlive (bool alive);
-	void giveStackExp (TExpType exp) override;
 	void levelUp ();
 
+	bool canGainExperience() const override;
 	bool gainsLevel() const; //true if commander has lower level than should upon his experience
 	ui64 getPower() const override {return 0;};
 	int getExpRank() const override;
@@ -255,12 +270,26 @@ public:
 	void setStackCount(const SlotID & slot, TQuantity count); //stack must exist!
 	std::unique_ptr<CStackInstance> detachStack(const SlotID & slot); //removes stack from army but doesn't destroy it (so it can be moved somewhere else or safely deleted)
 	void setStackType(const SlotID & slot, const CreatureID & type);
-	void giveStackExp(TExpType exp);
-	void setStackExp(const SlotID & slot, TExpType exp);
 
-	//derivative
-	void eraseStack(const SlotID & slot); //slot must be occupied
+	/// Give specified amount of experience to all units in army
+	/// Amount of granted experience is scaled by unit stack size
+	void giveAverageStackExperience(TExpType exp);
+
+	/// Give specified amount of experience to unit in specified slot
+	/// Amount of granted experience is not scaled by unit stack size
+	void giveTotalStackExperience(const SlotID & slot, TExpType exp);
+
+	/// Erased stack from specified slot. Slot must be non-empty
+	void eraseStack(const SlotID & slot);
+
+	/// Joins stack into stack that occupies targeted slot.
+	/// Slot must be non-empty and contain same creature type
 	void joinStack(const SlotID & slot, std::unique_ptr<CStackInstance> stack); //adds new stack to the existing stack of the same type
+
+	/// Splits off some units of specified stack and returns newly created stack
+	/// Slot must be non-empty and contain more units that split quantity
+	std::unique_ptr<CStackInstance> splitStack(const SlotID & slot, TQuantity toSplit);
+
 	void changeStackCount(const SlotID & slot, TQuantity toAdd); //stack must exist!
 	bool setCreature (SlotID slot, CreatureID type, TQuantity quantity) override; //replaces creature in stack; slots 0 to 6, if quantity=0 erases stack
 	void setToArmy(CSimpleArmy &src); //erases all our army and moves stacks from src to us; src MUST NOT be an armed object! WARNING: use it wisely. Or better do not use at all.
@@ -269,7 +298,8 @@ public:
 	CStackInstance * getStackPtr(const SlotID & slot) const; //if stack doesn't exist, returns nullptr
 	const CCreature * getCreature(const SlotID & slot) const; //workaround of map issue;
 	int getStackCount(const SlotID & slot) const;
-	TExpType getStackExperience(const SlotID & slot) const;
+	TExpType getStackTotalExperience(const SlotID & slot) const;
+	TExpType getStackAverageExperience(const SlotID & slot) const;
 	SlotID findStack(const CStackInstance *stack) const; //-1 if none
 	SlotID getSlotFor(const 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
@@ -311,7 +341,6 @@ public:
 	{
 		return !stacks.empty();
 	}
-	void sweep();
 };
 
 VCMI_LIB_NAMESPACE_END

+ 2 - 2
lib/CGameInfoCallback.cpp

@@ -360,13 +360,13 @@ bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero
 			doBasicDisguise(info);
 
 			for(auto & elem : info.army)
-				elem.second.count = 0;
+				elem.second.setCount(0);
 		};
 
 		auto doExpertDisguise = [this,h](InfoAboutHero & info)
 		{
 			for(auto & elem : info.army)
-				elem.second.count = 0;
+				elem.second.setCount(0);
 
 			const auto factionIndex = getStartInfo()->playerInfos.at(h->tempOwner).castle;
 

+ 2 - 2
lib/CStack.cpp

@@ -30,7 +30,7 @@ CStack::CStack(const CStackInstance * Base, const PlayerColor & O, int I, Battle
 	base(Base),
 	ID(I),
 	typeID(Base->getId()),
-	baseAmount(Base->count),
+	baseAmount(Base->getCount()),
 	owner(O),
 	slot(S),
 	side(Side)
@@ -51,7 +51,7 @@ CStack::CStack(const CStackBasicDescriptor * stack, const PlayerColor & O, int I
 	CBonusSystemNode(STACK_BATTLE),
 	ID(I),
 	typeID(stack->getId()),
-	baseAmount(stack->count),
+	baseAmount(stack->getCount()),
 	owner(O),
 	slot(S),
 	side(Side)

+ 1 - 1
lib/gameState/CGameState.cpp

@@ -1304,7 +1304,7 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio
 				{
 					for(const auto & elem : ai->Slots()) //iterate through army
 						if(elem.second->getId() == condition.objectType.as<CreatureID>()) //it's searched creature
-							total += elem.second->count;
+							total += elem.second->getCount();
 				}
 			}
 			return total >= condition.value;

+ 2 - 2
lib/gameState/InfoAboutArmy.cpp

@@ -43,12 +43,12 @@ int ArmyDescriptor::getStrength() const
 	if(isDetailed)
 	{
 		for(const auto & elem : *this)
-			ret += elem.second.getType()->getAIValue() * elem.second.count;
+			ret += elem.second.getType()->getAIValue() * elem.second.getCount();
 	}
 	else
 	{
 		for(const auto & elem : *this)
-			ret += elem.second.getType()->getAIValue() * CCreature::estimateCreatureCount(elem.second.count);
+			ret += elem.second.getType()->getAIValue() * CCreature::estimateCreatureCount(elem.second.getCount());
 	}
 	return static_cast<int>(ret);
 }

+ 1 - 1
lib/json/JsonRandom.cpp

@@ -504,7 +504,7 @@ JsonRandomizationException::JsonRandomizationException(const std::string & messa
 			throw JsonRandomizationException("Invalid creature picked!", value);
 
 		stack.setType(pickedCreature.toCreature());
-		stack.count = loadValue(value, rng, variables);
+		stack.setCount(loadValue(value, rng, variables));
 		if (!value["upgradeChance"].isNull() && !stack.getCreature()->upgrades.empty())
 		{
 			if (int(value["upgradeChance"].Float()) > rng.nextInt(99)) // select random upgrade

+ 1 - 1
lib/mapObjectConstructors/DwellingInstanceConstructor.cpp

@@ -104,7 +104,7 @@ void DwellingInstanceConstructor::randomizeObject(CGDwelling * dwelling, vstd::R
 		JsonRandom::Variables emptyVariables;
 		for(auto & stack : randomizer.loadCreatures(guards, rng, emptyVariables))
 		{
-			dwelling->putStack(SlotID(dwelling->stacksCount()), std::make_unique<CStackInstance>(dwelling->cb, stack.getId(), stack.count));
+			dwelling->putStack(SlotID(dwelling->stacksCount()), std::make_unique<CStackInstance>(dwelling->cb, stack.getId(), stack.getCount()));
 		}
 	}
 	else if (dwelling->ID == Obj::CREATURE_GENERATOR1 || dwelling->ID == Obj::CREATURE_GENERATOR4)

+ 19 - 20
lib/mapObjects/CGCreature.cpp

@@ -55,9 +55,9 @@ std::string CGCreature::getHoverText(const CGHeroInstance * hero) const
 	if(hero->hasVisions(this, BonusCustomSubtype::visionsMonsters))
 	{
 		MetaString ms;
-		ms.appendNumber(stacks.begin()->second->count);
+		ms.appendNumber(stacks.begin()->second->getCount());
 		ms.appendRawString(" ");
-		ms.appendName(getCreatureID(), stacks.begin()->second->count);
+		ms.appendName(getCreatureID(), stacks.begin()->second->getCount());
 		return ms.toString();
 	}
 	else
@@ -288,20 +288,19 @@ void CGCreature::initObj(vstd::RNG & rand)
 	}
 
 	stacks[SlotID(0)]->setType(getCreature());
-	TQuantity &amount = stacks[SlotID(0)]->count;
 	const Creature * c = getCreature();
-	if(amount == 0)
+	if(stacks[SlotID(0)]->getCount() == 0)
 	{
-		amount = rand.nextInt(c->getAdvMapAmountMin(), c->getAdvMapAmountMax());
+		stacks[SlotID(0)]->setCount(rand.nextInt(c->getAdvMapAmountMin(), c->getAdvMapAmountMax()));
 
-		if(amount == 0) //armies with 0 creatures are illegal
+		if(stacks[SlotID(0)]->getCount() == 0) //armies with 0 creatures are illegal
 		{
 			logGlobal->warn("Stack cannot have 0 creatures. Check properties of %s", c->getJsonKey());
-			amount = 1;
+			stacks[SlotID(0)]->setCount(1);
 		}
 	}
 
-	temppower = stacks[SlotID(0)]->count * static_cast<int64_t>(1000);
+	temppower = stacks[SlotID(0)]->getCount() * static_cast<int64_t>(1000);
 	refusedJoining = false;
 }
 
@@ -309,7 +308,7 @@ void CGCreature::newTurn(vstd::RNG & rand) const
 {//Works only for stacks of single type of size up to 2 millions
 	if (!notGrowingTeam)
 	{
-		if (stacks.begin()->second->count < cb->getSettings().getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_CAP) && cb->getDate(Date::DAY_OF_WEEK) == 1 && cb->getDate(Date::DAY) > 1)
+		if (stacks.begin()->second->getCount() < cb->getSettings().getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_CAP) && cb->getDate(Date::DAY_OF_WEEK) == 1 && cb->getDate(Date::DAY) > 1)
 		{
 			ui32 power = static_cast<ui32>(temppower * (100 + cb->getSettings().getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT)) / 100);
 			cb->setObjPropertyValue(id, ObjProperty::MONSTER_COUNT, std::min<uint32_t>(power / 1000, cb->getSettings().getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_CAP))); //set new amount
@@ -324,13 +323,13 @@ void CGCreature::setPropertyDer(ObjProperty what, ObjPropertyID identifier)
 	switch (what)
 	{
 		case ObjProperty::MONSTER_COUNT:
-			stacks[SlotID(0)]->count = identifier.getNum();
+			stacks[SlotID(0)]->setCount(identifier.getNum());
 			break;
 		case ObjProperty::MONSTER_POWER:
 			temppower = identifier.getNum();
 			break;
 		case ObjProperty::MONSTER_EXP:
-			giveStackExp(identifier.getNum());
+			giveAverageStackExperience(identifier.getNum());
 			break;
 		case ObjProperty::MONSTER_REFUSED_JOIN:
 			refusedJoining = identifier.getNum();
@@ -367,8 +366,8 @@ int CGCreature::takenAction(const CGHeroInstance *h, bool allowJoin) const
 		bool isOurDowngrade = vstd::contains(elem.second->getCreature()->upgrades, getCreatureID());
 
 		if(isOurUpgrade || isOurDowngrade)
-			count += elem.second->count;
-		totalCount += elem.second->count;
+			count += elem.second->getCount();
+		totalCount += elem.second->getCount();
 	}
 
 	int sympathy = 0; // 0 if hero have no similar creatures
@@ -455,7 +454,7 @@ void CGCreature::joinDecision(const CGHeroInstance *h, int cost, ui32 accept) co
 		giveReward(h);
 
 		for(auto & stack : this->stacks)
-			stack.second->count = getJoiningAmount();
+			stack.second->setCount(getJoiningAmount());
 
 		cb->tryJoiningArmy(this, h, true, true);
 	}
@@ -538,7 +537,7 @@ void CGCreature::battleFinished(const CGHeroInstance *hero, const BattleResult &
 
 		//first stack has to be at slot 0 -> if original one got killed, move there first remaining stack
 		if(!hasStackAtSlot(SlotID(0)))
-			cb->moveStack(StackLocation(id, stacks.begin()->first), StackLocation(id, SlotID(0)), stacks.begin()->second->count);
+			cb->moveStack(StackLocation(id, stacks.begin()->first), StackLocation(id, SlotID(0)), stacks.begin()->second->getCount());
 
 		while(stacks.size() > 1) //hopefully that's enough
 		{
@@ -549,10 +548,10 @@ void CGCreature::battleFinished(const CGHeroInstance *hero, const BattleResult &
 			if(slot == i->first) //no reason to move stack to its own slot
 				break;
 			else
-				cb->moveStack(StackLocation(id, i->first), StackLocation(id, slot), i->second->count);
+				cb->moveStack(StackLocation(id, i->first), StackLocation(id, slot), i->second->getCount());
 		}
 
-		cb->setObjPropertyValue(id, ObjProperty::MONSTER_POWER, stacks.begin()->second->count * 1000); //remember casualties
+		cb->setObjPropertyValue(id, ObjProperty::MONSTER_POWER, stacks.begin()->second->getCount() * 1000); //remember casualties
 	}
 }
 
@@ -615,7 +614,7 @@ int CGCreature::getNumberOfStacks(const CGHeroInstance *hero) const
 	else if (R4 >= 80)
 		split += 1;
 
-	vstd::amin(split, getStack(SlotID(0)).count); //can't divide into more stacks than creatures total
+	vstd::amin(split, getStack(SlotID(0)).getCount()); //can't divide into more stacks than creatures total
 	vstd::amin(split, 7); //can't have more than 7 stacks
 
 	return split;
@@ -664,7 +663,7 @@ void CGCreature::serializeJsonOptions(JsonSerializeFormat & handler)
 	{
 		if(hasStackAtSlot(SlotID(0)))
 		{
-			si32 amount = getStack(SlotID(0)).count;
+			si32 amount = getStack(SlotID(0)).getCount();
 			handler.serializeInt("amount", amount, 0);
 		}
 	}
@@ -673,7 +672,7 @@ void CGCreature::serializeJsonOptions(JsonSerializeFormat & handler)
 		si32 amount = 0;
 		handler.serializeInt("amount", amount);
 		auto hlp = std::make_unique<CStackInstance>(cb);
-		hlp->count = amount;
+		hlp->setCount(amount);
 		//type will be set during initialization
 		putStack(SlotID(0), std::move(hlp));
 	}

+ 1 - 1
lib/mapObjects/CGHeroInstance.cpp

@@ -468,7 +468,7 @@ void CGHeroInstance::initHero(vstd::RNG & rand)
 	{
 		commander = std::make_unique<CCommanderInstance>(cb, getHeroClass()->commander);
 		commander->setArmy(getArmy()); //TODO: separate function for setting commanders
-		commander->giveStackExp (exp); //after our exp is set
+		commander->giveTotalStackExperience(exp); //after our exp is set
 	}
 
 	skillsInfo = SecondarySkillsInfo();

+ 1 - 1
lib/mapObjects/CGPandoraBox.cpp

@@ -138,7 +138,7 @@ void CGPandoraBox::grantRewardWithMessage(const CGHeroInstance * h, int index, b
 			loot.replaceName(c);
 		}
 		
-		if(vi.reward.creatures.size() == 1 && vi.reward.creatures[0].count == 1)
+		if(vi.reward.creatures.size() == 1 && vi.reward.creatures[0].getCount() == 1)
 			txt.appendLocalString(EMetaText::ADVOB_TXT, 185);
 		else
 			txt.appendLocalString(EMetaText::ADVOB_TXT, 186);

+ 3 - 3
lib/mapObjects/CQuest.cpp

@@ -112,15 +112,15 @@ bool CQuest::checkMissionArmy(const CQuest * q, const CCreatureSet * army)
 		{
 			if(it->second->getType() == cre->getType())
 			{
-				count += it->second->count;
+				count += it->second->getCount();
 				slotsCount++;
 			}
 		}
 
-		if(static_cast<TQuantity>(count) < cre->count) //not enough creatures of this kind
+		if(static_cast<TQuantity>(count) < cre->getCount()) //not enough creatures of this kind
 			return false;
 
-		hasExtraCreatures |= static_cast<TQuantity>(count) > cre->count;
+		hasExtraCreatures |= static_cast<TQuantity>(count) > cre->getCount();
 	}
 
 	return hasExtraCreatures || slotsCount < army->Slots().size();

+ 3 - 3
lib/mapObjects/MiscObjects.cpp

@@ -609,7 +609,7 @@ void CGWhirlpool::teleportDialogAnswered(const CGHeroInstance *hero, ui32 answer
 bool CGWhirlpool::isProtected(const CGHeroInstance * h)
 {
 	return h->hasBonusOfType(BonusType::WHIRLPOOL_PROTECTION)
-		|| (h->stacksCount() == 1 && h->Slots().begin()->second->count == 1)
+		|| (h->stacksCount() == 1 && h->Slots().begin()->second->getCount() == 1)
 		|| (h->stacksCount() == 0 && h->getCommander() && h->getCommander()->alive);
 }
 
@@ -1040,11 +1040,11 @@ void CGSirens::onHeroVisit( const CGHeroInstance * h ) const
 		for (auto i = h->Slots().begin(); i != h->Slots().end(); i++)
 		{
 			// 1-sized stacks are not affected by sirens
-			if (i->second->count == 1)
+			if (i->second->getCount() == 1)
 				continue;
 
 			// tested H3 behavior: 30% (rounded up) of stack drowns
-			TQuantity drown = std::ceil(i->second->count * 0.3);
+			TQuantity drown = std::ceil(i->second->getCount() * 0.3);
 
 			if(drown)
 			{

+ 3 - 3
lib/mapping/MapFormatH3M.cpp

@@ -1191,7 +1191,7 @@ std::shared_ptr<CGObjectInstance> CMapLoaderH3M::readMonster(const int3 & mapPos
 	}
 
 	auto hlp = std::make_unique<CStackInstance>(map->cb);
-	hlp->count = reader->readUInt16();
+	hlp->setCount(reader->readUInt16());
 
 	//type will be set during initialization
 	object->putStack(SlotID(0), std::move(hlp));
@@ -1975,7 +1975,7 @@ void CMapLoaderH3M::readCreatureSet(CArmedInstance * out, const ObjectInstanceID
 			continue;
 
 		auto result = std::make_unique<CStackInstance>(map->cb);
-		result->count = count;
+		result->setCount(count);
 
 		if(creatureID < CreatureID::NONE)
 		{
@@ -2427,7 +2427,7 @@ EQuestMission CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & positi
 			for(size_t hh = 0; hh < typeNumber; ++hh)
 			{
 				guard->getQuest().mission.creatures[hh].setType(reader->readCreature().toCreature());
-				guard->getQuest().mission.creatures[hh].count = reader->readUInt16();
+				guard->getQuest().mission.creatures[hh].setCount(reader->readUInt16());
 			}
 			break;
 		}

+ 1 - 2
lib/networkPacks/NetPackVisitor.h

@@ -77,7 +77,6 @@ public:
 	virtual void visitInsertNewStack(InsertNewStack & pack) {}
 	virtual void visitRebalanceStacks(RebalanceStacks & pack) {}
 	virtual void visitBulkRebalanceStacks(BulkRebalanceStacks & pack) {}
-	virtual void visitBulkSmartRebalanceStacks(BulkSmartRebalanceStacks & pack) {}
 	virtual void visitPutArtifact(PutArtifact & pack) {}
 	virtual void visitEraseArtifact(BulkEraseArtifacts & pack) {}
 	virtual void visitBulkMoveArtifacts(BulkMoveArtifacts & pack) {}
@@ -124,7 +123,7 @@ public:
 	virtual void visitBulkMoveArmy(BulkMoveArmy & pack) {}
 	virtual void visitBulkSplitStack(BulkSplitStack & pack) {}
 	virtual void visitBulkMergeStacks(BulkMergeStacks & pack) {}
-	virtual void visitBulkSmartSplitStack(BulkSmartSplitStack & pack) {}
+	virtual void visitBulkSmartSplitStack(BulkSplitAndRebalanceStack & pack) {}
 	virtual void visitDisbandCreature(DisbandCreature & pack) {}
 	virtual void visitBuildStructure(BuildStructure & pack) {}
 	virtual void visitVisitTownBuilding(VisitTownBuilding & pack) {}

+ 22 - 65
lib/networkPacks/NetPacksLib.cpp

@@ -333,11 +333,6 @@ void BulkRebalanceStacks::visitTyped(ICPackVisitor & visitor)
 	visitor.visitBulkRebalanceStacks(*this);
 }
 
-void BulkSmartRebalanceStacks::visitTyped(ICPackVisitor & visitor)
-{
-	visitor.visitBulkSmartRebalanceStacks(*this);
-}
-
 void PutArtifact::visitTyped(ICPackVisitor & visitor)
 {
 	visitor.visitPutArtifact(*this);
@@ -573,7 +568,7 @@ void BulkMergeStacks::visitTyped(ICPackVisitor & visitor)
 	visitor.visitBulkMergeStacks(*this);
 }
 
-void BulkSmartSplitStack::visitTyped(ICPackVisitor & visitor)
+void BulkSplitAndRebalanceStack::visitTyped(ICPackVisitor & visitor)
 {
 	visitor.visitBulkSmartSplitStack(*this);
 }
@@ -895,7 +890,7 @@ void SetCommanderProperty::applyGs(CGameState *gs)
 				commander->setAlive(false);
 			break;
 		case EXPERIENCE:
-			commander->giveStackExp(amount); //TODO: allow setting exp for stacks via netpacks
+			commander->giveTotalStackExperience(amount);
 			commander->nodeHasChanged();
 			break;
 	}
@@ -1560,17 +1555,15 @@ void RebalanceStacks::applyGs(CGameState *gs)
 	StackLocation src(srcObj->id, srcSlot);
 	StackLocation dst(dstObj->id, dstSlot);
 
-	const CCreature * srcType = srcObj->getCreature(src.slot);
+	[[maybe_unused]] const CCreature * srcType = srcObj->getCreature(src.slot);
+	const CCreature * dstType = dstObj->getCreature(dst.slot);
 	TQuantity srcCount = srcObj->getStackCount(src.slot);
-	bool stackExp = gs->getSettings().getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE);
 
 	if(srcCount == count) //moving whole stack
 	{
-		const auto c = dstObj->getCreature(dst.slot);
-
-		if(c) //stack at dest -> merge
+		if(dstType) //stack at dest -> merge
 		{
-			assert(c == srcType);
+			assert(dstType == srcType);
 			
 			const auto srcHero = dynamic_cast<CGHeroInstance*>(srcObj);
 			const auto dstHero = dynamic_cast<CGHeroInstance*>(dstObj);
@@ -1609,50 +1602,28 @@ void RebalanceStacks::applyGs(CGameState *gs)
 					gs->getMap().moveArtifactInstance(*srcStack, ArtifactPosition::CREATURE_SLOT, *dstStack, ArtifactPosition::CREATURE_SLOT);
 				}
 			}
-			if (stackExp)
-			{
-				ui64 totalExp = srcCount * srcObj->getStackExperience(src.slot) + dstObj->getStackCount(dst.slot) * dstObj->getStackExperience(dst.slot);
-				srcObj->eraseStack(src.slot);
-				dstObj->changeStackCount(dst.slot, count);
-				dstObj->setStackExp(dst.slot, totalExp /(dstObj->getStackCount(dst.slot))); //mean
-			}
-			else
-			{
-				srcObj->eraseStack(src.slot);
-				dstObj->changeStackCount(dst.slot, count);
-			}
+
+			auto movedStack = srcObj->detachStack(src.slot);
+			dstObj->joinStack(dst.slot, std::move(movedStack));
 		}
-		else //move stack to an empty slot, no exp change needed
+		else
 		{
-			auto stackDetached = srcObj->detachStack(src.slot);
-			dstObj->putStack(dst.slot, std::move(stackDetached));
+			auto movedStack = srcObj->detachStack(src.slot);
+			dstObj->putStack(dst.slot, std::move(movedStack));
 		}
 	}
 	else
 	{
-		[[maybe_unused]] const CCreature *c = dstObj->getCreature(dst.slot);
-		if(c) //stack at dest -> rebalance
+		auto movedStack = srcObj->splitStack(src.slot, count);
+
+		if(dstType) //stack at dest -> rebalance
 		{
-			assert(c == srcType);
-			if (stackExp)
-			{
-				ui64 totalExp = srcCount * srcObj->getStackExperience(src.slot) + dstObj->getStackCount(dst.slot) * dstObj->getStackExperience(dst.slot);
-				srcObj->changeStackCount(src.slot, -count);
-				dstObj->changeStackCount(dst.slot, count);
-				dstObj->setStackExp(dst.slot, totalExp /(srcObj->getStackCount(src.slot) + dstObj->getStackCount(dst.slot))); //mean
-			}
-			else
-			{
-				srcObj->changeStackCount(src.slot, -count);
-				dstObj->changeStackCount(dst.slot, count);
-			}
+			assert(dstType == srcType);
+			dstObj->joinStack(dst.slot, std::move(movedStack));
 		}
-		else //split stack to an empty slot
+		else //move new stack to an empty slot
 		{
-			srcObj->changeStackCount(src.slot, -count);
-			dstObj->addToSlot(dst.slot, srcType->getId(), count, false);
-			if (stackExp)
-				dstObj->setStackExp(dst.slot, srcObj->getStackExperience(src.slot));
+			dstObj->putStack(dst.slot, std::move(movedStack));
 		}
 	}
 
@@ -1667,15 +1638,6 @@ void BulkRebalanceStacks::applyGs(CGameState *gs)
 		move.applyGs(gs);
 }
 
-void BulkSmartRebalanceStacks::applyGs(CGameState *gs)
-{
-	for(auto & move : moves)
-		move.applyGs(gs);
-
-	for(auto & change : changes)
-		change.applyGs(gs);
-}
-
 void PutArtifact::applyGs(CGameState *gs)
 {
 	auto art = gs->getArtInstance(id);
@@ -2094,15 +2056,10 @@ void BattleResultAccepted::applyGs(CGameState *gs)
 	if(gs->getSettings().getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE))
 	{
 		if(const auto attackerArmy = gs->getArmyInstance(heroResult[BattleSide::ATTACKER].armyID))
-		{
-			attackerArmy->giveStackExp(heroResult[BattleSide::ATTACKER].exp);
-			attackerArmy->nodeHasChanged();
-		}
+			attackerArmy->giveAverageStackExperience(heroResult[BattleSide::ATTACKER].exp);
+
 		if(const auto defenderArmy = gs->getArmyInstance(heroResult[BattleSide::DEFENDER].armyID))
-		{
-			defenderArmy->giveStackExp(heroResult[BattleSide::DEFENDER].exp);
-			defenderArmy->nodeHasChanged();
-		}
+			defenderArmy->giveAverageStackExperience(heroResult[BattleSide::DEFENDER].exp);
 	}
 }
 

+ 0 - 16
lib/networkPacks/PacksForClient.h

@@ -968,22 +968,6 @@ struct DLL_LINKAGE BulkRebalanceStacks : CGarrisonOperationPack
 	}
 };
 
-struct DLL_LINKAGE BulkSmartRebalanceStacks : CGarrisonOperationPack
-{
-	std::vector<RebalanceStacks> moves;
-	std::vector<ChangeStackCount> changes;
-
-	void applyGs(CGameState * gs) override;
-	void visitTyped(ICPackVisitor & visitor) override;
-
-	template <typename Handler>
-	void serialize(Handler & h)
-	{
-		h & moves;
-		h & changes;
-	}
-};
-
 struct DLL_LINKAGE CArtifactOperationPack : CPackForClient
 {
 };

+ 3 - 3
lib/networkPacks/PacksForServer.h

@@ -214,14 +214,14 @@ struct DLL_LINKAGE BulkMergeStacks : public CPackForServer
 	}
 };
 
-struct DLL_LINKAGE BulkSmartSplitStack : public CPackForServer
+struct DLL_LINKAGE BulkSplitAndRebalanceStack : public CPackForServer
 {
 	SlotID src;
 	ObjectInstanceID srcOwner;
 
-	BulkSmartSplitStack() = default;
+	BulkSplitAndRebalanceStack() = default;
 
-	BulkSmartSplitStack(const ObjectInstanceID & srcOwner, const SlotID & src)
+	BulkSplitAndRebalanceStack(const ObjectInstanceID & srcOwner, const SlotID & src)
 		: src(src)
 		, srcOwner(srcOwner)
 	{

+ 1 - 1
lib/rewardable/Interface.cpp

@@ -197,7 +197,7 @@ void Rewardable::Interface::grantRewardAfterLevelup(const Rewardable::VisitInfo
 	{
 		CCreatureSet creatures;
 		for(const auto & crea : info.reward.creatures)
-			creatures.addToSlot(creatures.getFreeSlot(), std::make_unique<CStackInstance>(cb, crea.getId(), crea.count));
+			creatures.addToSlot(creatures.getFreeSlot(), std::make_unique<CStackInstance>(cb, crea.getId(), crea.getCount()));
 
 		if(auto * army = dynamic_cast<const CArmedInstance*>(this)) //TODO: to fix that, CArmedInstance must be split on map instance part and interface part
 			cb->giveCreatures(army, hero, creatures, false);

+ 3 - 3
lib/rewardable/Limiter.cpp

@@ -85,9 +85,9 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const
 		{
 			const auto & heroStack = slot.second;
 			if (heroStack->getType() == reqStack.getType())
-				count += heroStack->count;
+				count += heroStack->getCount();
 		}
-		if (count < reqStack.count) //not enough creatures of this kind
+		if (count < reqStack.getCount()) //not enough creatures of this kind
 			return false;
 	}
 
@@ -233,7 +233,7 @@ void Rewardable::Limiter::loadComponents(std::vector<Component> & comps,
 		comps.emplace_back(ComponentType::SPELL, entry);
 
 	for(const auto & entry : creatures)
-		comps.emplace_back(ComponentType::CREATURE, entry.getId(), entry.count);
+		comps.emplace_back(ComponentType::CREATURE, entry.getId(), entry.getCount());
 	
 	for(const auto & entry : players)
 		comps.emplace_back(ComponentType::FLAG, entry);

+ 1 - 1
lib/rewardable/Reward.cpp

@@ -121,7 +121,7 @@ void Rewardable::Reward::loadComponents(std::vector<Component> & comps, const CG
 	}
 
 	for(const auto & entry : creatures)
-		comps.emplace_back(ComponentType::CREATURE, entry.getId(), entry.count);
+		comps.emplace_back(ComponentType::CREATURE, entry.getId(), entry.getCount());
 
 	for (size_t i=0; i<resources.size(); i++)
 	{

+ 2 - 1
lib/serializer/ESerializationVersion.h

@@ -36,8 +36,9 @@ enum class ESerializationVersion : int32_t
 
 	MAP_HEADER_DISPOSED_HEROES, // map header contains disposed heroes list
 	NO_RAW_POINTERS_IN_SERIALIZER, // large rework that removed all non-owning pointers from serializer
+	STACK_INSTANCE_EXPERIENCE_FIX, // stack experience is stored as total, not as average
 	
-	CURRENT = NO_RAW_POINTERS_IN_SERIALIZER,
+	CURRENT = STACK_INSTANCE_EXPERIENCE_FIX,
 };
 
 static_assert(ESerializationVersion::MINIMAL <= ESerializationVersion::CURRENT, "Invalid serialization version definition!");

+ 1 - 2
lib/serializer/RegisterTypes.h

@@ -226,7 +226,6 @@ void registerTypes(Serializer &s)
 	s.template registerType<BulkMoveArtifacts>(173);
 	s.template registerType<PlayerMessageClient>(174);
 	s.template registerType<BulkRebalanceStacks>(175);
-	s.template registerType<BulkSmartRebalanceStacks>(176);
 	s.template registerType<SetRewardableConfiguration>(177);
 	s.template registerType<CPackForServer>(179);
 	s.template registerType<EndTurn>(180);
@@ -255,7 +254,7 @@ void registerTypes(Serializer &s)
 	s.template registerType<PlayerMessage>(203);
 	s.template registerType<BulkSplitStack>(204);
 	s.template registerType<BulkMergeStacks>(205);
-	s.template registerType<BulkSmartSplitStack>(206);
+	s.template registerType<BulkSplitAndRebalanceStack>(206);
 	s.template registerType<BulkMoveArmy>(207);
 	s.template registerType<BulkExchangeArtifacts>(208);
 	s.template registerType<ManageBackpackArtifacts>(209);

+ 1 - 1
lib/texts/MetaString.cpp

@@ -446,7 +446,7 @@ void MetaString::replaceName(const CreatureID & id, TQuantity count) //adds sing
 
 void MetaString::replaceName(const CStackBasicDescriptor & stack)
 {
-	replaceName(stack.getId(), stack.count);
+	replaceName(stack.getId(), stack.getCount());
 }
 
 VCMI_LIB_NAMESPACE_END

+ 2 - 2
mapeditor/inspector/armywidget.cpp

@@ -155,8 +155,8 @@ void ArmyDelegate::updateModelData(QAbstractItemModel * model, const QModelIndex
 	QStringList textList;
 	for(const auto & [_, stack] : army.stacks)
 	{
-		if(stack->count != 0 && stack->getCreature() != nullptr)
-			textList += QString("%1 %2").arg(stack->count).arg(QString::fromStdString(stack->getCreature()->getNamePluralTranslated()));
+		if(stack->getCount() != 0 && stack->getCreature() != nullptr)
+			textList += QString("%1 %2").arg(stack->getCount()).arg(QString::fromStdString(stack->getCreature()->getNamePluralTranslated()));
 	}
 
 	setModelTextData(model, index, textList);

+ 2 - 2
mapeditor/inspector/inspector.cpp

@@ -423,7 +423,7 @@ void Inspector::updateProperties(CGCreature * o)
 	addProperty(QObject::tr("Not growing"), o->notGrowingTeam, false);
 	addProperty(QObject::tr("Artifact reward"), o->gainedArtifact); //TODO: implement in setProperty
 	addProperty(QObject::tr("Army"), PropertyEditorPlaceholder(), true);
-	addProperty(QObject::tr("Amount"), o->stacks[SlotID(0)]->count, false);
+	addProperty(QObject::tr("Amount"), o->stacks[SlotID(0)]->getCount(), false);
 	//addProperty(QObject::tr("Resources reward"), o->resources); //TODO: implement in setProperty
 }
 
@@ -763,7 +763,7 @@ void Inspector::setProperty(CGCreature * o, const QString & key, const QVariant
 	if(key == QObject::tr("Not growing"))
 		o->notGrowingTeam = value.toBool();
 	if(key == QObject::tr("Amount"))
-		o->stacks[SlotID(0)]->count = value.toString().toInt();
+		o->stacks[SlotID(0)]->setCount(value.toString().toInt());
 }
 
 void Inspector::setProperty(CGSeerHut * o, const QString & key, const QVariant & value)

+ 2 - 2
mapeditor/inspector/questwidget.cpp

@@ -175,7 +175,7 @@ void QuestWidget::obtainData()
 	{
 		int index = i.getType()->getIndex();
 		ui->lCreatureId->setCurrentIndex(index);
-		ui->lCreatureAmount->setValue(i.count);
+		ui->lCreatureAmount->setValue(i.getCount());
 		onCreatureAdd(ui->lCreatures, ui->lCreatureId, ui->lCreatureAmount);
 	}
 	for(auto & i : quest.mission.heroes)
@@ -489,7 +489,7 @@ void QuestDelegate::updateModelData(QAbstractItemModel * model, const QModelInde
 	QStringList creaturesList;
 	for(const auto & creature : quest.mission.creatures)
 	{
-		creaturesList += QString("%1 %2").arg(creature.count).arg(QString::fromStdString(creature.getType()->getNamePluralTranslated()));
+		creaturesList += QString("%1 %2").arg(creature.getCount()).arg(QString::fromStdString(creature.getType()->getNamePluralTranslated()));
 	}
 	textList += QObject::tr("Creatures: %1").arg(creaturesList.join(", "));
 

+ 3 - 3
mapeditor/inspector/rewardswidget.cpp

@@ -466,7 +466,7 @@ void RewardsWidget::loadCurrentVisitInfo(int index)
 	{
 		int index = i.getType()->getIndex();
 		ui->rCreatureId->setCurrentIndex(index);
-		ui->rCreatureAmount->setValue(i.count);
+		ui->rCreatureAmount->setValue(i.getCount());
 		onCreatureAdd(ui->rCreatures, ui->rCreatureId, ui->rCreatureAmount);
 	}
 	
@@ -534,7 +534,7 @@ void RewardsWidget::loadCurrentVisitInfo(int index)
 	{
 		int index = i.getType()->getIndex();
 		ui->lCreatureId->setCurrentIndex(index);
-		ui->lCreatureAmount->setValue(i.count);
+		ui->lCreatureAmount->setValue(i.getCount());
 		onCreatureAdd(ui->lCreatures, ui->lCreatureId, ui->lCreatureAmount);
 	}
 	
@@ -806,7 +806,7 @@ void RewardsDelegate::updateModelData(QAbstractItemModel * model, const QModelIn
 		QStringList creaturesList;
 		for (auto & creature : vinfo.reward.creatures)
 		{
-			creaturesList += QString("%1 %2").arg(creature.count).arg(QString::fromStdString(creature.getType()->getNamePluralTranslated()));
+			creaturesList += QString("%1 %2").arg(creature.getCount()).arg(QString::fromStdString(creature.getType()->getNamePluralTranslated()));
 		}
 		textList += QObject::tr("Creatures: %1").arg(creaturesList.join(", "));
 		if (vinfo.reward.spellCast.first != SpellID::NONE)

+ 46 - 39
server/CGameHandler.cpp

@@ -1151,7 +1151,7 @@ void CGameHandler::giveCreatures(const CArmedInstance *obj, const CGHeroInstance
 	//first we move creatures to give to make them army of object-source
 	for (auto & elem : creatures.Slots())
 	{
-		addToSlot(StackLocation(obj->id, obj->getSlotFor(elem.second->getCreature())), elem.second->getCreature(), elem.second->count);
+		addToSlot(StackLocation(obj->id, obj->getSlotFor(elem.second->getCreature())), elem.second->getCreature(), elem.second->getCount());
 	}
 
 	tryJoiningArmy(obj, h, remove, true);
@@ -1167,14 +1167,14 @@ void CGameHandler::takeCreatures(ObjectInstanceID objid, const std::vector<CStac
 	for (CStackBasicDescriptor &sbd : cres)
 	{
 		TQuantity collected = 0;
-		while(collected < sbd.count)
+		while(collected < sbd.getCount())
 		{
 			bool foundSth = false;
 			for (auto i = obj->Slots().begin(); i != obj->Slots().end(); i++)
 			{
 				if (i->second->getType() == sbd.getType())
 				{
-					TQuantity take = std::min(sbd.count - collected, i->second->count); //collect as much cres as we can
+					TQuantity take = std::min(sbd.getCount() - collected, i->second->getCount()); //collect as much cres as we can
 					changeStackCount(StackLocation(obj->id, i->first), -take, false);
 					collected += take;
 					foundSth = true;
@@ -1789,7 +1789,7 @@ bool CGameHandler::bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destA
 	return true;
 }
 
-bool CGameHandler::bulkSmartSplitStack(SlotID slotSrc, ObjectInstanceID srcOwner)
+bool CGameHandler::bulkSplitAndRebalanceStack(SlotID slotSrc, ObjectInstanceID srcOwner)
 {
 	if(!slotSrc.validSlot() && complain(complainInvalidSlot))
 		return false;
@@ -1811,8 +1811,8 @@ bool CGameHandler::bulkSmartSplitStack(SlotID slotSrc, ObjectInstanceID srcOwner
 	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;
+	auto creatureSlots = creatureSet.getCreatureSlots(currentCreature, slotSrc, 1); // Ignore slots where's only 1 creature
+	TQuantity totalCreatures = creatureSet.getStackCount(slotSrc);
 
 	for(auto slot : creatureSlots)
 		totalCreatures += creatureSet.getStackCount(slot);
@@ -1820,53 +1820,60 @@ bool CGameHandler::bulkSmartSplitStack(SlotID slotSrc, ObjectInstanceID srcOwner
 	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;
+	BulkRebalanceStacks bulkSRS;
 
-	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
+	// 1) merge all but one creatures back into source slot
+	// single creature needs to be kept, to avoid stack artifact dropping to hero backpack
+	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) - 1;
 
-	BulkSmartRebalanceStacks bulkSRS;
+		if (rs.count > 0)
+			bulkSRS.moves.push_back(rs);
+	}
 
+	// 2) split off single creature into new slot, if any
+	// strictly speaking, not needed, but more convenient
 	if(freeSlot != SlotID())
 	{
 		RebalanceStacks rs;
-		rs.srcArmy = rs.dstArmy = army->id;
+		rs.srcArmy = army->id;
+		rs.dstArmy = army->id;
 		rs.srcSlot = slotSrc;
 		rs.dstSlot = freeSlot;
 		rs.count = 1;
 		bulkSRS.moves.push_back(rs);
+
+		creatureSlots.push_back(freeSlot);
 	}
-	auto currSlot = 0;
-	auto check = 0;
 
+	if(creatureSlots.empty() && complain("No available slots for smart rebalancing"))
+		return false;
+
+	int slotsLeft = creatureSlots.size() + 1; // + srcSlot
+	TQuantity unitsToMove = totalCreatures - slotsLeft;
+
+	// 3) re-split creatures in a balanced way
 	for(auto slot : creatureSlots)
 	{
-		ChangeStackCount csc;
+		RebalanceStacks rs;
 
-		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;
-	}
+		rs.srcArmy = army->id;
+		rs.dstArmy = army->id;
+		rs.srcSlot = slotSrc;
+		rs.dstSlot = slot;
+		rs.count = vstd::divideAndCeil(unitsToMove, slotsLeft);
+		bulkSRS.moves.push_back(rs);
 
-	if(check != totalCreatures)
-	{
-		complain((boost::format("Failure: totalCreatures=%d but check=%d") % totalCreatures % check).str());
-		return false;
+		unitsToMove -= rs.count;
+		slotsLeft -= 1;
 	}
+
 	sendAndApply(bulkSRS);
 	return true;
 }
@@ -2435,7 +2442,7 @@ bool CGameHandler::upgradeCreature(ObjectInstanceID objid, SlotID pos, CreatureI
 	fillUpgradeInfo(obj, pos, upgradeInfo);
 	PlayerColor player = obj->tempOwner;
 	const PlayerState *p = getPlayerState(player);
-	int crQuantity = obj->stacks.at(pos)->count;
+	int crQuantity = obj->stacks.at(pos)->getCount();
 
 	//check if upgrade is possible
 	if (!upgradeInfo.hasUpgrades() && complain("That upgrade is not possible!"))
@@ -3167,8 +3174,8 @@ bool CGameHandler::sellCreatures(ui32 count, const IMarket *market, const CGHero
 
 	const CStackInstance &s = hero->getStack(slot);
 
-	if (s.count < (TQuantity)count //can't sell more creatures than have
-		|| (hero->stacksCount() == 1 && hero->needsLastStack() && s.count == count)) //can't sell last stack
+	if (s.getCount() < static_cast<TQuantity>(count) //can't sell more creatures than have
+		|| (hero->stacksCount() == 1 && hero->needsLastStack() && s.getCount() == count)) //can't sell last stack
 	{
 		COMPLAIN_RET("Not enough creatures in army!");
 	}

+ 1 - 1
server/CGameHandler.h

@@ -227,7 +227,7 @@ public:
 	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);
+	bool bulkSplitAndRebalanceStack(SlotID slotSrc, ObjectInstanceID srcOwner);
 	void save(const std::string &fname);
 	bool load(const std::string &fname);
 

+ 2 - 2
server/NetPacksServer.cpp

@@ -124,12 +124,12 @@ void ApplyGhNetPackVisitor::visitBulkMergeStacks(BulkMergeStacks & pack)
 	result = gh.bulkMergeStacks(pack.src, pack.srcOwner);
 }
 
-void ApplyGhNetPackVisitor::visitBulkSmartSplitStack(BulkSmartSplitStack & pack)
+void ApplyGhNetPackVisitor::visitBulkSmartSplitStack(BulkSplitAndRebalanceStack & pack)
 {
 	gh.throwIfWrongPlayer(connection, &pack);
 	gh.throwIfPlayerNotActive(connection, &pack);
 
-	result = gh.bulkSmartSplitStack(pack.src, pack.srcOwner);
+	result = gh.bulkSplitAndRebalanceStack(pack.src, pack.srcOwner);
 }
 
 void ApplyGhNetPackVisitor::visitDisbandCreature(DisbandCreature & pack)

+ 1 - 1
server/ServerNetPackVisitors.h

@@ -41,7 +41,7 @@ public:
 	void visitBulkMoveArmy(BulkMoveArmy & pack) override;
 	void visitBulkSplitStack(BulkSplitStack & pack) override;
 	void visitBulkMergeStacks(BulkMergeStacks & pack) override;
-	void visitBulkSmartSplitStack(BulkSmartSplitStack & pack) override;
+	void visitBulkSmartSplitStack(BulkSplitAndRebalanceStack & pack) override;
 	void visitDisbandCreature(DisbandCreature & pack) override;
 	void visitBuildStructure(BuildStructure & pack) override;
 	void visitSpellResearch(SpellResearch & pack) override;

+ 1 - 1
server/battles/BattleResultProcessor.cpp

@@ -487,7 +487,7 @@ void BattleResultProcessor::battleFinalize(const BattleID & battleID, const Batt
 		resultsApplied.raisedStack = winnerHero->calculateNecromancy(result);
 		const SlotID necroSlot = resultsApplied.raisedStack.getCreature() ? winnerHero->getSlotFor(resultsApplied.raisedStack.getCreature()) : SlotID();
 		if(necroSlot != SlotID() && !finishingBattle->isDraw())
-			gameHandler->addToSlot(StackLocation(finishingBattle->winnerId, necroSlot), resultsApplied.raisedStack.getCreature(), resultsApplied.raisedStack.count);
+			gameHandler->addToSlot(StackLocation(finishingBattle->winnerId, necroSlot), resultsApplied.raisedStack.getCreature(), resultsApplied.raisedStack.getCount());
 	}
 
 	resultsApplied.battleID = battleID;

+ 1 - 1
server/queries/MapQueries.cpp

@@ -73,7 +73,7 @@ bool CGarrisonDialogQuery::blocksPack(const CPackForServer * pack) const
 	if(auto stacks = dynamic_cast<const BulkMergeStacks*>(pack))
 		return !vstd::contains(ourIds, stacks->srcOwner);
 
-	if(auto stacks = dynamic_cast<const BulkSmartSplitStack*>(pack))
+	if(auto stacks = dynamic_cast<const BulkSplitAndRebalanceStack*>(pack))
 		return !vstd::contains(ourIds, stacks->srcOwner);
 
 	if(auto stacks = dynamic_cast<const BulkMoveArmy*>(pack))